Add pure-Dart engine core: RNG, grid, placement, line clear, scoring, piece generator
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>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
enum CellType { empty, filled, gem }
|
||||
|
||||
class Cell {
|
||||
const Cell(this.type, {this.colorId = 0});
|
||||
|
||||
final CellType type;
|
||||
final int colorId;
|
||||
|
||||
static const empty = Cell(CellType.empty);
|
||||
|
||||
bool get isOccupied => type != CellType.empty;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Cell && other.type == type && other.colorId == colorId;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(type, colorId);
|
||||
|
||||
@override
|
||||
String toString() => 'Cell(${type.name}, color: $colorId)';
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'cell.dart';
|
||||
|
||||
/// Immutable 8x8 board state. Mutating operations return new instances.
|
||||
///
|
||||
/// Occupancy is mirrored in a 64-bit bitmask (bit y*8+x) so fullness checks
|
||||
/// and placement scans are O(1) mask operations.
|
||||
class GridState {
|
||||
static const int size = 8;
|
||||
static const int cellCount = size * size;
|
||||
|
||||
GridState.empty()
|
||||
: _cells = List.filled(cellCount, Cell.empty),
|
||||
_mask = 0;
|
||||
|
||||
const GridState._(this._cells, this._mask);
|
||||
|
||||
final List<Cell> _cells;
|
||||
final int _mask;
|
||||
|
||||
static int _index(int x, int y) => y * size + x;
|
||||
|
||||
/// Raw occupancy bitmask; exposed for placement/clear mask math.
|
||||
int get mask => _mask;
|
||||
|
||||
Cell cellAt(int x, int y) => _cells[_index(x, y)];
|
||||
|
||||
bool isOccupied(int x, int y) => (_mask >>> _index(x, y)) & 1 == 1;
|
||||
|
||||
int get occupiedCount {
|
||||
var n = 0;
|
||||
var m = _mask;
|
||||
while (m != 0) {
|
||||
m &= m - 1;
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
double get fillRatio => occupiedCount / cellCount;
|
||||
|
||||
GridState withCell(int x, int y, Cell cell) {
|
||||
final cells = List.of(_cells);
|
||||
final i = _index(x, y);
|
||||
cells[i] = cell;
|
||||
final mask =
|
||||
cell.isOccupied ? (_mask | (1 << i)) : (_mask & ~(1 << i));
|
||||
return GridState._(List.unmodifiable(cells), mask);
|
||||
}
|
||||
|
||||
static int _rowMask(int y) => 0xFF << (y * size);
|
||||
|
||||
static int _colMask(int x) {
|
||||
var m = 0;
|
||||
for (var y = 0; y < size; y++) {
|
||||
m |= 1 << _index(x, y);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
bool isRowFull(int y) => (_mask & _rowMask(y)) == _rowMask(y);
|
||||
|
||||
bool isColFull(int x) => (_mask & _colMask(x)) == _colMask(x);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/// A polyomino with fixed orientation (no rotation at play time, genre
|
||||
/// convention). Offsets are normalized cell positions relative to the
|
||||
/// top-left anchor.
|
||||
class Piece {
|
||||
const Piece({
|
||||
required this.id,
|
||||
required this.offsets,
|
||||
required this.colorId,
|
||||
this.weight = 1.0,
|
||||
this.tier = 1,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final List<(int, int)> offsets;
|
||||
final int colorId;
|
||||
|
||||
/// Base draw weight; stage generator profiles scale this.
|
||||
final double weight;
|
||||
|
||||
/// Difficulty tier 1 (easy to fit) .. 3 (demanding).
|
||||
final int tier;
|
||||
|
||||
int get size => offsets.length;
|
||||
|
||||
@override
|
||||
String toString() => 'Piece($id)';
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'piece.dart';
|
||||
|
||||
/// The fixed-orientation polyomino set (no play-time rotation, genre
|
||||
/// convention). Base weights skew small pieces common; stage generator
|
||||
/// profiles rescale per difficulty.
|
||||
class PieceLibrary {
|
||||
static const double _small = 1.2;
|
||||
static const double _mid = 1.0;
|
||||
static const double _large = 0.7;
|
||||
|
||||
static const List<Piece> all = [
|
||||
// --- size 1-2 ---
|
||||
Piece(id: 'mono', colorId: 0, weight: _small, tier: 1, offsets: [(0, 0)]),
|
||||
Piece(
|
||||
id: 'domino_h',
|
||||
colorId: 1,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (1, 0)]),
|
||||
Piece(
|
||||
id: 'domino_v',
|
||||
colorId: 1,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (0, 1)]),
|
||||
// --- trominoes ---
|
||||
Piece(
|
||||
id: 'line3_h',
|
||||
colorId: 2,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (1, 0), (2, 0)]),
|
||||
Piece(
|
||||
id: 'line3_v',
|
||||
colorId: 2,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (0, 1), (0, 2)]),
|
||||
Piece(
|
||||
id: 'corner3_tl',
|
||||
colorId: 3,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (1, 0), (0, 1)]),
|
||||
Piece(
|
||||
id: 'corner3_tr',
|
||||
colorId: 3,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (1, 0), (1, 1)]),
|
||||
Piece(
|
||||
id: 'corner3_bl',
|
||||
colorId: 3,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (0, 1), (1, 1)]),
|
||||
Piece(
|
||||
id: 'corner3_br',
|
||||
colorId: 3,
|
||||
weight: _small,
|
||||
tier: 1,
|
||||
offsets: [(1, 0), (0, 1), (1, 1)]),
|
||||
// --- tetrominoes ---
|
||||
Piece(
|
||||
id: 'line4_h',
|
||||
colorId: 4,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (3, 0)]),
|
||||
Piece(
|
||||
id: 'line4_v',
|
||||
colorId: 4,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (0, 1), (0, 2), (0, 3)]),
|
||||
Piece(
|
||||
id: 'square2',
|
||||
colorId: 5,
|
||||
weight: _mid,
|
||||
tier: 1,
|
||||
offsets: [(0, 0), (1, 0), (0, 1), (1, 1)]),
|
||||
Piece(
|
||||
id: 't4_up',
|
||||
colorId: 6,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (1, 1)]),
|
||||
Piece(
|
||||
id: 't4_down',
|
||||
colorId: 6,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(1, 0), (0, 1), (1, 1), (2, 1)]),
|
||||
Piece(
|
||||
id: 't4_left',
|
||||
colorId: 6,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(1, 0), (0, 1), (1, 1), (1, 2)]),
|
||||
Piece(
|
||||
id: 't4_right',
|
||||
colorId: 6,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (0, 1), (1, 1), (0, 2)]),
|
||||
Piece(
|
||||
id: 's4_h',
|
||||
colorId: 7,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(1, 0), (2, 0), (0, 1), (1, 1)]),
|
||||
Piece(
|
||||
id: 's4_v',
|
||||
colorId: 7,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (0, 1), (1, 1), (1, 2)]),
|
||||
Piece(
|
||||
id: 'z4_h',
|
||||
colorId: 7,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (1, 1), (2, 1)]),
|
||||
Piece(
|
||||
id: 'z4_v',
|
||||
colorId: 7,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(1, 0), (0, 1), (1, 1), (0, 2)]),
|
||||
Piece(
|
||||
id: 'l4_0',
|
||||
colorId: 0,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (0, 1), (0, 2), (1, 2)]),
|
||||
Piece(
|
||||
id: 'l4_90',
|
||||
colorId: 0,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (0, 1)]),
|
||||
Piece(
|
||||
id: 'l4_180',
|
||||
colorId: 0,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (1, 1), (1, 2)]),
|
||||
Piece(
|
||||
id: 'l4_270',
|
||||
colorId: 0,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(2, 0), (0, 1), (1, 1), (2, 1)]),
|
||||
Piece(
|
||||
id: 'j4_0',
|
||||
colorId: 1,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(1, 0), (1, 1), (0, 2), (1, 2)]),
|
||||
Piece(
|
||||
id: 'j4_90',
|
||||
colorId: 1,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (0, 1), (1, 1), (2, 1)]),
|
||||
Piece(
|
||||
id: 'j4_180',
|
||||
colorId: 1,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (0, 1), (0, 2)]),
|
||||
Piece(
|
||||
id: 'j4_270',
|
||||
colorId: 1,
|
||||
weight: _mid,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (2, 1)]),
|
||||
// --- size 5-6 ---
|
||||
Piece(
|
||||
id: 'line5_h',
|
||||
colorId: 2,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
|
||||
Piece(
|
||||
id: 'line5_v',
|
||||
colorId: 2,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)]),
|
||||
Piece(
|
||||
id: 'rect2x3',
|
||||
colorId: 3,
|
||||
weight: _large,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)]),
|
||||
Piece(
|
||||
id: 'rect3x2',
|
||||
colorId: 3,
|
||||
weight: _large,
|
||||
tier: 2,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]),
|
||||
Piece(
|
||||
id: 'bigl_tl',
|
||||
colorId: 4,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (0, 1), (0, 2)]),
|
||||
Piece(
|
||||
id: 'bigl_tr',
|
||||
colorId: 4,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(0, 0), (1, 0), (2, 0), (2, 1), (2, 2)]),
|
||||
Piece(
|
||||
id: 'bigl_bl',
|
||||
colorId: 4,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]),
|
||||
Piece(
|
||||
id: 'bigl_br',
|
||||
colorId: 4,
|
||||
weight: _large,
|
||||
tier: 3,
|
||||
offsets: [(2, 0), (2, 1), (0, 2), (1, 2), (2, 2)]),
|
||||
// --- size 9 ---
|
||||
Piece(
|
||||
id: 'square3',
|
||||
colorId: 5,
|
||||
weight: 0.5,
|
||||
tier: 3,
|
||||
offsets: [
|
||||
(0, 0), (1, 0), (2, 0),
|
||||
(0, 1), (1, 1), (2, 1),
|
||||
(0, 2), (1, 2), (2, 2),
|
||||
]),
|
||||
];
|
||||
|
||||
static Piece byId(String id) => all.firstWhere((p) => p.id == id);
|
||||
}
|
||||
Reference in New Issue
Block a user