Merge pull request #55 from lichess-org/remove_fic

This commit is contained in:
Vincent Velociter
2026-05-18 11:15:44 +02:00
committed by GitHub
16 changed files with 387 additions and 158 deletions
+18
View File
@@ -1,3 +1,21 @@
## 0.13.0
**Breaking changes:**
- Remove `fast_immutable_collections` dependency. Public APIs that previously returned `IMap` or `IList` now return standard Dart collections:
- `Position.legalMoves` returns `Map<Square, SquareSet>` (was `IMap<Square, SquareSet>`)
- `Board.materialCount` returns `ByRole<int>` / `Map<Role, int>` (was `IMap<Role, int>`)
- `Castles.rooksPositions` returns `BySide<ByCastlingSide<Square?>>` / `Map` (was `IMap`-backed)
- `Castles.paths` returns `BySide<ByCastlingSide<SquareSet>>` / `Map` (was `IMap`-backed)
- `PgnComment.shapes` returns `List<PgnCommentShape>` (was `IList<PgnCommentShape>`)
- `makeLegalMoves()` returns `Map<Square, Set<Square>>` (was `IMap<Square, ISet<Square>>`)
- The `BySide<T>`, `ByRole<T>`, and `ByCastlingSide<T>` typedefs are now aliases for standard `Map` types.
- `Pockets.value` is removed; use `Pockets.of(side, role)`, `Pockets.count(role)`, `Pockets.size`, `Pockets.hasQuality(side)`, and `Pockets.hasPawn(side)` instead.
The `Position` class remains completely immutable. Only the return types of some
methods have changed to use standard Dart collections instead of immutable
collections.
## 0.12.3
- Fix `Crazyhouse.isGameOver` and `Crazyhouse.isCheckmate` in positions where all legal moves are drop moves.
+72
View File
@@ -0,0 +1,72 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Run all tests (exclude slow full_perft tests)
dart test -x full_perft
# Run a single test file
dart test test/position_test.dart
# Run a specific test by name
dart test --name "some test name"
# Analyze code (lint)
dart analyze
# Format code
dart format .
# Run benchmarks
dart run benchmark/dartchess_benchmark.dart
# Run perft (move generation correctness/performance test)
dart run example/perft.dart
```
## Architecture
This is a pure Dart chess rules library (`package:dartchess`) supporting standard chess and several variants. It targets native platforms only (not web).
### Core data flow
`Setup` (FEN parse) → `Position` (validated legal state) → `Move` (play/apply) → new `Position`
### Key layers
**`models.dart`** — fundamental value types: `Side`, `Role`, `Piece`, `File`, `Rank`, `Square`, `Move` (sealed: `NormalMove` / `DropMove`), `Rule` (the variant enum), `Outcome`, and exception types (`FenException`, `PositionSetupException`).
**`square_set.dart`** — `SquareSet` is a 64-bit integer bitboard (little-endian rank-file mapping). All move generation and attack computation operates on `SquareSet` values. Prefer bitwise operations over iterating squares when working in this layer.
**`attacks.dart`** — precomputed attack tables for kings and knights; bishop/rook/queen attacks via hyperbola quintessence (sliding piece attacks with `occupied` mask). Exposes `kingAttacks`, `knightAttacks`, `pawnAttacks`, `bishopAttacks`, `rookAttacks`, `queenAttacks`.
**`board.dart`** — `Board` stores piece placement as a set of overlapping `SquareSet`s (one per side, one per role). Immutable; queries like `board.bySide(side)`, `board.byRole(role)`, `board.kingOf(side)`, `board.attacksTo(square, attacker)`.
**`castles.dart`** — `Castles` tracks unmoved rooks and the path squares needed for castling legality. Supports Chess960 (king-to-rook encoding for castling moves).
**`setup.dart`** — `Setup` is a non-validated position read from FEN. Parses/emits FEN, including pocket notation for Crazyhouse and remaining checks for ThreeCheck.
**`position.dart`** — the heart of the library. `Position` is an immutable abstract base class; each variant subclasses it: `Chess`, `Antichess`, `Atomic`, `Crazyhouse`, `KingOfTheHill`, `ThreeCheck`, `RacingKings`, `Horde`. Concrete private implementations (`_Chess`, etc.) are returned by `fromSetup`. Key API:
- `Position.setupPosition(rule, setup)` — variant-aware factory
- `pos.legalMoves``Map<Square, SquareSet>` (king-to-rook encoding for castling)
- `pos.play(move)` / `pos.playUnchecked(move)` — returns new position
- `pos.parseSan(san)` / `pos.makeSan(move)` — SAN I/O
- `pos.isCheckmate`, `pos.isStalemate`, `pos.isGameOver`, `pos.outcome`
- Castling moves are encoded as king-to-rook; use `makeLegalMoves()` (from `utils.dart`) to also include the traditional king-to-destination squares.
**`pgn.dart`** — `PgnGame<T>` holds headers + a `PgnNode<T>` tree of moves (with variations, comments, NAGs, shapes, evaluations). `PgnGame.parsePgn` / `PgnGame.parseMultiGamePgn` for parsing; `game.makePgn()` for serialization. Use `PgnNode.transform` to walk the tree and attach computed data (e.g. FEN per node) without mutating nodes.
**`utils.dart`** — `makeLegalMoves(pos)` returns `Map<Square, Set<Square>>` adding traditional castling destinations alongside king-to-rook destinations.
**`debug.dart`** — `toSfen` helpers for printing boards in ASCII; primarily for testing and debugging.
### Immutability
All `Position`, `Board`, `Setup`, `Castles` instances are immutable (`@immutable`). Standard Dart collections are used throughout; immutability is enforced by the library's internal discipline rather than FIC types.
### Chess960
Castling is internally encoded king-to-rook throughout. `makeLegalMoves` adds the standard king-to-g/c destination for display purposes. When playing a castling move from UI, both encodings are accepted by `isLegal`.
+5 -1
View File
@@ -33,10 +33,14 @@ void main() {
final legalMovesPos = Chess.fromSetup(Setup.parseFen(
'rn1qkb1r/pbp2ppp/1p2p3/3n4/8/2N2NP1/PP1PPPBP/R1BQ1RK1 b kq -'));
benchmark('valid fen moves', () {
benchmark('valid moves', () {
legalMovesPos.legalMoves.length;
});
benchmark('makeLegalMoves (with alternate castling moves)', () {
makeLegalMoves(legalMovesPos);
});
benchmark('parsePgn - kasparov-deep-blue', () {
final String data =
io.File('./data/kasparov-deep-blue-1997.pgn').readAsStringSync();
+1 -2
View File
@@ -1,5 +1,4 @@
import 'package:meta/meta.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import './square_set.dart';
import './models.dart';
import './attacks.dart';
@@ -185,7 +184,7 @@ class Board {
}
/// Gets the number of pieces of each [Role] for the given [Side].
IMap<Role, int> materialCount(Side side) => IMap.fromEntries(
ByRole<int> materialCount(Side side) => Map.fromEntries(
Role.values.map((role) => MapEntry(role, piecesOf(side, role).size)));
/// A [SquareSet] of all the pieces matching this [Side] and [Role].
+12 -12
View File
@@ -105,30 +105,30 @@ abstract class Castles {
/// Gets rooks positions by side and castling side.
BySide<ByCastlingSide<Square?>> get rooksPositions {
return BySide({
Side.white: ByCastlingSide({
return {
Side.white: {
CastlingSide.queen: _whiteRookQueenSide,
CastlingSide.king: _whiteRookKingSide,
}),
Side.black: ByCastlingSide({
},
Side.black: {
CastlingSide.queen: _blackRookQueenSide,
CastlingSide.king: _blackRookKingSide,
}),
});
},
};
}
/// Gets rooks paths by side and castling side.
BySide<ByCastlingSide<SquareSet>> get paths {
return BySide({
Side.white: ByCastlingSide({
return {
Side.white: {
CastlingSide.queen: _whitePathQueenSide,
CastlingSide.king: _whitePathKingSide,
}),
Side.black: ByCastlingSide({
},
Side.black: {
CastlingSide.queen: _blackPathQueenSide,
CastlingSide.king: _blackPathKingSide,
}),
});
},
};
}
/// Gets the rook [Square] by side and castling side.
+3 -4
View File
@@ -1,5 +1,4 @@
import 'package:meta/meta.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import './square_set.dart';
/// The chessboard side, white or black.
@@ -337,9 +336,9 @@ extension type const Square._(int value) implements int {
static const h8 = Square(63);
}
typedef BySide<T> = IMap<Side, T>;
typedef ByRole<T> = IMap<Role, T>;
typedef ByCastlingSide<T> = IMap<CastlingSide, T>;
typedef BySide<T> = Map<Side, T>;
typedef ByRole<T> = Map<Role, T>;
typedef ByCastlingSide<T> = Map<CastlingSide, T>;
/// Describes a chess piece kind by its color and role.
enum PieceKind {
+18 -16
View File
@@ -1,5 +1,4 @@
import 'dart:math' as math;
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:meta/meta.dart';
import './setup.dart';
@@ -493,18 +492,14 @@ class PgnEvaluation {
@immutable
class PgnComment {
const PgnComment(
{this.text,
this.shapes = const IListConst([]),
this.clock,
this.emt,
this.eval})
{this.text, this.shapes = const [], this.clock, this.emt, this.eval})
: assert(text == null || text != '');
/// Comment string.
final String? text;
/// List of comment shapes.
final IList<PgnCommentShape> shapes;
final List<PgnCommentShape> shapes;
/// Player's remaining time.
final Duration? clock;
@@ -571,7 +566,7 @@ class PgnComment {
return PgnComment(
text: text.isNotEmpty ? text : null,
shapes: IList(shapes),
shapes: shapes,
emt: emt,
clock: clock,
eval: eval);
@@ -601,17 +596,24 @@ class PgnComment {
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is PgnComment &&
text == other.text &&
shapes == other.shapes &&
clock == other.clock &&
emt == other.emt &&
eval == other.eval;
if (identical(this, other)) return true;
if (other is! PgnComment) return false;
if (text != other.text ||
clock != other.clock ||
emt != other.emt ||
eval != other.eval) {
return false;
}
if (shapes.length != other.shapes.length) return false;
for (var i = 0; i < shapes.length; i++) {
if (shapes[i] != other.shapes[i]) return false;
}
return true;
}
@override
int get hashCode => Object.hash(text, shapes, clock, emt, eval);
int get hashCode =>
Object.hash(text, Object.hashAll(shapes), clock, emt, eval);
}
/// A frame used for parsing a line
+4 -5
View File
@@ -1,6 +1,5 @@
import 'package:meta/meta.dart';
import 'dart:math' as math;
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'attacks.dart';
import 'castles.dart';
import 'models.dart';
@@ -191,13 +190,13 @@ abstract class Position {
///
/// Use the [makeLegalMoves] helper to get all the legal moves including alternative
/// castling moves.
IMap<Square, SquareSet> get legalMoves {
Map<Square, SquareSet> get legalMoves {
final context = _makeContext();
if (context.isVariantEnd) return IMap(const {});
return IMap({
if (context.isVariantEnd) return const {};
return {
for (final s in board.bySide(turn).squares)
s: _legalMovesOf(s, context: context)
});
};
}
/// Gets all the legal drops of this position.
+28 -51
View File
@@ -1,5 +1,4 @@
import 'package:meta/meta.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'dart:math' as math;
import './square_set.dart';
import './models.dart';
@@ -214,64 +213,56 @@ class Setup {
/// Pockets (captured pieces) in chess variants like [Crazyhouse].
@immutable
class Pockets {
/// Creates a new [Pockets] with the provided value.
const Pockets({
required this.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]!.add(role, of(side, role) + 1);
return Pockets(value: value.add(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]!.add(role, of(side, role) - 1);
return Pockets(value: value.add(side, newPocket));
}
Pockets decrement(Side side, Role role) =>
Pockets._(_value - (1 << _offset(side, role)));
@override
bool operator ==(Object other) {
return identical(this, other) || other is Pockets && other.value == value;
if (identical(this, other)) return true;
return other is Pockets && other._value == _value;
}
@override
int get hashCode => value.hashCode;
int get hashCode => _value.hashCode;
}
Pockets _parsePockets(String pocketPart) {
@@ -397,17 +388,3 @@ int _nthIndexOf(String haystack, String needle, int nth) {
}
return index;
}
const ByRole<int> _emptyPocket = IMapConst({
Role.pawn: 0,
Role.knight: 0,
Role.bishop: 0,
Role.rook: 0,
Role.queen: 0,
Role.king: 0,
});
const BySide<ByRole<int>> _emptyPocketsBySide = IMapConst({
Side.white: _emptyPocket,
Side.black: _emptyPocket,
});
+4 -6
View File
@@ -1,16 +1,14 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'models.dart';
import 'position.dart';
/// Returns all the legal moves of the [Position] in a convenient format.
///
/// Includes both possible representations of castling moves unless `includeAlternateCastlingMoves` is false.
IMap<Square, ISet<Square>> makeLegalMoves(
Map<Square, Set<Square>> makeLegalMoves(
Position pos, {
bool includeAlternateCastlingMoves = true,
}) {
final Map<Square, ISet<Square>> result = {};
final Map<Square, Set<Square>> result = {};
for (final entry in pos.legalMoves.entries) {
final dests = entry.value.squares;
if (dests.isNotEmpty) {
@@ -30,8 +28,8 @@ IMap<Square, ISet<Square>> makeLegalMoves(
destSet.add(Square.g8);
}
}
result[from] = ISet(destSet);
result[from] = destSet;
}
}
return IMap(result);
return result;
}
+1 -2
View File
@@ -1,7 +1,7 @@
name: dartchess
description: Provides chess and chess variants rules and operations including chess move generation, read and write FEN, read and write PGN.
repository: https://github.com/lichess-org/dartchess
version: 0.12.3
version: 0.13.0
platforms:
android:
ios:
@@ -15,7 +15,6 @@ environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
fast_immutable_collections: ^11.0.0
meta: ^1.8.0
dev_dependencies:
+19 -19
View File
@@ -1,4 +1,3 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:dartchess/dartchess.dart';
import 'package:test/test.dart';
@@ -40,34 +39,35 @@ void main() {
expect(Castles.standard.discardRookAt(Square.a4), Castles.standard);
expect(
Castles.standard.discardRookAt(Square.h1).rooksPositions[Side.white],
IMap(const {CastlingSide.queen: Square.a1, CastlingSide.king: null}));
const {CastlingSide.queen: Square.a1, CastlingSide.king: null});
});
test('discard side', () {
expect(
Castles.standard.discardSide(Side.white).rooksPositions,
equals(BySide({
Side.white: ByCastlingSide(
const {CastlingSide.queen: null, CastlingSide.king: null},
),
Side.black: ByCastlingSide(
const {
CastlingSide.queen: Square.a8,
CastlingSide.king: Square.h8,
},
)
})));
equals({
Side.white: const {
CastlingSide.queen: null,
CastlingSide.king: null
},
Side.black: const {
CastlingSide.queen: Square.a8,
CastlingSide.king: Square.h8,
},
}));
expect(
Castles.standard.discardSide(Side.black).rooksPositions,
equals(BySide({
Side.white: ByCastlingSide(const {
equals({
Side.white: const {
CastlingSide.queen: Square.a1,
CastlingSide.king: Square.h1,
}),
Side.black: ByCastlingSide(
const {CastlingSide.queen: null, CastlingSide.king: null})
})));
},
Side.black: const {
CastlingSide.queen: null,
CastlingSide.king: null
},
}));
});
});
}
+65 -31
View File
@@ -1,6 +1,5 @@
import 'package:dartchess/dartchess.dart' hide File;
import 'package:test/test.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'dart:io';
import 'pgn_fixtures.dart';
@@ -109,26 +108,18 @@ void main() {
expect(
PgnComment.fromPgn(
'[%csl Ya1][%cal Ra1a1,Be1e2]commentary [%csl Gh8]'),
const PgnComment(
text: 'commentary',
shapes: IListConst([
PgnCommentShape(
color: CommentShapeColor.yellow,
from: Square.a1,
to: Square.a1),
PgnCommentShape(
color: CommentShapeColor.red,
from: Square.a1,
to: Square.a1),
PgnCommentShape(
color: CommentShapeColor.blue,
from: Square.e1,
to: Square.e2),
PgnCommentShape(
color: CommentShapeColor.green,
from: Square.h8,
to: Square.h8)
])));
const PgnComment(text: 'commentary', shapes: [
PgnCommentShape(
color: CommentShapeColor.yellow,
from: Square.a1,
to: Square.a1),
PgnCommentShape(
color: CommentShapeColor.red, from: Square.a1, to: Square.a1),
PgnCommentShape(
color: CommentShapeColor.blue, from: Square.e1, to: Square.e2),
PgnCommentShape(
color: CommentShapeColor.green, from: Square.h8, to: Square.h8)
]));
expect(
PgnComment.fromPgn('prefix [%eval .99,23]'),
@@ -145,14 +136,10 @@ void main() {
expect(
PgnComment.fromPgn('[%csl Ga1]foo'),
const PgnComment(
text: 'foo',
shapes: IListConst([
PgnCommentShape(
color: CommentShapeColor.green,
from: Square.a1,
to: Square.a1)
])));
const PgnComment(text: 'foo', shapes: [
PgnCommentShape(
color: CommentShapeColor.green, from: Square.a1, to: Square.a1)
]));
expect(
PgnComment.fromPgn(
@@ -169,7 +156,7 @@ void main() {
Duration(hours: 1, minutes: 2, seconds: 3, milliseconds: 400),
eval: PgnEvaluation.pawns(pawns: 10),
clock: Duration(seconds: 1),
shapes: IListConst([
shapes: [
PgnCommentShape(
color: CommentShapeColor.yellow,
from: Square.a1,
@@ -182,7 +169,7 @@ void main() {
color: CommentShapeColor.red,
from: Square.a1,
to: Square.c1)
])).makeComment(),
]).makeComment(),
'text [%csl Ya1] [%cal Ra1b1,Ra1c1] [%eval 10.00] [%emt 1:02:03.4] [%clk 0:00:01]');
expect(
@@ -208,6 +195,53 @@ void main() {
test('PgnComment implements hashCode/==', () {
const comment = '[%csl Ga1][%cal Ra1h1,Gb1b8] foo [%clk 3:25:45]';
expect(PgnComment.fromPgn(comment) == PgnComment.fromPgn(comment), true);
expect(PgnComment.fromPgn(comment).hashCode,
PgnComment.fromPgn(comment).hashCode);
});
test('PgnComment == distinguishes shapes content', () {
const withShape = PgnComment(shapes: [
PgnCommentShape(
color: CommentShapeColor.green, from: Square.a1, to: Square.a1),
]);
const withDifferentShape = PgnComment(shapes: [
PgnCommentShape(
color: CommentShapeColor.red, from: Square.a1, to: Square.a1),
]);
const withoutShape = PgnComment();
expect(withShape, withShape);
expect(withShape, isNot(withDifferentShape));
expect(withShape, isNot(withoutShape));
expect(withoutShape, withoutShape);
});
test('PgnComment == is order-sensitive for shapes', () {
const shapeA = PgnCommentShape(
color: CommentShapeColor.green, from: Square.a1, to: Square.h1);
const shapeB = PgnCommentShape(
color: CommentShapeColor.red, from: Square.b1, to: Square.b8);
const ab = PgnComment(shapes: [shapeA, shapeB]);
const ba = PgnComment(shapes: [shapeB, shapeA]);
expect(ab, isNot(ba));
});
test('PgnComment hashCode consistent with ==', () {
const shapeA = PgnCommentShape(
color: CommentShapeColor.green, from: Square.a1, to: Square.h1);
const shapeB = PgnCommentShape(
color: CommentShapeColor.red, from: Square.b1, to: Square.b8);
const c1 = PgnComment(text: 'hello', shapes: [shapeA, shapeB]);
const c2 = PgnComment(text: 'hello', shapes: [shapeA, shapeB]);
expect(c1, c2);
expect(c1.hashCode, c2.hashCode);
// Can be used as a map key.
final map = {c1: 42};
expect(map[c2], 42);
});
group('Invalid Pgns', () {
+4 -5
View File
@@ -1,4 +1,3 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:dartchess/dartchess.dart';
import 'package:test/test.dart';
import 'dart:io' as io;
@@ -361,7 +360,7 @@ void main() {
});
test('standard position legal moves', () {
final moves = IMap({
final moves = {
Square.a1: SquareSet.empty,
Square.b1: const SquareSet.fromSquare(Square.a3).withSquare(Square.c3),
Square.c1: SquareSet.empty,
@@ -378,7 +377,7 @@ void main() {
Square.f2: const SquareSet.fromSquare(Square.f3).withSquare(Square.f4),
Square.g2: const SquareSet.fromSquare(Square.g3).withSquare(Square.g4),
Square.h2: const SquareSet.fromSquare(Square.h3).withSquare(Square.h4),
});
};
expect(Chess.initial.legalMoves, equals(moves));
});
@@ -665,10 +664,10 @@ void main() {
.play(const NormalMove(from: Square.h1, to: Square.f1));
expect(
pos.castles.rooksPositions[Side.white],
equals(IMap(const {
equals(const {
CastlingSide.queen: Square.a1,
CastlingSide.king: null
})));
}));
expect(pos.castles.castlingRights.has(Square.h1), false);
});
+132 -2
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(
@@ -124,5 +133,126 @@ void main() {
.of(Side.white, Role.knight),
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);
final a = Pockets.empty.increment(Side.white, Role.knight);
final b = Pockets.empty.increment(Side.white, Role.knight);
expect(a, b);
final c = Pockets.empty.increment(Side.black, Role.knight);
expect(a, isNot(c));
final d = Pockets.empty.increment(Side.white, Role.pawn);
expect(a, isNot(d));
});
test('implements hashCode', () {
final a = Pockets.empty.increment(Side.white, Role.knight);
final b = Pockets.empty.increment(Side.white, Role.knight);
expect(a.hashCode, b.hashCode);
expect(Pockets.empty.hashCode, Pockets.empty.hashCode);
final c = Pockets.empty.increment(Side.black, Role.queen);
expect(a.hashCode, isNot(c.hashCode));
// Can be used as a map key.
final map = {a: 'knight'};
expect(map[b], 'knight');
});
});
}
+1 -2
View File
@@ -1,5 +1,4 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:test/test.dart';
void main() {
@@ -61,7 +60,7 @@ void main() {
);
expect(
makeLegalMoves(pos, includeAlternateCastlingMoves: false)[Square.b8],
equals(ISet(const {Square.a8, Square.c8, Square.e8})),
equals(const {Square.a8, Square.c8, Square.e8}),
);
});
}