62cbb4b16a
Sealed Objective types (clearGems/reachScore/clearLines) with JSON round-trip; StageConfig with preset cells and star thresholds; GameEngine orchestrating placement -> clear -> scoring -> objectives with stuck detection, one-shot rescue (continue / +5 moves), and deterministic per-attempt RNG. 100-game headless stress test and pure-Dart architecture guard. 76 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.3 KiB
Dart
64 lines
2.3 KiB
Dart
import 'package:block_seasons/core/rng.dart';
|
|
import 'package:block_seasons/game/engine/game_engine.dart';
|
|
import 'package:block_seasons/game/models/grid.dart';
|
|
import 'package:block_seasons/game/models/stage.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
StageConfig _endlessStage(int seed) => StageConfig.fromJson({
|
|
'id': 'stress_$seed',
|
|
'seed': seed,
|
|
'moveLimit': 40,
|
|
'preset': const <Map<String, dynamic>>[],
|
|
'objectives': [
|
|
{'type': 'reachScore', 'target': 99999999},
|
|
],
|
|
'stars': {
|
|
'two': {'movesLeft': 5},
|
|
'three': {'movesLeft': 10},
|
|
},
|
|
'generatorProfile': 'mid',
|
|
});
|
|
|
|
void main() {
|
|
test('100 seeded random games run to a terminal state without violations',
|
|
() {
|
|
for (var seed = 0; seed < 100; seed++) {
|
|
final engine = GameEngine(_endlessStage(seed));
|
|
final rng = SeededRng(seed * 7919 + 1);
|
|
var guard = 0;
|
|
|
|
while (engine.phase == GamePhase.playing) {
|
|
guard++;
|
|
expect(guard, lessThan(300), reason: 'seed $seed: game never ended');
|
|
|
|
// Enumerate all legal moves; the engine promises at least one exists
|
|
// while phase == playing.
|
|
final moves = <(int, int, int)>[];
|
|
for (var i = 0; i < engine.tray.length; i++) {
|
|
for (var y = 0; y < GridState.size; y++) {
|
|
for (var x = 0; x < GridState.size; x++) {
|
|
if (engine.tryPlaceWouldSucceed(i, x, y)) moves.add((i, x, y));
|
|
}
|
|
}
|
|
}
|
|
expect(moves, isNotEmpty,
|
|
reason: 'seed $seed: playing phase but no legal move');
|
|
|
|
final (i, x, y) = moves[rng.nextInt(moves.length)];
|
|
final result = engine.tryPlace(i, x, y);
|
|
expect(result, isNotNull, reason: 'seed $seed: legal move rejected');
|
|
|
|
// Invariants after every move.
|
|
expect(engine.tray.length, inInclusiveRange(1, 3),
|
|
reason: 'seed $seed');
|
|
expect(engine.movesUsed, lessThanOrEqualTo(40), reason: 'seed $seed');
|
|
expect(engine.score, greaterThanOrEqualTo(0), reason: 'seed $seed');
|
|
}
|
|
|
|
// The score objective is unreachable, so every game ends stuck.
|
|
expect(engine.phase, GamePhase.stuck, reason: 'seed $seed');
|
|
expect(engine.stuckReason, isNotNull, reason: 'seed $seed');
|
|
}
|
|
});
|
|
}
|