Repository navigation
feat(hexmap): uniform object spreader (L7) - #130
Merged
Merged
Conversation
Spreader::generate picks exactly `count` walkable tiles, uniformly and deterministically for a seed, on any grid (open edges included). Hash-and-mask with an exact fix-up: two Bernoulli mask rounds built from Poseidon words (density in 1/16, then in 1/32 on the deficit), rejection sampling for the last tiles, uniform trim on overshoot, complement trick above half of the walkable tiles, and a u128 path on boards of at most 128 bits. 20 objects on CAVE_17X14 cost 170k (target 300k); an open 19x13 board with 35 objects costs 181k against 13.65M for origami_map. Selection sampling, Floyd rank selection, plain rejection and a one-round mask are kept test-only in bench_spreader.cairo with their budgets; the measurements are in GAS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit A2 finding 1: the rejection fix-up of the first spreader had no iteration bound (2 walkable tiles on a 10x25 board: 1.33M gas on average, 9.3M at worst over 256 seeds). Spreader::generate now runs a radix select on random keys: one Poseidon word per level splits the current class with one AND and one popcount, and a class of at most 8 tiles is finished by a uniform subset from a 511-entry table deposited in at most 8 steps. Small counts are picked one by one when a measured cost rule says it is cheaper: at most 2 rejection trials, then a loop-free select from SWAR byte counts kept up to date across removals, or a walk on sets of at most 8 tiles. Every loop has a stated bound. Over 256 seeds, 20 objects on CAVE_17X14 cost 191k mean, 241k max (was 177k / 465k), the sparse grids at most 83k, and no case exceeds 1.84x its mean. Findings 2 and 3 (random source biases) are quantified in the doc of generate and in GAS.md, with tests checking the bounds: below 2^-51 for the key words, 2^-24 per Rng pool. Benchmarks are fuzz tests reporting mean and max; statistical tests cover the sparse grids and every path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bal7hazar
force-pushed
the
feat/hexmap-spreader
branch
from
September 27, 2026 17:11
b56d86c to
f8eec20
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lot L7 of
origami_hexmap:Spreader::generate(grid, width, height, count, seed)returns a bitmap with exactlycountbits set, all on walkable tiles, chosen uniformly and deterministically for a seed. It works on any grid, open edge tiles included.The second commit follows up audit A2. Finding 1: the first version's rejection fix-up had no iteration bound. With 2 walkable tiles on a 10x25 board it cost 1.33M gas on average and 9.3M at worst. It is replaced by a bounded algorithm.
Algorithm: radix select on random keys.
k = min(count, walkable - count)tiles.log2(n / 8)levels, whatever the seed.u128limb.Uniformity. The algorithm itself is exact: permutation-invariant. The only deviations come from the random sources, and they are quantified in the doc of
generatewith tests checking the bounds (audit findings 2 and 3):2^-51in total.Rngdraws:2^-24per 128-bit pool, one pool per call in practice.Gas (per call, over 256 seeds)
The targets were a max under 300k for 20 objects on CAVE_17X14, and no case above 3x its mean. Both are met: no row goes above 1.84x its mean.
The cost of bounding: small counts on dense boards are dearer on average. Count 1 costs +14k to +30k, and count 5 on EMPTY/CAVE 17x14 costs about +65k.
The benchmarks are snforge fuzz tests, so CI checks the max over all seeds against the budget. The full tables (every fixture and count, mean and max), the losers, the microbenchmarks and the budgets are in the
GAS.mdL7 section.Tests
26 unit tests:
🤖 Generated with Claude Code