Files
BlockSeasons/test/game/engine/headless_stress_test.dart
airkjw 62cbb4b16a Add objectives, stage config, and GameEngine session core
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>
2026-06-11 13:11:31 +09:00

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');
}
});
}