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,48 @@
|
||||
/// Deterministic seedable PRNG for the game engine.
|
||||
///
|
||||
/// dart:math's Random is implementation-unspecified across platforms; piece
|
||||
/// sequences must be byte-identical on iOS, Android, and in tests, so we own
|
||||
/// the algorithm (PCG32, https://www.pcg-random.org).
|
||||
class SeededRng {
|
||||
SeededRng(int seed, [int sequence = 0])
|
||||
: _state = 0,
|
||||
_inc = ((sequence << 1) | 1) {
|
||||
_nextRaw();
|
||||
_state = _state + seed;
|
||||
_nextRaw();
|
||||
}
|
||||
|
||||
// Dart native ints are 64-bit with wrapping arithmetic, which is exactly
|
||||
// what the PCG32 state transition needs.
|
||||
int _state;
|
||||
final int _inc;
|
||||
|
||||
static const _multiplier = 6364136223846793005;
|
||||
static const _mask32 = 0xFFFFFFFF;
|
||||
|
||||
/// One PCG32 step: 32-bit output via xorshift-high + random rotation.
|
||||
int _nextRaw() {
|
||||
final old = _state;
|
||||
_state = old * _multiplier + _inc;
|
||||
final xorshifted = (((old >>> 18) ^ old) >>> 27) & _mask32;
|
||||
final rot = old >>> 59;
|
||||
return ((xorshifted >>> rot) | (xorshifted << ((-rot) & 31))) & _mask32;
|
||||
}
|
||||
|
||||
/// Uniform integer in [0, max), bias-free via rejection sampling.
|
||||
int nextInt(int max) {
|
||||
assert(max > 0);
|
||||
final threshold = (0x100000000 - (0x100000000 % max)) & _mask32;
|
||||
if (threshold == 0) return _nextRaw() % max;
|
||||
while (true) {
|
||||
final r = _nextRaw();
|
||||
if (r < threshold) return r % max;
|
||||
}
|
||||
}
|
||||
|
||||
/// Uniform double in [0, 1).
|
||||
double nextDouble() => _nextRaw() / 4294967296.0;
|
||||
|
||||
/// Derives an independent deterministic stream (e.g. per retry attempt).
|
||||
SeededRng fork(int streamId) => SeededRng(_nextRaw() ^ streamId, streamId);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import '../models/cell.dart';
|
||||
import '../models/grid.dart';
|
||||
|
||||
class ClearResult {
|
||||
const ClearResult({
|
||||
required this.grid,
|
||||
required this.clearedRows,
|
||||
required this.clearedCols,
|
||||
required this.gemsCleared,
|
||||
});
|
||||
|
||||
final GridState grid;
|
||||
final List<int> clearedRows;
|
||||
final List<int> clearedCols;
|
||||
final int gemsCleared;
|
||||
|
||||
int get linesCleared => clearedRows.length + clearedCols.length;
|
||||
}
|
||||
|
||||
/// Detects all simultaneously full rows/columns and clears them in one pass.
|
||||
ClearResult detectAndClear(GridState grid) {
|
||||
final rows = <int>[];
|
||||
final cols = <int>[];
|
||||
for (var y = 0; y < GridState.size; y++) {
|
||||
if (grid.isRowFull(y)) rows.add(y);
|
||||
}
|
||||
for (var x = 0; x < GridState.size; x++) {
|
||||
if (grid.isColFull(x)) cols.add(x);
|
||||
}
|
||||
if (rows.isEmpty && cols.isEmpty) {
|
||||
return ClearResult(
|
||||
grid: grid,
|
||||
clearedRows: const [],
|
||||
clearedCols: const [],
|
||||
gemsCleared: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final rowSet = rows.toSet();
|
||||
final colSet = cols.toSet();
|
||||
var gems = 0;
|
||||
var next = grid;
|
||||
for (var y = 0; y < GridState.size; y++) {
|
||||
for (var x = 0; x < GridState.size; x++) {
|
||||
if (!rowSet.contains(y) && !colSet.contains(x)) continue;
|
||||
if (grid.cellAt(x, y).type == CellType.gem) gems++;
|
||||
next = next.withCell(x, y, Cell.empty);
|
||||
}
|
||||
}
|
||||
return ClearResult(
|
||||
grid: next,
|
||||
clearedRows: rows,
|
||||
clearedCols: cols,
|
||||
gemsCleared: gems,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import '../models/cell.dart';
|
||||
import '../models/grid.dart';
|
||||
import '../models/piece.dart';
|
||||
|
||||
/// Whether [piece] fits with its anchor at ([x], [y]).
|
||||
bool canPlace(GridState grid, Piece piece, int x, int y) {
|
||||
for (final (dx, dy) in piece.offsets) {
|
||||
final px = x + dx;
|
||||
final py = y + dy;
|
||||
if (px < 0 || px >= GridState.size || py < 0 || py >= GridState.size) {
|
||||
return false;
|
||||
}
|
||||
if (grid.isOccupied(px, py)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Places [piece] at ([x], [y]); caller must check [canPlace] first.
|
||||
GridState place(GridState grid, Piece piece, int x, int y) {
|
||||
var next = grid;
|
||||
for (final (dx, dy) in piece.offsets) {
|
||||
next = next.withCell(
|
||||
x + dx,
|
||||
y + dy,
|
||||
Cell(CellType.filled, colorId: piece.colorId),
|
||||
);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/// Whether at least one of [pieces] has at least one legal placement.
|
||||
bool anyPlacementExists(GridState grid, List<Piece> pieces) {
|
||||
for (final piece in pieces) {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Pure scoring math: placement points, line-clear bonuses, combo streaks.
|
||||
library;
|
||||
|
||||
/// Base score for clearing [lines] rows/columns simultaneously.
|
||||
/// 1 -> 100, 2 -> 300, 3 -> 600, 4 -> 1000: simultaneous clears are
|
||||
/// superlinearly rewarded.
|
||||
int lineClearBase(int lines) => 100 * lines + 50 * (lines - 1) * lines;
|
||||
|
||||
/// Multiplier applied to clear score at combo [streak], capped at streak 8
|
||||
/// (x5.0) so late-game scores stay bounded.
|
||||
double comboMultiplier(int streak) => 1 + 0.5 * (streak > 8 ? 8 : streak);
|
||||
|
||||
/// Combo streak with one dry-move grace before reset.
|
||||
class ComboState {
|
||||
const ComboState({required this.streak, required this.dryMoves});
|
||||
|
||||
static const initial = ComboState(streak: 0, dryMoves: 0);
|
||||
|
||||
final int streak;
|
||||
final int dryMoves;
|
||||
|
||||
/// One dry move keeps the streak alive (grace); the second resets it.
|
||||
ComboState advance({required bool cleared}) {
|
||||
if (cleared) return ComboState(streak: streak + 1, dryMoves: 0);
|
||||
if (dryMoves + 1 >= 2) return initial;
|
||||
return ComboState(streak: streak, dryMoves: dryMoves + 1);
|
||||
}
|
||||
}
|
||||
|
||||
class ScoreDelta {
|
||||
const ScoreDelta({required this.points, required this.combo});
|
||||
|
||||
final int points;
|
||||
final ComboState combo;
|
||||
}
|
||||
|
||||
/// Computes the score delta for one placement and the resulting combo state.
|
||||
ScoreDelta scorePlacement({
|
||||
required int cellsPlaced,
|
||||
required int linesCleared,
|
||||
required ComboState combo,
|
||||
}) {
|
||||
final next = combo.advance(cleared: linesCleared > 0);
|
||||
var points = cellsPlaced;
|
||||
if (linesCleared > 0) {
|
||||
points += (lineClearBase(linesCleared) * comboMultiplier(next.streak))
|
||||
.round();
|
||||
}
|
||||
return ScoreDelta(points: points, combo: next);
|
||||
}
|
||||
@@ -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