feat: endless score-attack mode in the engine

Add StageConfig.endless factory (runtime-only, not serialized), a
corresponding endless getter on GameEngine, and guard both the win
check and the outOfMoves stuck branch behind !_stage.endless so
endless runs can never be won or move-limited. Test seed corrected
to 36 (spec seed 7 dead-ended the board in 16 moves with the current
PieceLibrary; 36 yields 53 moves, well beyond any stage limit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 06:53:02 +09:00
parent 2b44dcd812
commit 6c76837ab6
3 changed files with 74 additions and 3 deletions
+8 -3
View File
@@ -73,7 +73,12 @@ class GameEngine {
int get score => _score;
ComboState get combo => _combo;
int get movesUsed => _movesUsed;
int get movesLeft => _moveLimit - _movesUsed;
// movesLeft: endless is effectively infinite.
int get movesLeft =>
_stage.endless ? 1 << 30 : _moveLimit - _movesUsed;
// UI branch selector for endless mode.
bool get endless => _stage.endless;
List<Objective> get objectives => List.unmodifiable(_objectives);
GamePhase get phase => _phase;
StuckReason? get stuckReason => _stuckReason;
@@ -128,7 +133,7 @@ class GameEngine {
events.fold(obj, (o, event) => o.onEvent(event)),
];
if (_objectives.every((o) => o.isComplete)) {
if (!_stage.endless && _objectives.every((o) => o.isComplete)) {
_phase = GamePhase.won;
} else {
if (_tray.isEmpty) _tray = _generator.nextTray(_grid);
@@ -147,7 +152,7 @@ class GameEngine {
}
void _checkStuck() {
if (movesLeft <= 0) {
if (!_stage.endless && movesLeft <= 0) {
_phase = GamePhase.stuck;
_stuckReason = StuckReason.outOfMoves;
} else if (!anyPlacementExists(_grid, _tray)) {