import '../../core/rng.dart'; import '../models/cell.dart'; import '../models/grid.dart'; import '../models/stage.dart'; import 'game_engine.dart'; import 'line_clear.dart'; import 'piece_generator.dart'; import 'placement.dart'; import 'scoring.dart'; class BotRun { const BotRun({ required this.won, required this.movesUsed, required this.movesLeft, required this.stars, required this.score, }); final bool won; final int movesUsed; final int movesLeft; final int stars; final int score; } /// Greedy objective-seeking bot used by the stage generator to calibrate /// difficulty (win rates, move budgets) against the real shipping engine. /// It never uses rescues, so its results reflect a clean attempt. class AutoPlayer { BotRun play(StageConfig stage, {int attempt = 0}) { final engine = GameEngine(stage, attempt: attempt); final tieBreaker = SeededRng(stage.seed ^ (attempt * 31 + 7)); var guard = 0; while (engine.phase == GamePhase.playing && guard < 400) { guard++; final moves = <(double, int, int, int)>[]; for (var i = 0; i < engine.tray.length; i++) { final piece = engine.tray[i]; for (var y = 0; y < GridState.size; y++) { for (var x = 0; x < GridState.size; x++) { if (!canPlace(engine.grid, piece, x, y)) continue; final clear = detectAndClear(place(engine.grid, piece, x, y)); final h = clear.gemsCleared * 120.0 + lineClearBase(clear.linesCleared) * 0.5 + piece.size - clear.grid.fillRatio * 10 + _gemLineProgress(clear.grid) * 30 + tieBreaker.nextDouble() * 1e-6; moves.add((h, i, x, y)); } } } if (moves.isEmpty) break; moves.sort((a, b) => b.$1.compareTo(a.$1)); // Greedy alone walls itself in: prefer the best-ranked move that // leaves the rest of the tray playable (with clears resolved). var chosen = moves.first; for (final move in moves.take(10)) { final (_, i, x, y) = move; final after = detectAndClear(place(engine.grid, engine.tray[i], x, y)).grid; final rest = [ for (var k = 0; k < engine.tray.length; k++) if (k != i) engine.tray[k], ]; if (isTrayPlayable(after, rest)) { chosen = move; break; } } engine.tryPlace(chosen.$2, chosen.$3, chosen.$4); } return BotRun( won: engine.phase == GamePhase.won, movesUsed: engine.movesUsed, movesLeft: engine.movesLeft, stars: engine.starsEarned, score: engine.score, ); } /// How close remaining gems are to being cleared: for each gem, the best /// fill fraction of its row or column. Steers the bot toward building the /// lines that matter. double _gemLineProgress(GridState grid) { var progress = 0.0; for (var y = 0; y < GridState.size; y++) { for (var x = 0; x < GridState.size; x++) { if (grid.cellAt(x, y).type != CellType.gem) continue; var rowFill = 0; var colFill = 0; for (var i = 0; i < GridState.size; i++) { if (grid.isOccupied(i, y)) rowFill++; if (grid.isOccupied(x, i)) colFill++; } progress += (rowFill > colFill ? rowFill : colFill) / GridState.size; } } return progress; } }