P7.1: freeze finished level — reject moves after won/lost

play_turn now checks level_status before committing: a won or lost
level rejects the swap (accepted=false) with no move spent and no
score change, until restart returns it to in_progress. Adds an
accepted flag to TurnResult so the renderer can show the move was
ignored. Regression in tests/level.sx asserts post-won and post-lost
play_turn leaves score/moves/status unchanged and that restart
re-enables play.
This commit is contained in:
swipelab
2026-06-05 08:37:28 +03:00
parent a40a994ae1
commit e77c470546
3 changed files with 88 additions and 10 deletions

View File

@@ -890,27 +890,40 @@ restart :: (board: *Board, seed: s64) {
board.init(seed);
}
// Outcome of one turn through the goal loop: the underlying `PlayerMove`, the
// level `status` AFTER it, and whether a deadlock `reshuffle` ran (so P7.2 can
// flash a "shuffled" note). The status is recomputed from the model, never
// stored.
// Outcome of one turn through the goal loop: whether the turn was `accepted`
// (false only when a finished level rejected the move), the underlying
// `PlayerMove`, the level `status` AFTER it, and whether a deadlock `reshuffle`
// ran (so P7.2 can flash a "shuffled" note). When `accepted` is false the move
// is a no-op (illegal, depth-0 cascade) and `status` is the terminal status that
// caused the rejection. The status is recomputed from the model, never stored.
TurnResult :: struct {
accepted: bool;
move: PlayerMove;
status: Status;
reshuffled: bool;
}
// Play one turn: attempt the swap via `commit_swap` (an illegal swap changes
// nothing and spends no move), then — only while the level is still in progress —
// reshuffle if the board has deadlocked (no legal swaps left), so the player is
// Play one turn. A FINISHED level is frozen: once `level_status` is won or lost
// the move is REJECTED (`accepted = false`) — no swap, no move spent, no score
// change, status unchanged — until `restart` reseeds a fresh level. P7.2 reads
// `accepted` to tell the player the input was ignored because the level is over.
// While in progress the swap is attempted via `commit_swap` (an illegal swap
// changes nothing and spends no move); then — only if still in progress — the
// board reshuffles if it has deadlocked (no legal swaps left), so the player is
// never stranded. A reshuffle costs no move. A winning or losing move skips the
// reshuffle: the level is over. Returns the move outcome, the resulting status,
// and whether a reshuffle ran.
// reshuffle: the level is over. Returns whether the turn was accepted, the move
// outcome, the resulting status, and whether a reshuffle ran.
play_turn :: (board: *Board, a: Cell, b: Cell) -> TurnResult {
status := level_status(board);
if status != .in_progress {
empty := Cascade.{ depth = 0, cleared = List(s64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
frozen := PlayerMove.{ legal = false, cascade = empty, moves_remaining = board.moves_remaining() };
return TurnResult.{ accepted = false, move = frozen, status = status, reshuffled = false };
}
move := commit_swap(board, a, b);
reshuffled := false;
if level_status(board) == .in_progress and !has_legal_swap(board) {
reshuffled = reshuffle(board);
}
TurnResult.{ move = move, status = level_status(board), reshuffled = reshuffled }
TurnResult.{ accepted = true, move = move, status = level_status(board), reshuffled = reshuffled }
}