From 8243acdb38c4c255f82d976aadacdfb74a571c04 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 17 May 2026 14:16:19 +0200 Subject: [PATCH] Remove fast immutable collections --- CLAUDE.md | 8 ++++---- lib/src/board.dart | 3 +-- lib/src/castles.dart | 24 ++++++++++++------------ lib/src/models.dart | 7 +++---- lib/src/pgn.dart | 24 ++++++++++++------------ lib/src/position.dart | 9 ++++----- lib/src/setup.dart | 29 ++++++++++++++++++----------- pubspec.yaml | 1 - test/castles_test.dart | 32 +++++++++++++------------------- test/pgn_test.dart | 13 ++++++------- test/position_test.dart | 9 ++++----- utils_test.dart | 3 +-- 12 files changed, 78 insertions(+), 84 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c105b1b..3ae43d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands ```bash -# Run all tests -dart test +# Run all tests (exclude slow full_perft tests) +dart test -x full_perft # Run a single test file dart test test/position_test.dart @@ -51,7 +51,7 @@ This is a pure Dart chess rules library (`package:dartchess`) supporting standar **`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` — `IMap` (king-to-rook encoding for castling) +- `pos.legalMoves` — `Map` (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` @@ -65,7 +65,7 @@ This is a pure Dart chess rules library (`package:dartchess`) supporting standar ### Immutability -All `Position`, `Board`, `Setup`, `Castles` instances are immutable (`@immutable`). The library uses `fast_immutable_collections` (`IMap`, `ISet`) for collections stored in positions. +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 diff --git a/lib/src/board.dart b/lib/src/board.dart index dd6a0d0..cb37c78 100644 --- a/lib/src/board.dart +++ b/lib/src/board.dart @@ -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 materialCount(Side side) => IMap.fromEntries( + ByRole 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]. diff --git a/lib/src/castles.dart b/lib/src/castles.dart index 503e94f..b583e5b 100644 --- a/lib/src/castles.dart +++ b/lib/src/castles.dart @@ -105,30 +105,30 @@ abstract class Castles { /// Gets rooks positions by side and castling side. BySide> 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> 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. diff --git a/lib/src/models.dart b/lib/src/models.dart index b915f4e..3decf89 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -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 = IMap; -typedef ByRole = IMap; -typedef ByCastlingSide = IMap; +typedef BySide = Map; +typedef ByRole = Map; +typedef ByCastlingSide = Map; /// Describes a chess piece kind by its color and role. enum PieceKind { diff --git a/lib/src/pgn.dart b/lib/src/pgn.dart index 5bb6340..1264e3d 100644 --- a/lib/src/pgn.dart +++ b/lib/src/pgn.dart @@ -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'; @@ -494,7 +493,7 @@ class PgnEvaluation { class PgnComment { const PgnComment( {this.text, - this.shapes = const IListConst([]), + this.shapes = const [], this.clock, this.emt, this.eval}) @@ -504,7 +503,7 @@ class PgnComment { final String? text; /// List of comment shapes. - final IList shapes; + final List shapes; /// Player's remaining time. final Duration? clock; @@ -571,7 +570,7 @@ class PgnComment { return PgnComment( text: text.isNotEmpty ? text : null, - shapes: IList(shapes), + shapes: List.unmodifiable(shapes), emt: emt, clock: clock, eval: eval); @@ -601,17 +600,18 @@ 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 diff --git a/lib/src/position.dart b/lib/src/position.dart index 8f5a45c..340b0d4 100644 --- a/lib/src/position.dart +++ b/lib/src/position.dart @@ -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 get legalMoves { + Map 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. diff --git a/lib/src/setup.dart b/lib/src/setup.dart index 826d68f..fb5664a 100644 --- a/lib/src/setup.dart +++ b/lib/src/setup.dart @@ -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'; @@ -255,23 +254,31 @@ class Pockets { /// 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)); + final newPocket = {...value[side]!, role: of(side, role) + 1}; + return Pockets(value: {...value, side: newPocket}); } /// 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)); + final newPocket = {...value[side]!, role: of(side, role) - 1}; + return Pockets(value: {...value, side: newPocket}); } @override bool operator ==(Object other) { - return identical(this, other) || other is Pockets && other.value == value; + 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; } @override - int get hashCode => value.hashCode; + int get hashCode => Object.hashAll( + Side.values.expand((s) => Role.values.map((r) => of(s, r)))); } Pockets _parsePockets(String pocketPart) { @@ -398,16 +405,16 @@ int _nthIndexOf(String haystack, String needle, int nth) { return index; } -const ByRole _emptyPocket = IMapConst({ +const ByRole _emptyPocket = { Role.pawn: 0, Role.knight: 0, Role.bishop: 0, Role.rook: 0, Role.queen: 0, Role.king: 0, -}); +}; -const BySide> _emptyPocketsBySide = IMapConst({ +const BySide> _emptyPocketsBySide = { Side.white: _emptyPocket, Side.black: _emptyPocket, -}); +}; diff --git a/pubspec.yaml b/pubspec.yaml index 02d1fa0..5157c57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/castles_test.dart b/test/castles_test.dart index 552bf25..2482456 100644 --- a/test/castles_test.dart +++ b/test/castles_test.dart @@ -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,29 @@ 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}, + })); }); }); } diff --git a/test/pgn_test.dart b/test/pgn_test.dart index ce9be5b..01e0a3b 100644 --- a/test/pgn_test.dart +++ b/test/pgn_test.dart @@ -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'; @@ -111,7 +110,7 @@ void main() { '[%csl Ya1][%cal Ra1a1,Be1e2]commentary [%csl Gh8]'), const PgnComment( text: 'commentary', - shapes: IListConst([ + shapes: [ PgnCommentShape( color: CommentShapeColor.yellow, from: Square.a1, @@ -128,7 +127,7 @@ void main() { color: CommentShapeColor.green, from: Square.h8, to: Square.h8) - ]))); + ])); expect( PgnComment.fromPgn('prefix [%eval .99,23]'), @@ -147,12 +146,12 @@ void main() { PgnComment.fromPgn('[%csl Ga1]foo'), const PgnComment( text: 'foo', - shapes: IListConst([ + shapes: [ PgnCommentShape( color: CommentShapeColor.green, from: Square.a1, to: Square.a1) - ]))); + ])); expect( PgnComment.fromPgn( @@ -169,7 +168,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 +181,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( diff --git a/test/position_test.dart b/test/position_test.dart index 0cd92cf..717d279 100644 --- a/test/position_test.dart +++ b/test/position_test.dart @@ -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); }); diff --git a/utils_test.dart b/utils_test.dart index 4fd4cac..ea5c3b9 100644 --- a/utils_test.dart +++ b/utils_test.dart @@ -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}), ); }); }