Make Position completely immutable by switching pockets implementation

This commit is contained in:
Vincent Velociter
2026-05-18 10:55:10 +02:00
parent cb89b3ab3c
commit 829538954c
2 changed files with 130 additions and 65 deletions
+27 -54
View File
@@ -213,69 +213,56 @@ class Setup {
/// Pockets (captured pieces) in chess variants like [Crazyhouse].
@immutable
class Pockets {
const Pockets({required BySide<ByRole<int>> value}) : _value = value;
const Pockets._(this._value);
final BySide<ByRole<int>> _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<int> _emptyPocket = {
Role.pawn: 0,
Role.knight: 0,
Role.bishop: 0,
Role.rook: 0,
Role.queen: 0,
Role.king: 0,
};
const BySide<ByRole<int>> _emptyPocketsBySide = {
Side.white: _emptyPocket,
Side.black: _emptyPocket,
};
+103 -11
View File
@@ -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));