Skip to content

Commit

Permalink
Crazyhouse en passant bug with DropMove
Browse files Browse the repository at this point in the history
  • Loading branch information
dbergan committed Apr 17, 2023
1 parent 9305591 commit d908c68
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 2 deletions.
6 changes: 5 additions & 1 deletion lib/src/debug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion lib/src/position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,17 @@ abstract class Position<T extends Position<T>> {
}
}

/// Plays a move from a Standard Algebraic Notation string.
///
/// Throws a [PlayError] if the move is not legal.
Position<T> 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<T> playUnchecked(Move move) {
assert(move is NormalMove || move is DropMove);
Expand Down Expand Up @@ -1526,7 +1537,7 @@ class Crazyhouse extends Position<Crazyhouse> {
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,
);
Expand Down
5 changes: 5 additions & 0 deletions lib/src/setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ class Pockets {

@override
int get hashCode => value.hashCode;

@override
String toString() {
return _makePockets(this);
}
}

Pockets _parsePockets(String pocketPart) {
Expand Down
48 changes: 48 additions & 0 deletions test/crazyhouse_test.dart
Original file line number Diff line number Diff line change
@@ -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<Move> 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);
});
}
164 changes: 164 additions & 0 deletions test/db_testing_lib.dart
Original file line number Diff line number Diff line change
@@ -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 = <int>[];
const whiteHomeRow = <int>[
0,
1,
2,
3,
4,
5,
6,
7,
];
const blackHomeRow = <int>[
56,
57,
58,
59,
60,
61,
62,
63,
];

class MyExpectations {
final int legalDrops;
final int legalMoves;
final DropZone legalDropZone;
final List<Role> rolesThatCanDrop;
final List<Role> rolesThatCantDrop;

const MyExpectations(
{required this.legalMoves,
required this.legalDrops,
required this.legalDropZone,
required this.rolesThatCanDrop,
required this.rolesThatCantDrop});

String testLegalMoves(List<Move> a) {
if (a.whereType<NormalMove>().length != legalMoves) {
return 'Expected $legalMoves legal moves, got ${a.whereType<NormalMove>().length}';
}
if (a.whereType<DropMove>().length != legalDrops) {
return 'Expected $legalDrops legal drops, got ${a.whereType<DropMove>().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<DropMove> dropTestEachSquare(Position position) {
final legalDrops = <DropMove>[];
final allRoles = <Role>[
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<NormalMove> moveTestEachSquare(Position position) {
final legalMoves = <NormalMove>[];
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<Move> printBoard(Position a, {bool printLegalMoves = false}) {
final z = StringBuffer();
final y = StringBuffer();
String x = '';
int moves = 0;
int drops = 0;

final legalMoves = <Move>[];
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;
}

0 comments on commit d908c68

Please sign in to comment.