Add a thin, deterministic level loop over the headless model in board.sx:
- target_score: per-level score goal (DEFAULT_TARGET_SCORE=1500), seeded by init.
- Status (in_progress/won/lost) derived purely from score/target/move budget via
level_status; won is checked before lost so meeting the goal on the final move
wins. status_name for the HUD/snapshots.
- has_legal_swap: allocation-free deadlock probe (first legal pair short-circuits).
- reshuffle: Fisher-Yates over existing gems via the board's seeded RNG until the
arrangement has no immediate match and at least one legal move; consumes no
move, terminates via MAX_RESHUFFLE_TRIES.
- restart: reseed a fresh, reproducible level (resets cells/score/moves/goal).
- play_turn / TurnResult: commit_swap then reshuffle on deadlock while in
progress, reporting the resulting status.
tests/level.sx (golden tests/expected/level.{stdout,exit}) asserts: start
in_progress; one legal swap crossing a low goal -> won transition; budget
exhausted below an unreachable goal -> lost transition; a provably deadlocked
diagonal Latin-square board ((col-row) mod 6) reshuffles to >=1 legal move with
no immediate match and no move spent; restart resets progress and a fixed seed
reproduces the same starting board.
Gate: sx build --target ios-sim main.sx (exit 0); bash tools/run_tests.sh
(17 passed, 0 failed).
75 lines
1.2 KiB
Plaintext
75 lines
1.2 KiB
Plaintext
== level (turn / goal state machine) ==
|
|
== start-in-progress ==
|
|
RRPPOGRG
|
|
PGPOPRRO
|
|
YYBBYRYB
|
|
GBYYRGGP
|
|
OGBRRORY
|
|
BYRRPRBG
|
|
YOYYROBB
|
|
OROBPPRB
|
|
score 0 target 1500 moves_remaining 30 status in_progress
|
|
== win-transition ==
|
|
RROGOGOG
|
|
GGROGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
before: score 0 target 30 moves_remaining 5 status in_progress
|
|
after: legal true awarded 30 reshuffled false
|
|
after: score 30 moves_remaining 4 status won
|
|
RBRGOGOG
|
|
GGOOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
== lose-transition ==
|
|
RROGOGOG
|
|
GGROGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
OGOGOGOG
|
|
GOGOGOGO
|
|
before: score 0 target 1000000 moves_remaining 1 status in_progress
|
|
after: legal true score 30 moves_remaining 0 status lost
|
|
== deadlock-reshuffle ==
|
|
ROYGBPRO
|
|
PROYGBPR
|
|
BPROYGBP
|
|
GBPROYGB
|
|
YGBPROYG
|
|
OYGBPROY
|
|
ROYGBPRO
|
|
PROYGBPR
|
|
before: matches 0 legal_swaps 0 has_legal_swap false
|
|
reshuffled true matches 0 legal_swaps 9
|
|
after: score 0 moves_made 0
|
|
BGGYORYR
|
|
RRYGOPBY
|
|
YRYBPRGB
|
|
OOBGBPRG
|
|
RPRPYRPO
|
|
OBBPOOPG
|
|
OBGGOPGY
|
|
YPRYBORP
|
|
== restart ==
|
|
dirty: score 500 moves_made 7 status in_progress
|
|
after restart: score 0 moves_made 0 moves_remaining 30 status in_progress
|
|
RRPPOGRG
|
|
PGPOPRRO
|
|
YYBBYRYB
|
|
GBYYRGGP
|
|
OGBRRORY
|
|
BYRRPRBG
|
|
YOYYROBB
|
|
OROBPPRB
|
|
ok: level / turn-goal state machine
|