Skip to content

Commit d908c68

Browse files
committed
Crazyhouse en passant bug with DropMove
Fixes lichess-org#23
1 parent 9305591 commit d908c68

File tree

5 files changed

+234
-2
lines changed

5 files changed

+234
-2
lines changed

lib/src/debug.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import './models.dart';
33
import './position.dart';
44
import './square_set.dart';
55
import './utils.dart';
6+
import './setup.dart';
67

78
/// Takes a string and returns a SquareSet. Useful for debugging/testing purposes.
89
///
@@ -55,8 +56,11 @@ String humanReadableSquareSet(SquareSet sq) {
5556
}
5657

5758
/// Prints the board as a human readable string format
58-
String humanReadableBoard(Board board) {
59+
String humanReadableBoard(Board board, [Pockets? pockets]) {
5960
final buffer = StringBuffer();
61+
if (pockets != null) {
62+
buffer.write('Pockets: $pockets\n');
63+
}
6064
for (int y = 7; y >= 0; y--) {
6165
for (int x = 0; x < 8; x++) {
6266
final square = x + y * 8;

lib/src/position.dart

+12-1
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,17 @@ abstract class Position<T extends Position<T>> {
557557
}
558558
}
559559

560+
/// Plays a move from a Standard Algebraic Notation string.
561+
///
562+
/// Throws a [PlayError] if the move is not legal.
563+
Position<T> playSan(String san) {
564+
final move = parseSan(san);
565+
if (move == null) {
566+
throw PlayError('Invalid SAN $san');
567+
}
568+
return play(move);
569+
}
570+
560571
/// Plays a move without checking if the move is legal.
561572
Position<T> playUnchecked(Move move) {
562573
assert(move is NormalMove || move is DropMove);
@@ -1526,7 +1537,7 @@ class Crazyhouse extends Position<Crazyhouse> {
15261537
pockets: pockets != null ? pockets.value : this.pockets,
15271538
turn: turn ?? this.turn,
15281539
castles: castles ?? this.castles,
1529-
epSquare: epSquare != null ? epSquare.value : this.epSquare,
1540+
epSquare: epSquare?.value,
15301541
halfmoves: halfmoves ?? this.halfmoves,
15311542
fullmoves: fullmoves ?? this.fullmoves,
15321543
);

lib/src/setup.dart

+5
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ class Pockets {
268268

269269
@override
270270
int get hashCode => value.hashCode;
271+
272+
@override
273+
String toString() {
274+
return _makePockets(this);
275+
}
271276
}
272277

273278
Pockets _parsePockets(String pocketPart) {

test/crazyhouse_test.dart

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'package:dartchess/dartchess.dart';
2+
import 'package:test/test.dart';
3+
import 'db_testing_lib.dart';
4+
5+
void main() {
6+
test('Crazyhouse - issue #23: Crazyhouse en passant bug with DropMove', () {
7+
Position a = Crazyhouse.initial;
8+
a = a.playSan('d4');
9+
a = a.playSan('e5');
10+
a = a.playSan('Nf3');
11+
a = a.playSan('Qg5');
12+
a = a.playSan('Nxg5');
13+
a = a.playSan('Be7');
14+
a = a.playSan('dxe5');
15+
a = a.playSan('Bd8');
16+
17+
a = a.playSan('Nc3');
18+
printBoard(a, printLegalMoves: true);
19+
20+
a = a.playSan('f5'); // creates epSquare at f6 for White
21+
printBoard(a, printLegalMoves: true);
22+
23+
a = a.playSan(
24+
'Q@f7'); // Bug: copyWith() is given null for epSquare, which then copies White's epSquare of f6 forward to Black
25+
final List<Move> legalMoves = printBoard(a, printLegalMoves: true);
26+
const MyExpectations myExpectations = MyExpectations(
27+
legalMoves: 0,
28+
legalDrops: 0,
29+
legalDropZone: DropZone.anywhere,
30+
rolesThatCanDrop: [],
31+
rolesThatCantDrop: [
32+
Role.king,
33+
Role.queen,
34+
Role.rook,
35+
Role.bishop,
36+
Role.knight,
37+
Role.pawn,
38+
],
39+
);
40+
41+
expect(myExpectations.testLegalMoves(legalMoves), '');
42+
43+
expect(a.outcome, Outcome.whiteWins);
44+
45+
// a = a.playSan('gxf6'); // captures the Queen at f7!
46+
// printBoard(a, printLegalMoves: true);
47+
});
48+
}

test/db_testing_lib.dart

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import 'package:dartchess/dartchess.dart';
2+
3+
const verbosePrinting = false;
4+
bool printedNotice = false;
5+
6+
void conditionalPrint(Object? a) {
7+
if (!printedNotice) {
8+
printedNotice = true;
9+
print('=' * 60);
10+
print('${'=\tVERBOSE PRINTING'.padRight(53)}=');
11+
if (verbosePrinting) {
12+
print('${'=\tverbosePrinting is ON'.padRight(53)}=');
13+
print('${'=\t'.padRight(53)}=');
14+
print('${'=\tTo disable, set the "verbosePrinting"'.padRight(53)}=');
15+
print('${'=\tconstant to false'.padRight(53)}=');
16+
} else {
17+
print('${'=\tverbosePrinting is OFF'.padRight(53)}=');
18+
print('${'=\t'.padRight(53)}=');
19+
print(
20+
'${'=\tSet the "verbosePrinting" constant to true to help'.padRight(53)}=');
21+
print('${'=\twith debugging these tests'.padRight(53)}=');
22+
}
23+
print('${'=\t(line 3 of \\test\\db_testing_lib.dart)'.padRight(53)}=');
24+
print('=' * 60);
25+
}
26+
if (verbosePrinting) {
27+
print(a);
28+
}
29+
}
30+
31+
enum DropZone { none, whiteHomeRow, blackHomeRow, anywhere }
32+
33+
const noDrops = <int>[];
34+
const whiteHomeRow = <int>[
35+
0,
36+
1,
37+
2,
38+
3,
39+
4,
40+
5,
41+
6,
42+
7,
43+
];
44+
const blackHomeRow = <int>[
45+
56,
46+
57,
47+
58,
48+
59,
49+
60,
50+
61,
51+
62,
52+
63,
53+
];
54+
55+
class MyExpectations {
56+
final int legalDrops;
57+
final int legalMoves;
58+
final DropZone legalDropZone;
59+
final List<Role> rolesThatCanDrop;
60+
final List<Role> rolesThatCantDrop;
61+
62+
const MyExpectations(
63+
{required this.legalMoves,
64+
required this.legalDrops,
65+
required this.legalDropZone,
66+
required this.rolesThatCanDrop,
67+
required this.rolesThatCantDrop});
68+
69+
String testLegalMoves(List<Move> a) {
70+
if (a.whereType<NormalMove>().length != legalMoves) {
71+
return 'Expected $legalMoves legal moves, got ${a.whereType<NormalMove>().length}';
72+
}
73+
if (a.whereType<DropMove>().length != legalDrops) {
74+
return 'Expected $legalDrops legal drops, got ${a.whereType<DropMove>().length}';
75+
}
76+
for (final move in a) {
77+
if (move is DropMove) {
78+
if (rolesThatCantDrop.contains(move.role)) {
79+
return '${move.role} is listed in rolesThatCantDrop';
80+
}
81+
if (!rolesThatCanDrop.contains(move.role)) {
82+
return '${move.role} is not listed in rolesThatCanDrop';
83+
}
84+
if (legalDropZone == DropZone.anywhere &&
85+
move.role == Role.pawn &&
86+
(whiteHomeRow.contains(move.to) ||
87+
blackHomeRow.contains(move.to))) {
88+
return 'Drop zone is anywhere, but a pawn cannot be dropped in rows 1 or 8';
89+
} else if (legalDropZone == DropZone.whiteHomeRow &&
90+
!whiteHomeRow.contains(move.to)) {
91+
return 'Drop zone is whiteHomeRow, but ${move.to} is not in whiteHomeRow';
92+
} else if (legalDropZone == DropZone.blackHomeRow &&
93+
!blackHomeRow.contains(move.to)) {
94+
return 'Drop zone is blackHomeRow, but ${move.to} is not in whiteHomeRow';
95+
} else if (legalDropZone == DropZone.none &&
96+
!noDrops.contains(move.to)) {
97+
return 'Drop zone is none, but ${move.to} is not in noDrops';
98+
}
99+
}
100+
}
101+
return '';
102+
}
103+
}
104+
105+
List<DropMove> dropTestEachSquare(Position position) {
106+
final legalDrops = <DropMove>[];
107+
final allRoles = <Role>[
108+
Role.pawn,
109+
Role.knight,
110+
Role.bishop,
111+
Role.rook,
112+
Role.queen,
113+
Role.king
114+
];
115+
for (final pieceRole in allRoles) {
116+
for (int a = 0; a < 64; a++) {
117+
if (position.isLegal(DropMove(role: pieceRole, to: a))) {
118+
legalDrops.add(DropMove(role: pieceRole, to: a));
119+
}
120+
}
121+
}
122+
return legalDrops;
123+
}
124+
125+
List<NormalMove> moveTestEachSquare(Position position) {
126+
final legalMoves = <NormalMove>[];
127+
for (int a = 0; a < 64; a++) {
128+
for (int b = 0; b < 64; b++) {
129+
if (position.isLegal(NormalMove(from: a, to: b))) {
130+
legalMoves.add(NormalMove(from: a, to: b));
131+
}
132+
}
133+
}
134+
return legalMoves;
135+
}
136+
137+
List<Move> printBoard(Position a, {bool printLegalMoves = false}) {
138+
final z = StringBuffer();
139+
final y = StringBuffer();
140+
String x = '';
141+
int moves = 0;
142+
int drops = 0;
143+
144+
final legalMoves = <Move>[];
145+
if (printLegalMoves) {
146+
for (final move in moveTestEachSquare(a)) {
147+
z.write('${move.uci}, ');
148+
legalMoves.add(move);
149+
moves++;
150+
}
151+
x = 'Legal Moves ($moves):\n$z\n';
152+
for (final drop in dropTestEachSquare(a)) {
153+
y.write('${drop.uci}, ');
154+
legalMoves.add(drop);
155+
drops++;
156+
}
157+
x += 'Legal Drops ($drops):\n$y';
158+
}
159+
160+
conditionalPrint('${humanReadableBoard(a.board, a.pockets)}$x');
161+
conditionalPrint(
162+
'------------------------------------------------------------');
163+
return legalMoves;
164+
}

0 commit comments

Comments
 (0)