0210c14858
PCG32 seeded RNG; immutable 8x8 GridState with occupancy bitmask; placement legality + anyPlacementExists; simultaneous row/col clears with single-count gem credit; combo scoring with one-move grace; weighted-bag generator with pity bias and depth-3 solvability nudge. All TDD, 51 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
3.6 KiB
Dart
118 lines
3.6 KiB
Dart
import '../../core/rng.dart';
|
|
import '../models/grid.dart';
|
|
import '../models/piece.dart';
|
|
import '../models/piece_library.dart';
|
|
import 'line_clear.dart';
|
|
import 'placement.dart';
|
|
|
|
/// Whether some ordering of [tray] can be fully played out (with line clears
|
|
/// resolved between placements). Depth-3 backtracking on an 8x8 is cheap.
|
|
bool isTrayPlayable(GridState grid, List<Piece> tray) {
|
|
if (tray.isEmpty) return true;
|
|
final triedIds = <String>{};
|
|
for (var i = 0; i < tray.length; i++) {
|
|
final piece = tray[i];
|
|
if (!triedIds.add(piece.id)) continue;
|
|
final rest = [...tray]..removeAt(i);
|
|
for (var y = 0; y < GridState.size; y++) {
|
|
for (var x = 0; x < GridState.size; x++) {
|
|
if (!canPlace(grid, piece, x, y)) continue;
|
|
final after = detectAndClear(place(grid, piece, x, y)).grid;
|
|
if (isTrayPlayable(after, rest)) return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Weighted-bag tray dealer with pity bias on tight boards and a solvability
|
|
/// nudge: the game never deals an instantly dead tray when a playable deal
|
|
/// exists, but bad play can still lose.
|
|
class PieceGenerator {
|
|
PieceGenerator(this._rng, {List<Piece>? pool})
|
|
: _pool = pool ?? PieceLibrary.all;
|
|
|
|
static const int traySize = 3;
|
|
static const int _maxRedraws = 5;
|
|
static const double _pityThreshold = 0.6;
|
|
|
|
final SeededRng _rng;
|
|
final List<Piece> _pool;
|
|
|
|
List<Piece> nextTray(GridState grid) {
|
|
for (var attempt = 0; attempt < _maxRedraws; attempt++) {
|
|
final tray = _draw(grid);
|
|
if (isTrayPlayable(grid, tray)) return tray;
|
|
}
|
|
return _fallback(grid);
|
|
}
|
|
|
|
List<Piece> _draw(GridState grid) {
|
|
final fill = grid.fillRatio;
|
|
final candidates = List.of(_pool);
|
|
final tray = <Piece>[];
|
|
while (tray.length < traySize) {
|
|
final pick = _weightedPick(candidates, fill);
|
|
tray.add(pick);
|
|
candidates.removeWhere((p) => p.id == pick.id);
|
|
}
|
|
return tray;
|
|
}
|
|
|
|
/// Above 60% board fill, small pieces get progressively more likely so the
|
|
/// late game feels fair instead of dealing unplaceable monsters.
|
|
double _effectiveWeight(Piece piece, double fill) {
|
|
var w = piece.weight;
|
|
if (fill > _pityThreshold && piece.size <= 3) {
|
|
w *= 1 + 2 * (fill - _pityThreshold);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
Piece _weightedPick(List<Piece> candidates, double fill) {
|
|
var total = 0.0;
|
|
for (final p in candidates) {
|
|
total += _effectiveWeight(p, fill);
|
|
}
|
|
var r = _rng.nextDouble() * total;
|
|
for (final p in candidates) {
|
|
r -= _effectiveWeight(p, fill);
|
|
if (r < 0) return p;
|
|
}
|
|
return candidates.last;
|
|
}
|
|
|
|
/// After repeated unplayable draws: the largest piece that still fits plus
|
|
/// the smallest fillers. Consumes no RNG, so retries stay deterministic.
|
|
List<Piece> _fallback(GridState grid) {
|
|
final bySizeDesc = [..._pool]
|
|
..sort((a, b) {
|
|
final s = b.size.compareTo(a.size);
|
|
return s != 0 ? s : a.id.compareTo(b.id);
|
|
});
|
|
Piece? largestFitting;
|
|
for (final p in bySizeDesc) {
|
|
if (_fitsAnywhere(grid, p)) {
|
|
largestFitting = p;
|
|
break;
|
|
}
|
|
}
|
|
final tray = <Piece>[if (largestFitting != null) largestFitting];
|
|
for (final p in bySizeDesc.reversed) {
|
|
if (tray.length == traySize) break;
|
|
if (tray.any((t) => t.id == p.id)) continue;
|
|
tray.add(p);
|
|
}
|
|
return tray;
|
|
}
|
|
|
|
bool _fitsAnywhere(GridState grid, Piece piece) {
|
|
for (var y = 0; y < GridState.size; y++) {
|
|
for (var x = 0; x < GridState.size; x++) {
|
|
if (canPlace(grid, piece, x, y)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|