mirror of
https://github.com/lichess-org/dartchess.git
synced 2026-05-26 13:51:01 +00:00
Merge pull request #55 from lichess-org/remove_fic
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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', () {
|
||||
|
||||
@@ -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
@@ -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
@@ -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}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user