mirror of
https://github.com/lichess-org/dartchess.git
synced 2026-05-26 13:51:01 +00:00
Make Position completely immutable by switching pockets implementation
This commit is contained in:
+27
-54
@@ -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
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user