From 829538954c8079e389eb9abf40640ea23370fe14 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 18 May 2026 10:55:10 +0200 Subject: [PATCH] Make Position completely immutable by switching pockets implementation --- lib/src/setup.dart | 81 ++++++++++-------------------- test/setup_test.dart | 114 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 65 deletions(-) diff --git a/lib/src/setup.dart b/lib/src/setup.dart index d3d1a9e..c0a232e 100644 --- a/lib/src/setup.dart +++ b/lib/src/setup.dart @@ -213,69 +213,56 @@ class Setup { /// Pockets (captured pieces) in chess variants like [Crazyhouse]. @immutable class Pockets { - const Pockets({required BySide> value}) : _value = value; + const Pockets._(this._value); - final BySide> _value; + // Bitfield: 5 bits per (side, role) slot. + // Offset = side.index * 30 + role.index * 5. Total: 60 bits. + final int _value; /// An empty pocket. - static const empty = Pockets(value: _emptyPocketsBySide); + static const empty = Pockets._(0); - /// Gets the total number of pieces in the pocket. - int get size => _value.values - .fold(0, (acc, e) => acc + e.values.fold(0, (acc, e) => acc + e)); + static int _offset(Side side, Role role) => side.index * 30 + role.index * 5; /// Gets the number of pieces of that [Side] and [Role] in the pocket. - int of(Side side, Role role) { - return _value[side]![role]!; - } + int of(Side side, Role role) => (_value >> _offset(side, role)) & 0x1F; + + /// Gets the total number of pieces in the pocket. + int get size => Side.values.fold( + 0, + (acc, s) => acc + Role.values.fold(0, (acc2, r) => acc2 + of(s, r)), + ); /// Counts the number of pieces by [Role]. - int count(Role role) { - return _value[Side.white]![role]! + _value[Side.black]![role]!; - } + int count(Role role) => of(Side.white, role) + of(Side.black, role); /// Checks whether this side has at least 1 quality (any piece but a pawn). - bool hasQuality(Side side) { - final bySide = _value[side]!; - return bySide[Role.knight]! > 0 || - bySide[Role.bishop]! > 0 || - bySide[Role.rook]! > 0 || - bySide[Role.queen]! > 0 || - bySide[Role.king]! > 0; - } + bool hasQuality(Side side) => + of(side, Role.knight) > 0 || + of(side, Role.bishop) > 0 || + of(side, Role.rook) > 0 || + of(side, Role.queen) > 0 || + of(side, Role.king) > 0; /// Checks whether this side has at least 1 pawn. - bool hasPawn(Side side) { - return _value[side]![Role.pawn]! > 0; - } + bool hasPawn(Side side) => of(side, Role.pawn) > 0; /// Increments the number of pieces in the pocket of that [Side] and [Role]. - Pockets increment(Side side, Role role) { - final newPocket = {..._value[side]!, role: of(side, role) + 1}; - return Pockets(value: {..._value, side: newPocket}); - } + Pockets increment(Side side, Role role) => + Pockets._(_value + (1 << _offset(side, role))); /// Decrements the number of pieces in the pocket of that [Side] and [Role]. - Pockets decrement(Side side, Role role) { - final newPocket = {..._value[side]!, role: of(side, role) - 1}; - return Pockets(value: {..._value, side: newPocket}); - } + Pockets decrement(Side side, Role role) => + Pockets._(_value - (1 << _offset(side, role))); @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is! Pockets) return false; - for (final side in Side.values) { - for (final role in Role.values) { - if (of(side, role) != other.of(side, role)) return false; - } - } - return true; + return other is Pockets && other._value == _value; } @override - int get hashCode => Object.hashAll( - Side.values.expand((s) => Role.values.map((r) => of(s, r)))); + int get hashCode => _value.hashCode; } Pockets _parsePockets(String pocketPart) { @@ -401,17 +388,3 @@ int _nthIndexOf(String haystack, String needle, int nth) { } return index; } - -const ByRole _emptyPocket = { - Role.pawn: 0, - Role.knight: 0, - Role.bishop: 0, - Role.rook: 0, - Role.queen: 0, - Role.king: 0, -}; - -const BySide> _emptyPocketsBySide = { - Side.white: _emptyPocket, - Side.black: _emptyPocket, -}; diff --git a/test/setup_test.dart b/test/setup_test.dart index 5689166..98ca881 100644 --- a/test/setup_test.dart +++ b/test/setup_test.dart @@ -103,10 +103,19 @@ void main() { }); group('Pockets', () { + test('empty', () { + expect(Pockets.empty.size, 0); + for (final side in Side.values) { + for (final role in Role.values) { + expect(Pockets.empty.of(side, role), 0); + } + expect(Pockets.empty.hasPawn(side), false); + expect(Pockets.empty.hasQuality(side), false); + } + }); + test('increment', () { final pockets = Pockets.empty.increment(Side.white, Role.knight); - expect(pockets.hasPawn(Side.white), false); - expect(pockets.hasQuality(Side.white), true); expect(pockets.of(Side.white, Role.knight), 1); expect(pockets.size, 1); expect( @@ -125,6 +134,98 @@ void main() { 0); }); + test('increment/decrement round-trip back to empty', () { + for (final side in Side.values) { + for (final role in Role.values) { + expect( + Pockets.empty.increment(side, role).decrement(side, role), + Pockets.empty, + ); + } + } + }); + + test('of — all roles on both sides are independent (no bitfield overlap)', + () { + for (final side in Side.values) { + for (final role in Role.values) { + final p = Pockets.empty.increment(side, role); + // Only the targeted slot is non-zero. + for (final s in Side.values) { + for (final r in Role.values) { + expect(p.of(s, r), s == side && r == role ? 1 : 0); + } + } + } + } + }); + + test('of — black side is independent from white', () { + final p = Pockets.empty + .increment(Side.white, Role.rook) + .increment(Side.black, Role.queen); + expect(p.of(Side.white, Role.rook), 1); + expect(p.of(Side.black, Role.queen), 1); + expect(p.of(Side.white, Role.queen), 0); + expect(p.of(Side.black, Role.rook), 0); + }); + + test('size counts pieces across both sides', () { + final p = Pockets.empty + .increment(Side.white, Role.knight) + .increment(Side.white, Role.knight) + .increment(Side.black, Role.pawn); + expect(p.size, 3); + }); + + test('count sums both sides for a role', () { + final p = Pockets.empty + .increment(Side.white, Role.rook) + .increment(Side.white, Role.rook) + .increment(Side.black, Role.rook); + expect(p.count(Role.rook), 3); + expect(p.count(Role.pawn), 0); + }); + + test('hasPawn', () { + expect(Pockets.empty.hasPawn(Side.white), false); + expect(Pockets.empty.hasPawn(Side.black), false); + + final p = Pockets.empty.increment(Side.black, Role.pawn); + expect(p.hasPawn(Side.black), true); + expect(p.hasPawn(Side.white), false); + }); + + test('hasQuality', () { + expect(Pockets.empty.hasQuality(Side.white), false); + + // Pawn alone does not count as quality. + expect( + Pockets.empty.increment(Side.white, Role.pawn).hasQuality(Side.white), + false, + ); + + // Each non-pawn role counts as quality. + for (final role in [ + Role.knight, + Role.bishop, + Role.rook, + Role.queen, + Role.king + ]) { + expect( + Pockets.empty.increment(Side.white, role).hasQuality(Side.white), + true, + reason: '$role should count as quality', + ); + } + + // Black side is checked independently. + final p = Pockets.empty.increment(Side.black, Role.bishop); + expect(p.hasQuality(Side.black), true); + expect(p.hasQuality(Side.white), false); + }); + test('implements ==', () { expect(Pockets.empty, Pockets.empty); @@ -137,14 +238,6 @@ void main() { final d = Pockets.empty.increment(Side.white, Role.pawn); expect(a, isNot(d)); - - // incrementing then decrementing round-trips back to empty - expect( - Pockets.empty - .increment(Side.white, Role.rook) - .decrement(Side.white, Role.rook), - Pockets.empty, - ); }); test('implements hashCode', () { @@ -154,7 +247,6 @@ void main() { expect(Pockets.empty.hashCode, Pockets.empty.hashCode); - // Different pockets should (almost certainly) have different hashes. final c = Pockets.empty.increment(Side.black, Role.queen); expect(a.hashCode, isNot(c.hashCode));