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:
@@ -29,6 +29,9 @@ OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
== frozen-when-won ==
|
||||
post-win play_turn: accepted false legal false status won
|
||||
unchanged: score 30 moves_made 1 moves_remaining 4
|
||||
== lose-transition ==
|
||||
RROGOGOG
|
||||
GGROGOGO
|
||||
@@ -40,6 +43,9 @@ OGOGOGOG
|
||||
GOGOGOGO
|
||||
before: score 0 target 1000000 moves_remaining 1 status in_progress
|
||||
after: legal true score 30 moves_remaining 0 status lost
|
||||
== frozen-when-lost ==
|
||||
post-lose play_turn: accepted false legal false status lost
|
||||
unchanged: score 30 moves_made 1 moves_remaining 0
|
||||
== deadlock-reshuffle ==
|
||||
ROYGBPRO
|
||||
PROYGBPR
|
||||
@@ -71,4 +77,5 @@ OGBRRORY
|
||||
BYRRPRBG
|
||||
YOYYROBB
|
||||
OROBPPRB
|
||||
re-enabled: (3,0)-(3,1) accepted true legal true moves_made 1
|
||||
ok: level / turn-goal state machine
|
||||
|
||||
@@ -100,6 +100,29 @@ main :: () -> s32 {
|
||||
t.expect(bw.moves_remaining() > 0, "win: won with moves to spare");
|
||||
t.expect(!tw.reshuffled, "win: no reshuffle once won");
|
||||
|
||||
// ── Frozen when WON: a finished level rejects further moves ──────────────
|
||||
// The level is over the instant it is won. Until `restart`, every `play_turn`
|
||||
// must be REJECTED (accepted=false): no swap applied, no move spent, no score
|
||||
// change, status stays won — even for an otherwise-legal swap. (4,0)<->(4,1)
|
||||
// is a legal swap on the won board, so WITHOUT the freeze it would commit and
|
||||
// move score/budget; the freeze is what keeps them put.
|
||||
print("== frozen-when-won ==\n");
|
||||
won_score := bw.score;
|
||||
won_made := bw.moves_made;
|
||||
won_remain := bw.moves_remaining();
|
||||
twf := play_turn(@bw, Cell.{ col = 4, row = 0 }, Cell.{ col = 4, row = 1 });
|
||||
print("post-win play_turn: accepted {} legal {} status {}\n",
|
||||
twf.accepted, twf.move.legal, status_name(twf.status));
|
||||
print("unchanged: score {} moves_made {} moves_remaining {}\n",
|
||||
bw.score, bw.moves_made, bw.moves_remaining());
|
||||
t.expect(!twf.accepted, "frozen-won: move rejected");
|
||||
t.expect(!twf.move.legal, "frozen-won: no swap committed");
|
||||
t.expect(twf.status == .won, "frozen-won: turn still reports won");
|
||||
t.expect(bw.score == won_score, "frozen-won: score unchanged");
|
||||
t.expect(bw.moves_made == won_made, "frozen-won: no move spent");
|
||||
t.expect(bw.moves_remaining() == won_remain, "frozen-won: budget unchanged");
|
||||
t.expect(level_status(@bw) == .won, "frozen-won: board still won");
|
||||
|
||||
// ── LOSS transition: the move budget runs out short of the goal ──────────
|
||||
// Same legal swap (award 30) but a 1-move budget and an unreachable goal:
|
||||
// after the move moves_remaining hits 0 with score < goal, flipping
|
||||
@@ -129,6 +152,27 @@ main :: () -> s32 {
|
||||
t.expect(level_status(@bl) == .lost, "lose: board reads lost");
|
||||
t.expect(!tl.reshuffled, "lose: no reshuffle once lost");
|
||||
|
||||
// ── Frozen when LOST: a finished level rejects further moves ─────────────
|
||||
// Symmetric to the won case. (4,0)<->(4,1) is a legal swap on the lost board,
|
||||
// so the REJECTION — not the spent budget — is what holds score and moves
|
||||
// put: `commit_swap` itself does not refuse a swap once the budget is gone.
|
||||
print("== frozen-when-lost ==\n");
|
||||
lost_score := bl.score;
|
||||
lost_made := bl.moves_made;
|
||||
lost_remain := bl.moves_remaining();
|
||||
tlf := play_turn(@bl, Cell.{ col = 4, row = 0 }, Cell.{ col = 4, row = 1 });
|
||||
print("post-lose play_turn: accepted {} legal {} status {}\n",
|
||||
tlf.accepted, tlf.move.legal, status_name(tlf.status));
|
||||
print("unchanged: score {} moves_made {} moves_remaining {}\n",
|
||||
bl.score, bl.moves_made, bl.moves_remaining());
|
||||
t.expect(!tlf.accepted, "frozen-lost: move rejected");
|
||||
t.expect(!tlf.move.legal, "frozen-lost: no swap committed");
|
||||
t.expect(tlf.status == .lost, "frozen-lost: turn still reports lost");
|
||||
t.expect(bl.score == lost_score, "frozen-lost: score unchanged");
|
||||
t.expect(bl.moves_made == lost_made, "frozen-lost: no move spent");
|
||||
t.expect(bl.moves_remaining() == lost_remain, "frozen-lost: budget unchanged");
|
||||
t.expect(level_status(@bl) == .lost, "frozen-lost: board still lost");
|
||||
|
||||
// ── No-legal-moves rule: RESHUFFLE ──────────────────────────────────────
|
||||
// A provably deadlocked board: gem(col,row) = (col-row) mod 6, a diagonal
|
||||
// Latin square. Equal gems lie only on slope-1 diagonals, so no row/column
|
||||
@@ -191,6 +235,20 @@ main :: () -> s32 {
|
||||
t.expect(level_status(@br) == .in_progress, "restart: status in_progress");
|
||||
t.expect(boards_equal(@br, @ref), "restart: same seed reproduces starting board");
|
||||
|
||||
// restart RE-ENABLES play: the fresh in-progress level accepts moves again
|
||||
// (it was frozen only while terminal). A legal swap on the restored board now
|
||||
// commits — accepted, one move spent — proving the entry point is live again.
|
||||
rsw := legal_swaps(@br);
|
||||
t.expect(rsw.len > 0, "restart: fresh board has a legal move");
|
||||
rmove := rsw.items[0];
|
||||
tr := play_turn(@br, rmove.a, rmove.b);
|
||||
print("re-enabled: ({},{})-({},{}) accepted {} legal {} moves_made {}\n",
|
||||
rmove.a.col, rmove.a.row, rmove.b.col, rmove.b.row,
|
||||
tr.accepted, tr.move.legal, br.moves_made);
|
||||
t.expect(tr.accepted, "restart: play_turn accepted after restart");
|
||||
t.expect(tr.move.legal, "restart: legal swap committed after restart");
|
||||
t.expect(br.moves_made == 1, "restart: a move spent after restart");
|
||||
|
||||
print("ok: level / turn-goal state machine\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user