diff --git a/lib/src/debug.dart b/lib/src/debug.dart index 95f240e..7203e7e 100644 --- a/lib/src/debug.dart +++ b/lib/src/debug.dart @@ -3,6 +3,7 @@ import './models.dart'; import './position.dart'; import './square_set.dart'; import './utils.dart'; +import './setup.dart'; /// Takes a string and returns a SquareSet. Useful for debugging/testing purposes. /// @@ -55,8 +56,11 @@ String humanReadableSquareSet(SquareSet sq) { } /// Prints the board as a human readable string format -String humanReadableBoard(Board board) { +String humanReadableBoard(Board board, [Pockets? pockets]) { final buffer = StringBuffer(); + if (pockets != null) { + buffer.write('Pockets: $pockets\n'); + } for (int y = 7; y >= 0; y--) { for (int x = 0; x < 8; x++) { final square = x + y * 8; diff --git a/lib/src/position.dart b/lib/src/position.dart index d62473e..1f7840f 100644 --- a/lib/src/position.dart +++ b/lib/src/position.dart @@ -557,6 +557,17 @@ abstract class Position> { } } + /// Plays a move from a Standard Algebraic Notation string. + /// + /// Throws a [PlayError] if the move is not legal. + Position playSan(String san) { + final move = parseSan(san); + if (move == null) { + throw PlayError('Invalid SAN $san'); + } + return play(move); + } + /// Plays a move without checking if the move is legal. Position playUnchecked(Move move) { assert(move is NormalMove || move is DropMove); @@ -1526,7 +1537,7 @@ class Crazyhouse extends Position { pockets: pockets != null ? pockets.value : this.pockets, turn: turn ?? this.turn, castles: castles ?? this.castles, - epSquare: epSquare != null ? epSquare.value : this.epSquare, + epSquare: epSquare?.value, halfmoves: halfmoves ?? this.halfmoves, fullmoves: fullmoves ?? this.fullmoves, ); diff --git a/lib/src/setup.dart b/lib/src/setup.dart index a6050df..19bab7d 100644 --- a/lib/src/setup.dart +++ b/lib/src/setup.dart @@ -268,6 +268,11 @@ class Pockets { @override int get hashCode => value.hashCode; + + @override + String toString() { + return _makePockets(this); + } } Pockets _parsePockets(String pocketPart) { diff --git a/test/crazyhouse_test.dart b/test/crazyhouse_test.dart new file mode 100644 index 0000000..6a04982 --- /dev/null +++ b/test/crazyhouse_test.dart @@ -0,0 +1,48 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:test/test.dart'; +import 'db_testing_lib.dart'; + +void main() { + test('Crazyhouse - issue #23: Crazyhouse en passant bug with DropMove', () { + Position a = Crazyhouse.initial; + a = a.playSan('d4'); + a = a.playSan('e5'); + a = a.playSan('Nf3'); + a = a.playSan('Qg5'); + a = a.playSan('Nxg5'); + a = a.playSan('Be7'); + a = a.playSan('dxe5'); + a = a.playSan('Bd8'); + + a = a.playSan('Nc3'); + printBoard(a, printLegalMoves: true); + + a = a.playSan('f5'); // creates epSquare at f6 for White + printBoard(a, printLegalMoves: true); + + a = a.playSan( + 'Q@f7'); // Bug: copyWith() is given null for epSquare, which then copies White's epSquare of f6 forward to Black + final List legalMoves = printBoard(a, printLegalMoves: true); + const MyExpectations myExpectations = MyExpectations( + legalMoves: 0, + legalDrops: 0, + legalDropZone: DropZone.anywhere, + rolesThatCanDrop: [], + rolesThatCantDrop: [ + Role.king, + Role.queen, + Role.rook, + Role.bishop, + Role.knight, + Role.pawn, + ], + ); + + expect(myExpectations.testLegalMoves(legalMoves), ''); + + expect(a.outcome, Outcome.whiteWins); + + // a = a.playSan('gxf6'); // captures the Queen at f7! + // printBoard(a, printLegalMoves: true); + }); +} diff --git a/test/db_testing_lib.dart b/test/db_testing_lib.dart new file mode 100644 index 0000000..e81ad99 --- /dev/null +++ b/test/db_testing_lib.dart @@ -0,0 +1,164 @@ +import 'package:dartchess/dartchess.dart'; + +const verbosePrinting = false; +bool printedNotice = false; + +void conditionalPrint(Object? a) { + if (!printedNotice) { + printedNotice = true; + print('=' * 60); + print('${'=\tVERBOSE PRINTING'.padRight(53)}='); + if (verbosePrinting) { + print('${'=\tverbosePrinting is ON'.padRight(53)}='); + print('${'=\t'.padRight(53)}='); + print('${'=\tTo disable, set the "verbosePrinting"'.padRight(53)}='); + print('${'=\tconstant to false'.padRight(53)}='); + } else { + print('${'=\tverbosePrinting is OFF'.padRight(53)}='); + print('${'=\t'.padRight(53)}='); + print( + '${'=\tSet the "verbosePrinting" constant to true to help'.padRight(53)}='); + print('${'=\twith debugging these tests'.padRight(53)}='); + } + print('${'=\t(line 3 of \\test\\db_testing_lib.dart)'.padRight(53)}='); + print('=' * 60); + } + if (verbosePrinting) { + print(a); + } +} + +enum DropZone { none, whiteHomeRow, blackHomeRow, anywhere } + +const noDrops = []; +const whiteHomeRow = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, +]; +const blackHomeRow = [ + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, +]; + +class MyExpectations { + final int legalDrops; + final int legalMoves; + final DropZone legalDropZone; + final List rolesThatCanDrop; + final List rolesThatCantDrop; + + const MyExpectations( + {required this.legalMoves, + required this.legalDrops, + required this.legalDropZone, + required this.rolesThatCanDrop, + required this.rolesThatCantDrop}); + + String testLegalMoves(List a) { + if (a.whereType().length != legalMoves) { + return 'Expected $legalMoves legal moves, got ${a.whereType().length}'; + } + if (a.whereType().length != legalDrops) { + return 'Expected $legalDrops legal drops, got ${a.whereType().length}'; + } + for (final move in a) { + if (move is DropMove) { + if (rolesThatCantDrop.contains(move.role)) { + return '${move.role} is listed in rolesThatCantDrop'; + } + if (!rolesThatCanDrop.contains(move.role)) { + return '${move.role} is not listed in rolesThatCanDrop'; + } + if (legalDropZone == DropZone.anywhere && + move.role == Role.pawn && + (whiteHomeRow.contains(move.to) || + blackHomeRow.contains(move.to))) { + return 'Drop zone is anywhere, but a pawn cannot be dropped in rows 1 or 8'; + } else if (legalDropZone == DropZone.whiteHomeRow && + !whiteHomeRow.contains(move.to)) { + return 'Drop zone is whiteHomeRow, but ${move.to} is not in whiteHomeRow'; + } else if (legalDropZone == DropZone.blackHomeRow && + !blackHomeRow.contains(move.to)) { + return 'Drop zone is blackHomeRow, but ${move.to} is not in whiteHomeRow'; + } else if (legalDropZone == DropZone.none && + !noDrops.contains(move.to)) { + return 'Drop zone is none, but ${move.to} is not in noDrops'; + } + } + } + return ''; + } +} + +List dropTestEachSquare(Position position) { + final legalDrops = []; + final allRoles = [ + Role.pawn, + Role.knight, + Role.bishop, + Role.rook, + Role.queen, + Role.king + ]; + for (final pieceRole in allRoles) { + for (int a = 0; a < 64; a++) { + if (position.isLegal(DropMove(role: pieceRole, to: a))) { + legalDrops.add(DropMove(role: pieceRole, to: a)); + } + } + } + return legalDrops; +} + +List moveTestEachSquare(Position position) { + final legalMoves = []; + for (int a = 0; a < 64; a++) { + for (int b = 0; b < 64; b++) { + if (position.isLegal(NormalMove(from: a, to: b))) { + legalMoves.add(NormalMove(from: a, to: b)); + } + } + } + return legalMoves; +} + +List printBoard(Position a, {bool printLegalMoves = false}) { + final z = StringBuffer(); + final y = StringBuffer(); + String x = ''; + int moves = 0; + int drops = 0; + + final legalMoves = []; + if (printLegalMoves) { + for (final move in moveTestEachSquare(a)) { + z.write('${move.uci}, '); + legalMoves.add(move); + moves++; + } + x = 'Legal Moves ($moves):\n$z\n'; + for (final drop in dropTestEachSquare(a)) { + y.write('${drop.uci}, '); + legalMoves.add(drop); + drops++; + } + x += 'Legal Drops ($drops):\n$y'; + } + + conditionalPrint('${humanReadableBoard(a.board, a.pockets)}$x'); + conditionalPrint( + '------------------------------------------------------------'); + return legalMoves; +}