Commit Graph

8 Commits

Author SHA1 Message Date
swipelab
23d08e44ce P2.3: seeded refill (pure sx)
Add refill to the board model: every .empty hole is filled with a fresh gem
drawn from the board's OWN seeded RNG, so refills are fully reproducible for a
seed and continue the stream rather than reseeding.

- Board now owns its RNG state (rng: Rng); init seeds and draws from it, so
  draws after init/clears thread deterministically. The init draw sequence is
  unchanged, so board_init's golden is byte-identical.
- refill(board) fills all holes in row-major order wherever they sit (does not
  assume collapse ran) and makes no attempt to avoid matches — a refill may
  create new runs, which drives the P2.4 cascade.
- tests/refill.sx (fixed seed) runs clear -> collapse -> refill, locks the
  staged dump as a golden, and asserts: zero empties after refill; each hole
  holds the next seeded-stream gem (replayed from the pre-refill state); drawn
  gems vary (not a constant); same start+seed -> identical board; a second
  refill of the same holes draws new gems (RNG threads, no reseed).
2026-06-04 20:35:50 +03:00
swipelab
b5a3b16651 P2.2: gravity collapse (pure sx)
Add collapse(board): per-column gravity that packs gems contiguously at
the bottom (preserving top-to-bottom order) and bubbles holes to the top,
with no horizontal movement. Returns whether any gem fell, for the P2.4
cascade. Does not refill (that is P2.3).

tests/collapse.sx snapshots gravity over hand-crafted boards exercising
holes in the middle / at the bottom, a full column of holes, a column
with none, a lone gem, an alternating stack, and an already-settled board
(idempotency). Asserts, independently of the dump, that each column's gems
end packed at the bottom in original order with holes above, plus the
exact moved flag. Golden locked in tests/expected/collapse.{stdout,exit}.
2026-06-04 20:21:12 +03:00
swipelab
6e2a57f3a2 P2.1: holes never match in find_matches
find_matches walked maximal same-type spans without excluding `.empty`, so a
line of 3+ holes (left by a prior clear) was reported as a match. After any
vertical 3-clear or L/T clear the board carries such a line, so find_matches /
clear_matches returned non-zero on a board with no real gem match — which would
prevent the P2.4 cascade from ever stabilising.

Fix at the source: a run is only a match if its gem type is not `.empty`. Holes
already break runs of real gems (a hole differs from every gem), so this is the
only change needed and every caller (P1.3 legality, P2.4 cascade) is now correct.

Regression in tests/clear.sx: a holes-only board yields zero matches and
clear_matches 0, and re-clearing a holed board returns 0. Other goldens are
unchanged (no board without holes is affected).
2026-06-04 20:08:26 +03:00
swipelab
2713a67b2b P2.1: clear matched cells (pure sx)
Add the first resolution-pipeline step to the headless board model.

- Introduce an `empty` hole sentinel on the Gem enum (ordinal 6, outside
  GEM_COUNT so the RNG/pick_gem never draw it). board_dump renders holes as
  EMPTY_CHAR ('.') via a single branch in gem_char, leaving boards without
  holes byte-identical to before (existing goldens unchanged).
- clear_cells(board, mask): set every matched cell to `.empty`, leave all
  others untouched, return the count cleared.
- clear_matches(board): detect+clear in one call; returns 0 (board unchanged)
  when there are no matches.

No gravity or refill yet (P2.2 / P2.3).

tests/clear.sx applies detect->clear to hand-crafted boards (single
horizontal/vertical runs, disjoint runs, an overlapping L/T whose shared cell
clears once, and a no-match checkerboard), snapshots before/after, and asserts
matched cells became holes, non-matched cells are unchanged, and the cleared
count is exact. Locked as tests/expected/clear.{stdout,exit}.
2026-06-04 19:57:08 +03:00
swipelab
4264f5f36f P1.3: adjacent-swap legality (pure sx)
Add swap + legality to the board model:
- swap(board, a, b): in-place, self-inverse cell exchange (trial then revert).
- adjacent(a, b): orthogonal-adjacency predicate (diagonal/gap = false).
- swap_legal(board, a, b): legal iff adjacent AND, after the trial swap, either
  swapped cell participates in a 3+ match (reuses find_matches); leaves the
  board unchanged. Non-adjacent/diagonal rejected before any match check.
- Cell/Swap structs + legal_swaps(board): all currently-legal swaps in a stable
  row-major, right-before-down order; dump_swaps for deterministic snapshotting.

tests/swap_legality.sx asserts the predicate over hand-crafted boards (legal
3-run, no-match, non-adjacent, diagonal, only-the-other-gem-matches) and the
non-mutating revert; locks legal_swaps over the seeded board as a golden.
2026-06-04 19:44:54 +03:00
swipelab
b0c081e397 P1.2: match detection (3+ horizontal/vertical runs)
Add a pure-sx match detector to the board model: `find_matches` walks each
row and column once in maximal same-type spans and marks every cell in a run
of length >= 3 into a `MatchMask` (a per-cell membership set mirroring
Board.cells). Overlapping shapes (L / T where a horizontal and vertical run
share a cell) collapse to the union automatically. `dump_matches` renders the
set deterministically: matched cells show their gem char, others '.'.

Detection only — no clear/collapse/refill (that is P2.1).

tests/match_detect.sx exercises hand-crafted boards (built explicitly on a
run-free checkerboard, no seeded init): a horizontal 3-run, a vertical 3-run,
multiple disjoint runs, length-4 and length-5 runs, intersecting L and T
shapes (shared cell counted once), and a no-match board. Output is locked as
tests/expected/match_detect.stdout (+ .exit) and asserts matched-cell counts.
2026-06-04 19:27:15 +03:00
swipelab
45e7eb803e P1.1: pure-sx Gem & Board model with seeded, match-free init
Add board.sx, the headless Phase-1 match-3 core:
- Gem enum (6 types, ordinal 0..5) + single-char dump alphabet.
- Rng: a 32-bit LCG carried in s64, masked to 32 bits each step, so the
  stream is host-width independent and valid for any seed.
- Board (8x8, row-major) with idx/at/set accessors and a seeded init that
  fills row-major, excluding any gem that would complete a 3-in-a-row with
  the two cells to the left or above — so the result has zero pre-existing
  matches. Single RNG draw per cell, always terminates.
- board_dump: deterministic one-row-per-line textual snapshot.

tests/board_init.sx seeds with a fixed seed, dumps the board, and asserts
zero horizontal/vertical 3-in-a-row runs via an independent scan. Output and
exit code are locked as goldens. App ios-sim build is unaffected (main.sx
does not import the model yet).
2026-06-04 19:12:55 +03:00
swipelab
6d9aee67ba P0.4: wire the logic verification gate (sx assert helper + snapshot runner)
Add a real logic-test gate so future pure-sx game logic fails the build on a
bad assertion:

- tests/test.sx: `expect(cond, msg)` assert helper — prints a greppable
  `FAIL <file>:<line>: <msg>` and exits non-zero via process.exit on failure.
- tools/run_tests.sh: snapshot runner mirroring sx/tests/run_examples.sh; runs
  each tests/<name>.sx and diffs stdout + exit code against tests/expected/.
  Exits 0 iff all tests pass.
- tests/arith.sx (+ expected snapshots): seed passing sanity test.
- README.md: document both halves of the gate — logic runner and the
  reproducible ios-sim build/launch sequence (with device discovery).
2026-06-04 18:57:21 +03:00