+152
gen.js
+152
gen.js
···
1
+
const tables = {
2
+
"pawn": `#(
3
+
#(000, 000, 000, 000, 000, 000, 000, 000),
4
+
#(-007, 007, -003, -013, 005, -016, 010, -008),
5
+
#(005, -012, -007, 022, -008, -005, -015, -008),
6
+
#(013, 000, -013, 001, 011, -002, -013, 005),
7
+
#(-004, -023, 006, 020, 040, 017, 004, -008),
8
+
#(-009, -015, 011, 015, 032, 022, 005, -022),
9
+
#(003, 003, 010, 019, 016, 019, 007, -005),
10
+
#(000, 000, 000, 000, 000, 000, 000, 000),
11
+
)`,
12
+
13
+
"knight": `#(
14
+
#(-201, -083, -056, -026, -026, -056, -083, -201),
15
+
#(-067, -027, 004, 037, 037, 004, -027, -067),
16
+
#(-009, 022, 058, 053, 053, 058, 022, -009),
17
+
#(-034, 013, 044, 051, 051, 044, 013, -034),
18
+
#(-035, 008, 040, 049, 049, 040, 008, -035),
19
+
#(-061, -017, 006, 012, 012, 006, -017, -061),
20
+
#(-077, -041, -027, -015, -015, -027, -041, -077),
21
+
#(-175, -092, -074, -073, -073, -074, -092, -175),
22
+
)`,
23
+
24
+
"bishop": `#(
25
+
#(-048, 001, -014, -023, -023, -014, 001, -048),
26
+
#(-017, -014, 005, 000, 000, 005, -014, -017),
27
+
#(-016, 006, 001, 011, 011, 001, 006, -016),
28
+
#(-012, 029, 022, 031, 031, 022, 029, -012),
29
+
#(-005, 011, 025, 039, 039, 025, 011, -005),
30
+
#(-007, 021, -005, 017, 017, -005, 021, -007),
31
+
#(-015, 008, 019, 004, 004, 019, 008, -015),
32
+
#(-053, -005, -008, -023, -023, -008, -005, -053),
33
+
)`,
34
+
35
+
"rook": `#(
36
+
#(-017, -019, -001, 009, 009, -001, -019, -017),
37
+
#(-002, 012, 016, 018, 018, 016, 012, -002),
38
+
#(-022, -002, 006, 012, 012, 006, -002, -022),
39
+
#(-027, -015, -004, 003, 003, -004, -015, -027),
40
+
#(-013, -005, -004, -006, -006, -004, -005, -013),
41
+
#(-025, -011, -001, 003, 003, -001, -011, -025),
42
+
#(-021, -013, -008, 006, 006, -008, -013, -021),
43
+
#(-031, -020, -014, -005, -005, -014, -020, -031),
44
+
)`,
45
+
46
+
"queen": `#(
47
+
#(-002, -002, 001, -002, -002, 001, -002, -002),
48
+
#(-005, 006, 010, 008, 008, 010, 006, -005),
49
+
#(-004, 010, 006, 008, 008, 006, 010, -004),
50
+
#(000, 014, 012, 005, 005, 012, 014, 000),
51
+
#(004, 005, 009, 008, 008, 009, 005, 004),
52
+
#(-003, 006, 013, 007, 007, 013, 006, -003),
53
+
#(-003, 005, 008, 012, 012, 008, 005, -003),
54
+
#(003, -005, -005, 004, 004, -005, -005, 003),
55
+
)`,
56
+
57
+
"king": `#(
58
+
#(059, 089, 045, -001, -001, 045, 089, 059),
59
+
#(088, 120, 065, 033, 033, 065, 120, 088),
60
+
#(123, 145, 081, 031, 031, 081, 145, 123),
61
+
#(154, 179, 105, 070, 070, 105, 179, 154),
62
+
#(164, 190, 138, 098, 098, 138, 190, 164),
63
+
#(195, 258, 169, 120, 120, 169, 258, 195),
64
+
#(278, 303, 234, 179, 179, 234, 303, 278),
65
+
#(271, 327, 271, 198, 198, 271, 327, 271),
66
+
)`,
67
+
68
+
"pawn_endgame": `#(
69
+
#(000, 000, 000, 000, 000, 000, 000, 000),
70
+
#(000, -011, 012, 021, 025, 019, 004, 007),
71
+
#(028, 020, 021, 028, 030, 007, 006, 013),
72
+
#(010, 005, 004, -005, -005, -005, 014, 009),
73
+
#(006, -002, -008, -004, -013, -012, -010, -009),
74
+
#(-010, -010, -010, 004, 004, 003, -006, -004),
75
+
#(-010, -006, 010, 000, 014, 007, -005, -019),
76
+
#(000, 000, 000, 000, 000, 000, 000, 000),
77
+
)`,
78
+
79
+
"knight_endgame": `#(
80
+
#(-100, -088, -056, -017, -017, -056, -088, -100),
81
+
#(-069, -050, -051, 012, 012, -051, -050, -069),
82
+
#(-051, -044, -016, 017, 017, -016, -044, -051),
83
+
#(-045, -016, 009, 039, 039, 009, -016, -045),
84
+
#(-035, -002, 013, 028, 028, 013, -002, -035),
85
+
#(-040, -027, -008, 029, 029, -008, -027, -040),
86
+
#(-067, -054, -018, 008, 008, -018, -054, -067),
87
+
#(-096, -065, -049, -021, -021, -049, -065, -096),
88
+
)`,
89
+
90
+
"bishop_endgame": `#(
91
+
#(-046, -042, -037, -024, -024, -037, -042, -046),
92
+
#(-031, -020, -001, 001, 001, -001, -020, -031),
93
+
#(-030, 006, 004, 006, 006, 004, 006, -030),
94
+
#(-017, -001, -014, 015, 015, -014, -001, -017),
95
+
#(-020, -006, 000, 017, 017, 000, -006, -020),
96
+
#(-016, -001, -002, 010, 010, -002, -001, -016),
97
+
#(-037, -013, -017, 001, 001, -017, -013, -037),
98
+
#(-057, -030, -037, -012, -012, -037, -030, -057),
99
+
)`,
100
+
101
+
"rook_endgame": `#(
102
+
#(018, 000, 019, 013, 013, 019, 000, 018),
103
+
#(004, 005, 020, -005, -005, 020, 005, 004),
104
+
#(006, 001, -007, 010, 010, -007, 001, 006),
105
+
#(-005, 008, 007, -006, -006, 007, 008, -005),
106
+
#(-006, 001, -009, 007, 007, -009, 001, -006),
107
+
#(006, -008, -002, -006, -006, -002, -008, 006),
108
+
#(-012, -009, -001, -002, -002, -001, -009, -012),
109
+
#(-009, -013, -010, -009, -009, -010, -013, -009),
110
+
)`,
111
+
112
+
"queen_endgame": `#(
113
+
#(-075, -052, -043, -036, -036, -043, -052, -075),
114
+
#(-050, -027, -024, -008, -008, -024, -027, -050),
115
+
#(-038, -018, -012, 001, 001, -012, -018, -038),
116
+
#(-029, -006, 009, 021, 021, 009, -006, -029),
117
+
#(-023, -003, 013, 024, 024, 013, -003, -023),
118
+
#(-039, -018, -009, 003, 003, -009, -018, -039),
119
+
#(-055, -031, -022, -004, -004, -022, -031, -055),
120
+
#(-069, -057, -047, -026, -026, -047, -057, -069),
121
+
)`,
122
+
123
+
"king_endgame": `#(
124
+
#(011, 059, 073, 078, 078, 073, 059, 011),
125
+
#(047, 121, 116, 131, 131, 116, 121, 047),
126
+
#(092, 172, 184, 191, 191, 184, 172, 092),
127
+
#(096, 166, 199, 199, 199, 199, 166, 096),
128
+
#(103, 156, 172, 172, 172, 172, 156, 103),
129
+
#(088, 130, 169, 175, 175, 169, 130, 088),
130
+
#(053, 100, 133, 135, 135, 133, 100, 053),
131
+
#(001, 045, 085, 076, 076, 085, 045, 001),
132
+
)`,
133
+
}
134
+
135
+
let out = "";
136
+
137
+
for (let [name, table] of Object.entries(tables)) {
138
+
if (!table.startsWith("#(\n")) throw undefined;
139
+
if (!table.endsWith("\n)")) throw undefined;
140
+
141
+
table = table.slice(3, table.length - 2);
142
+
143
+
const rows = table.split("\n").reverse();
144
+
145
+
const reverseTable = `#(
146
+
${rows.join("\n")}
147
+
)`;
148
+
149
+
out += `const ${name} = ${reverseTable}\n\n`;
150
+
}
151
+
152
+
console.log(out);
-1
gleam.toml
-1
gleam.toml
-1
manifest.toml
-1
manifest.toml
+8
-6
src/starfish/internal/board.gleam
+8
-6
src/starfish/internal/board.gleam
···
34
34
King
35
35
}
36
36
37
-
pub const pawn_value = 100
37
+
// Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/
38
38
39
-
pub const knight_value = 300
39
+
pub const pawn_value = 124
40
40
41
-
pub const bishop_value = 300
41
+
pub const knight_value = 781
42
42
43
-
pub const rook_value = 500
43
+
pub const bishop_value = 825
44
44
45
-
pub const queen_value = 900
45
+
pub const rook_value = 1276
46
+
47
+
pub const queen_value = 2538
46
48
47
-
pub const king_value = 1000
49
+
pub const king_value = 10_000
48
50
49
51
pub fn piece_value(piece: Piece) -> Int {
50
52
case piece {
-27
src/starfish/internal/evaluate.gleam
-27
src/starfish/internal/evaluate.gleam
···
1
-
import gleam/list
2
-
import starfish/internal/board
3
-
import starfish/internal/game
4
-
5
-
/// Statically evaluates a position. Does not take into account checkmate or
6
-
/// stalemate, those must be accounted for beforehand.
7
-
pub fn evaluate(game: game.Game, legal_moves: List(move)) -> Int {
8
-
let black_position_score = position_score(game.black_pieces)
9
-
let white_position_score = position_score(game.white_pieces)
10
-
11
-
let position_score = case game.to_move {
12
-
board.Black -> black_position_score - white_position_score
13
-
board.White -> white_position_score - black_position_score
14
-
}
15
-
16
-
position_score + list.length(legal_moves)
17
-
}
18
-
19
-
fn position_score(pieces: game.PieceInfo) -> Int {
20
-
let game.PieceInfo(
21
-
king_position: _,
22
-
non_pawn_material:,
23
-
pawn_material:,
24
-
piece_square_score:,
25
-
) = pieces
26
-
non_pawn_material + pawn_material + piece_square_score
27
-
}
+109
-39
src/starfish/internal/game.gleam
+109
-39
src/starfish/internal/game.gleam
···
1
1
import gleam/bool
2
2
import gleam/dict
3
3
import gleam/int
4
+
import gleam/list
4
5
import gleam/option.{type Option, None, Some}
5
6
import gleam/result
6
7
import gleam/string
···
41
42
king_position: Int,
42
43
non_pawn_material: Int,
43
44
pawn_material: Int,
44
-
piece_square_score: Int,
45
+
piece_square_score_midgame: Int,
46
+
piece_square_score_endgame: Int,
45
47
)
46
48
}
47
49
···
68
70
69
71
let attack_information = attack.calculate(board, white_king_position, to_move)
70
72
71
-
let phase = phase(non_pawn_material, non_pawn_material)
72
-
let #(white_piece_scores, black_piece_scores) =
73
-
piece_square_scores(board, phase)
73
+
let #(
74
+
white_piece_scores_mid,
75
+
white_piece_scores_end,
76
+
black_piece_scores_mid,
77
+
black_piece_scores_end,
78
+
) = piece_square_scores(board)
74
79
75
80
Game(
76
81
board:,
···
86
91
king_position: white_king_position,
87
92
non_pawn_material:,
88
93
pawn_material:,
89
-
piece_square_score: white_piece_scores,
94
+
piece_square_score_midgame: white_piece_scores_mid,
95
+
piece_square_score_endgame: white_piece_scores_end,
90
96
),
91
97
black_pieces: PieceInfo(
92
98
king_position: black_king_position,
93
99
non_pawn_material:,
94
100
pawn_material:,
95
-
piece_square_score: black_piece_scores,
101
+
piece_square_score_midgame: black_piece_scores_mid,
102
+
piece_square_score_endgame: black_piece_scores_end,
96
103
),
97
104
)
98
105
}
99
106
100
-
fn piece_square_scores(board: board.Board, phase: Int) -> #(Int, Int) {
101
-
use #(white_score, black_score), position, #(piece, colour) <- dict.fold(
107
+
fn piece_square_scores(board: board.Board) -> #(Int, Int, Int, Int) {
108
+
use #(white_mid, white_end, black_mid, black_end), position, #(piece, colour) <- dict.fold(
102
109
board,
103
-
#(0, 0),
110
+
#(0, 0, 0, 0),
104
111
)
105
-
let score = piece_table.piece_score(piece, colour, position, phase)
112
+
let mid_score = piece_table.piece_score_midgame(piece, colour, position)
113
+
let end_score = piece_table.piece_score_endgame(piece, colour, position)
106
114
case colour {
107
-
Black -> #(white_score, black_score + score)
108
-
White -> #(white_score + score, black_score)
115
+
Black -> #(
116
+
white_mid,
117
+
white_end,
118
+
black_mid + mid_score,
119
+
black_end + end_score,
120
+
)
121
+
White -> #(
122
+
white_mid + mid_score,
123
+
white_end + end_score,
124
+
black_mid,
125
+
black_end,
126
+
)
109
127
}
110
128
}
111
129
···
168
186
}
169
187
let attack_information = attack.calculate(board, king_position, to_move)
170
188
171
-
let phase = phase(white_non_pawn_material, black_non_pawn_material)
172
-
let #(white_piece_scores, black_piece_scores) =
173
-
piece_square_scores(board, phase)
189
+
let #(
190
+
white_piece_scores_mid,
191
+
white_piece_scores_end,
192
+
black_piece_scores_mid,
193
+
black_piece_scores_end,
194
+
) = piece_square_scores(board)
174
195
175
196
Game(
176
197
board:,
···
186
207
king_position: white_king_position,
187
208
non_pawn_material: white_non_pawn_material,
188
209
pawn_material: white_pawn_material,
189
-
piece_square_score: white_piece_scores,
210
+
piece_square_score_midgame: white_piece_scores_mid,
211
+
piece_square_score_endgame: white_piece_scores_end,
190
212
),
191
213
black_pieces: PieceInfo(
192
214
king_position: black_king_position,
193
215
non_pawn_material: black_non_pawn_material,
194
216
pawn_material: black_pawn_material,
195
-
piece_square_score: black_piece_scores,
217
+
piece_square_score_midgame: black_piece_scores_mid,
218
+
piece_square_score_endgame: black_piece_scores_end,
196
219
),
197
220
)
198
221
}
···
352
375
353
376
let attack_information = attack.calculate(board, king_position, to_move)
354
377
355
-
let phase = phase(white_non_pawn_material, black_non_pawn_material)
356
-
let #(white_piece_scores, black_piece_scores) =
357
-
piece_square_scores(board, phase)
378
+
let #(
379
+
white_piece_scores_mid,
380
+
white_piece_scores_end,
381
+
black_piece_scores_mid,
382
+
black_piece_scores_end,
383
+
) = piece_square_scores(board)
358
384
359
385
Ok(Game(
360
386
board:,
···
370
396
king_position: white_king_position,
371
397
non_pawn_material: white_non_pawn_material,
372
398
pawn_material: white_pawn_material,
373
-
piece_square_score: white_piece_scores,
399
+
piece_square_score_midgame: white_piece_scores_mid,
400
+
piece_square_score_endgame: white_piece_scores_end,
374
401
),
375
402
black_pieces: PieceInfo(
376
403
king_position: black_king_position,
377
404
non_pawn_material: black_non_pawn_material,
378
405
pawn_material: black_pawn_material,
379
-
piece_square_score: black_piece_scores,
406
+
piece_square_score_midgame: black_piece_scores_mid,
407
+
piece_square_score_endgame: black_piece_scores_end,
380
408
),
381
409
))
382
410
}
···
466
494
pub fn is_insufficient_material(game: Game) -> Bool {
467
495
game.black_pieces.pawn_material == 0
468
496
&& game.white_pieces.pawn_material == 0
469
-
&& {
470
-
game.black_pieces.non_pawn_material == board.bishop_value
471
-
|| game.black_pieces.non_pawn_material == board.knight_value
472
-
|| game.black_pieces.non_pawn_material == 0
473
-
}
474
-
&& {
475
-
game.white_pieces.non_pawn_material == board.bishop_value
476
-
|| game.white_pieces.non_pawn_material == board.knight_value
477
-
|| game.white_pieces.non_pawn_material == 0
478
-
}
497
+
&& game.black_pieces.non_pawn_material <= board.bishop_value
498
+
&& game.white_pieces.non_pawn_material <= board.bishop_value
479
499
}
480
500
481
501
pub fn is_threefold_repetition(game: Game) -> Bool {
···
504
524
505
525
const phase_multiplier = 128
506
526
507
-
/// About queen + rook, so one major piece per side
508
-
const endgame_material = 1400
527
+
// Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/
528
+
529
+
/// About queen + rook, so one major piece per side. If total material is less
530
+
/// than this, then we are completely in the endgame.
531
+
const endgame_material = 3915
509
532
510
-
/// Below this material limit, the endgame weight is zero. this is about enough
511
-
/// for three minor pieces to be captured.
512
-
const middlegame_material = 3000
533
+
/// Above this material limit, the endgame weight is zero. this is about enough
534
+
/// for three minor pieces to be captured .
535
+
const middlegame_material = 15_258
513
536
514
-
pub fn phase(white_material: Int, black_material: Int) -> Int {
515
-
let non_pawn_material = white_material + black_material
537
+
pub fn phase(game: Game) -> Int {
538
+
let non_pawn_material =
539
+
game.white_pieces.non_pawn_material + game.black_pieces.non_pawn_material
516
540
517
541
let clamped_material = case non_pawn_material > middlegame_material {
518
542
True -> middlegame_material
···
527
551
* phase_multiplier
528
552
/ { middlegame_material - endgame_material }
529
553
}
554
+
555
+
pub fn interpolate_phase(
556
+
middlegame_value: Int,
557
+
endgame_value: Int,
558
+
phase: Int,
559
+
) -> Int {
560
+
{ middlegame_value * { phase_multiplier - phase } + endgame_value * phase }
561
+
/ phase_multiplier
562
+
}
563
+
564
+
/// Statically evaluates a position. Does not take into account checkmate or
565
+
/// stalemate, those must be accounted for beforehand.
566
+
pub fn evaluation(game: Game, legal_moves: List(move)) -> Int {
567
+
let phase = phase(game)
568
+
569
+
let black_position_score = position_score(game.black_pieces, phase)
570
+
let white_position_score = position_score(game.white_pieces, phase)
571
+
572
+
let position_score = case game.to_move {
573
+
board.Black -> black_position_score - white_position_score
574
+
board.White -> white_position_score - black_position_score
575
+
}
576
+
577
+
position_score + list.length(legal_moves)
578
+
}
579
+
580
+
fn position_score(pieces: PieceInfo, phase: Int) -> Int {
581
+
let PieceInfo(
582
+
king_position: _,
583
+
non_pawn_material:,
584
+
pawn_material:,
585
+
piece_square_score_midgame:,
586
+
piece_square_score_endgame:,
587
+
) = pieces
588
+
589
+
// Pawns become much more valuable in the endgame, about 1.5x
590
+
let pawn_material_endgame = pawn_material * 15 / 10
591
+
592
+
non_pawn_material
593
+
+ interpolate_phase(pawn_material, pawn_material_endgame, phase)
594
+
+ interpolate_phase(
595
+
piece_square_score_midgame,
596
+
piece_square_score_endgame,
597
+
phase,
598
+
)
599
+
}
-3
src/starfish/internal/move/attack.gleam
-3
src/starfish/internal/move/attack.gleam
···
345
345
board,
346
346
position,
347
347
king_position,
348
-
False,
349
348
direction,
350
349
[position],
351
350
)
···
361
360
board: Board,
362
361
position: Int,
363
362
king_position: Int,
364
-
found_king: Bool,
365
363
direction: Direction,
366
364
line: List(Int),
367
365
) -> List(Int) {
···
374
372
board,
375
373
new_position,
376
374
king_position,
377
-
found_king,
378
375
direction,
379
376
[new_position, ..line],
380
377
)
+285
-139
src/starfish/internal/move.gleam
+285
-139
src/starfish/internal/move.gleam
···
13
13
14
14
pub type Move {
15
15
Castle(from: Int, to: Int)
16
-
Move(from: Int, to: Int)
17
-
Capture(from: Int, to: Int)
16
+
Move(from: Int, to: Int, piece: board.Piece)
17
+
Capture(from: Int, to: Int, piece: board.Piece, captured_piece: board.Piece)
18
18
EnPassant(from: Int, to: Int)
19
-
Promotion(from: Int, to: Int, piece: board.Piece)
19
+
Promotion(
20
+
from: Int,
21
+
to: Int,
22
+
piece: board.Piece,
23
+
captured_piece: Option(board.Piece),
24
+
)
25
+
}
26
+
27
+
pub fn moving_piece(move: Move) -> board.Piece {
28
+
case move {
29
+
Capture(piece:, ..) | Move(piece:, ..) -> piece
30
+
Castle(..) -> board.King
31
+
EnPassant(..) | Promotion(..) -> board.Pawn
32
+
}
20
33
}
21
34
22
35
pub fn legal(game: Game) -> List(Move) {
···
83
96
) -> List(Move) {
84
97
case piece {
85
98
board.Bishop ->
86
-
sliding_moves(game, position, moves, direction.bishop_directions)
99
+
sliding_moves(game, piece, position, moves, direction.bishop_directions)
87
100
board.Rook ->
88
-
sliding_moves(game, position, moves, direction.rook_directions)
101
+
sliding_moves(game, piece, position, moves, direction.rook_directions)
89
102
board.Queen ->
90
-
sliding_moves(game, position, moves, direction.queen_directions)
103
+
sliding_moves(game, piece, position, moves, direction.queen_directions)
91
104
board.King -> king_moves(game, position, moves, direction.queen_directions)
92
105
board.Knight ->
93
106
knight_moves(game, position, moves, direction.knight_directions)
···
120
133
{
121
134
False -> moves
122
135
True if is_promotion ->
123
-
add_promotions(position, forward_one, moves, board.pawn_promotions)
124
-
True -> [Move(from: position, to: forward_one), ..moves]
136
+
add_promotions(
137
+
position,
138
+
forward_one,
139
+
None,
140
+
moves,
141
+
board.pawn_promotions,
142
+
)
143
+
True -> [Move(board.Pawn, from: position, to: forward_one), ..moves]
125
144
}
126
145
127
146
let can_double_move = case game.to_move, position / 8 {
···
136
155
board.Empty ->
137
156
case can_move(position, forward_two, game.attack_information) {
138
157
False -> moves
139
-
True -> [Move(from: position, to: forward_two), ..moves]
158
+
True -> [Move(board.Pawn, from: position, to: forward_two), ..moves]
140
159
}
141
160
board.Occupied(_, _) | board.OffBoard -> moves
142
161
}
···
146
165
147
166
let new_position = direction.in_direction(position, left)
148
167
let moves = case board.get(game.board, new_position) {
149
-
board.Occupied(colour:, ..) if colour != game.to_move ->
168
+
board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
150
169
case can_move(position, new_position, game.attack_information) {
151
170
False -> moves
152
171
True if is_promotion ->
153
-
add_promotions(position, new_position, moves, board.pawn_promotions)
154
-
True -> [Capture(from: position, to: new_position), ..moves]
172
+
add_promotions(
173
+
position,
174
+
new_position,
175
+
Some(captured_piece),
176
+
moves,
177
+
board.pawn_promotions,
178
+
)
179
+
True -> [
180
+
Capture(board.Pawn, from: position, to: new_position, captured_piece:),
181
+
..moves
182
+
]
155
183
}
156
184
board.Empty if game.en_passant_square == Some(new_position) ->
157
185
case en_passant_is_valid(game, position, new_position) {
···
163
191
164
192
let new_position = direction.in_direction(position, right)
165
193
case board.get(game.board, new_position) {
166
-
board.Occupied(colour:, ..) if colour != game.to_move ->
194
+
board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
167
195
case can_move(position, new_position, game.attack_information) {
168
196
False -> moves
169
197
True if is_promotion ->
170
-
add_promotions(position, new_position, moves, board.pawn_promotions)
171
-
True -> [Capture(from: position, to: new_position), ..moves]
198
+
add_promotions(
199
+
position,
200
+
new_position,
201
+
Some(captured_piece),
202
+
moves,
203
+
board.pawn_promotions,
204
+
)
205
+
True -> [
206
+
Capture(board.Pawn, from: position, to: new_position, captured_piece:),
207
+
..moves
208
+
]
172
209
}
173
210
board.Empty if game.en_passant_square == Some(new_position) ->
174
211
case en_passant_is_valid(game, position, new_position) {
···
288
325
fn add_promotions(
289
326
from: Int,
290
327
to: Int,
328
+
captured_piece: Option(board.Piece),
291
329
moves: List(Move),
292
330
pieces: List(board.Piece),
293
331
) -> List(Move) {
294
332
case pieces {
295
333
[] -> moves
296
334
[piece, ..pieces] ->
297
-
add_promotions(from, to, [Promotion(from:, to:, piece:), ..moves], pieces)
335
+
add_promotions(
336
+
from,
337
+
to,
338
+
captured_piece,
339
+
[Promotion(from:, to:, piece:, captured_piece:), ..moves],
340
+
pieces,
341
+
)
298
342
}
299
343
}
300
344
···
312
356
board.Empty ->
313
357
case can_move(position, new_position, game.attack_information) {
314
358
False -> moves
315
-
True -> [Move(from: position, to: new_position), ..moves]
359
+
True -> [
360
+
Move(board.Knight, from: position, to: new_position),
361
+
..moves
362
+
]
316
363
}
317
-
board.Occupied(colour:, ..) if colour != game.to_move ->
364
+
board.Occupied(colour:, piece: captured_piece)
365
+
if colour != game.to_move
366
+
->
318
367
case can_move(position, new_position, game.attack_information) {
319
368
False -> moves
320
-
True -> [Capture(from: position, to: new_position), ..moves]
369
+
True -> [
370
+
Capture(
371
+
board.Knight,
372
+
from: position,
373
+
to: new_position,
374
+
captured_piece:,
375
+
),
376
+
..moves
377
+
]
321
378
}
322
379
board.Occupied(_, _) | board.OffBoard -> moves
323
380
}
···
388
445
board.Empty ->
389
446
case king_can_move(new_position, game.attack_information) {
390
447
False -> moves
391
-
True -> [Move(from: position, to: new_position), ..moves]
448
+
True -> [
449
+
Move(board.King, from: position, to: new_position),
450
+
..moves
451
+
]
392
452
}
393
-
board.Occupied(colour:, ..) if colour != game.to_move ->
453
+
board.Occupied(colour:, piece: captured_piece)
454
+
if colour != game.to_move
455
+
->
394
456
case king_can_move(new_position, game.attack_information) {
395
457
False -> moves
396
-
True -> [Capture(from: position, to: new_position), ..moves]
458
+
True -> [
459
+
Capture(
460
+
board.King,
461
+
from: position,
462
+
to: new_position,
463
+
captured_piece:,
464
+
),
465
+
..moves
466
+
]
397
467
}
398
468
board.Occupied(_, _) | board.OffBoard -> moves
399
469
}
···
405
475
406
476
fn sliding_moves(
407
477
game: Game,
478
+
piece: board.Piece,
408
479
position: Int,
409
480
moves: List(Move),
410
481
directions: List(Direction),
···
414
485
[direction, ..directions] ->
415
486
sliding_moves(
416
487
game,
488
+
piece,
417
489
position,
418
-
sliding_moves_in_direction(game, position, position, direction, moves),
490
+
sliding_moves_in_direction(
491
+
game,
492
+
piece,
493
+
position,
494
+
position,
495
+
direction,
496
+
moves,
497
+
),
419
498
directions,
420
499
)
421
500
}
···
423
502
424
503
fn sliding_moves_in_direction(
425
504
game: Game,
505
+
piece: board.Piece,
426
506
start_position: Int,
427
507
position: Int,
428
508
direction: Direction,
···
433
513
board.Empty ->
434
514
sliding_moves_in_direction(
435
515
game,
516
+
piece,
436
517
start_position,
437
518
new_position,
438
519
direction,
439
520
case can_move(start_position, new_position, game.attack_information) {
440
521
False -> moves
441
-
True -> [Move(from: start_position, to: new_position), ..moves]
522
+
True -> [Move(piece, from: start_position, to: new_position), ..moves]
442
523
},
443
524
)
444
-
board.Occupied(colour:, ..) if colour != game.to_move ->
525
+
board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
445
526
case can_move(start_position, new_position, game.attack_information) {
446
527
False -> moves
447
-
True -> [Capture(from: start_position, to: new_position), ..moves]
528
+
True -> [
529
+
Capture(
530
+
piece,
531
+
from: start_position,
532
+
to: new_position,
533
+
captured_piece:,
534
+
),
535
+
..moves
536
+
]
448
537
}
449
538
board.Occupied(_, _) | board.OffBoard -> moves
450
539
}
···
452
541
453
542
pub fn apply(game: Game, move: Move) -> game.Game {
454
543
case move {
455
-
Capture(from:, to:) -> do_apply(game, from, to, False, None, True)
544
+
Capture(from:, to:, piece:, captured_piece:) ->
545
+
do_apply(game, piece, from, to, False, None, Some(captured_piece))
456
546
Castle(from:, to:) -> apply_castle(game, from, to, to % 8 == 2)
457
-
EnPassant(from:, to:) -> do_apply(game, from, to, True, None, True)
458
-
Move(from:, to:) -> do_apply(game, from, to, False, None, False)
459
-
Promotion(from:, to:, piece:) ->
460
-
do_apply(game, from, to, False, Some(piece), False)
547
+
EnPassant(from:, to:) ->
548
+
do_apply(game, board.Pawn, from, to, True, None, None)
549
+
Move(from:, to:, piece:) ->
550
+
do_apply(game, piece, from, to, False, None, None)
551
+
Promotion(from:, to:, piece:, captured_piece:) ->
552
+
do_apply(game, board.Pawn, from, to, False, Some(piece), captured_piece)
461
553
}
462
554
}
463
555
···
476
568
king_position: white_king_position,
477
569
non_pawn_material: white_non_pawn_material,
478
570
pawn_material: white_pawn_material,
479
-
piece_square_score: white_piece_square_score,
571
+
piece_square_score_midgame: white_piece_square_score_midgame,
572
+
piece_square_score_endgame: white_piece_square_score_endgame,
480
573
),
481
574
black_pieces: game.PieceInfo(
482
575
king_position: black_king_position,
483
576
non_pawn_material: black_non_pawn_material,
484
577
pawn_material: black_pawn_material,
485
-
piece_square_score: black_piece_square_score,
578
+
piece_square_score_midgame: black_piece_square_score_midgame,
579
+
piece_square_score_endgame: black_piece_square_score_endgame,
486
580
),
487
581
) = game
488
582
···
517
611
|> hash.toggle_piece(rook_from, board.Rook, to_move)
518
612
|> hash.toggle_piece(rook_to, board.Rook, to_move)
519
613
520
-
let phase = game.phase(white_non_pawn_material, black_non_pawn_material)
521
-
522
-
let #(white_piece_square_score, black_piece_square_score) = case to_move {
614
+
let #(
615
+
white_piece_square_score_midgame,
616
+
white_piece_square_score_endgame,
617
+
black_piece_square_score_midgame,
618
+
black_piece_square_score_endgame,
619
+
) = case to_move {
523
620
board.Black -> #(
524
-
white_piece_square_score,
525
-
black_piece_square_score
526
-
- piece_table.piece_score(board.King, to_move, from, phase)
527
-
- piece_table.piece_score(board.Rook, to_move, rook_from, phase)
528
-
+ piece_table.piece_score(board.King, to_move, to, phase)
529
-
+ piece_table.piece_score(board.Rook, to_move, rook_to, phase),
621
+
white_piece_square_score_midgame,
622
+
white_piece_square_score_endgame,
623
+
black_piece_square_score_midgame
624
+
- piece_table.piece_score_midgame(board.King, to_move, from)
625
+
- piece_table.piece_score_midgame(board.Rook, to_move, rook_from)
626
+
+ piece_table.piece_score_midgame(board.King, to_move, to)
627
+
+ piece_table.piece_score_midgame(board.Rook, to_move, rook_to),
628
+
black_piece_square_score_endgame
629
+
- piece_table.piece_score_endgame(board.King, to_move, from)
630
+
- piece_table.piece_score_endgame(board.Rook, to_move, rook_from)
631
+
+ piece_table.piece_score_endgame(board.King, to_move, to)
632
+
+ piece_table.piece_score_endgame(board.Rook, to_move, rook_to),
530
633
)
531
634
board.White -> #(
532
-
white_piece_square_score
533
-
- piece_table.piece_score(board.King, to_move, from, phase)
534
-
- piece_table.piece_score(board.Rook, to_move, rook_from, phase)
535
-
+ piece_table.piece_score(board.King, to_move, to, phase)
536
-
+ piece_table.piece_score(board.Rook, to_move, rook_to, phase),
537
-
black_piece_square_score,
635
+
white_piece_square_score_midgame
636
+
- piece_table.piece_score_midgame(board.King, to_move, from)
637
+
- piece_table.piece_score_midgame(board.Rook, to_move, rook_from)
638
+
+ piece_table.piece_score_midgame(board.King, to_move, to)
639
+
+ piece_table.piece_score_midgame(board.Rook, to_move, rook_to),
640
+
white_piece_square_score_endgame
641
+
- piece_table.piece_score_endgame(board.King, to_move, from)
642
+
- piece_table.piece_score_endgame(board.Rook, to_move, rook_from)
643
+
+ piece_table.piece_score_endgame(board.King, to_move, to)
644
+
+ piece_table.piece_score_endgame(board.Rook, to_move, rook_to),
645
+
black_piece_square_score_midgame,
646
+
black_piece_square_score_endgame,
538
647
)
539
648
}
540
649
···
584
693
king_position: white_king_position,
585
694
non_pawn_material: white_non_pawn_material,
586
695
pawn_material: white_pawn_material,
587
-
piece_square_score: white_piece_square_score,
696
+
piece_square_score_midgame: white_piece_square_score_midgame,
697
+
piece_square_score_endgame: white_piece_square_score_endgame,
588
698
),
589
699
black_pieces: game.PieceInfo(
590
700
king_position: black_king_position,
591
701
non_pawn_material: black_non_pawn_material,
592
702
pawn_material: black_pawn_material,
593
-
piece_square_score: black_piece_square_score,
703
+
piece_square_score_midgame: black_piece_square_score_midgame,
704
+
piece_square_score_endgame: black_piece_square_score_endgame,
594
705
),
595
706
)
596
707
}
597
708
598
709
fn do_apply(
599
710
game: Game,
711
+
piece: board.Piece,
600
712
from: Int,
601
713
to: Int,
602
714
en_passant: Bool,
603
715
promotion: Option(board.Piece),
604
-
capture: Bool,
716
+
captured_piece: Option(board.Piece),
605
717
) -> Game {
606
718
let Game(
607
719
board:,
···
622
734
king_position: our_king_position,
623
735
non_pawn_material: our_non_pawn_material,
624
736
pawn_material: our_pawn_material,
625
-
piece_square_score: our_piece_square_score,
737
+
piece_square_score_midgame: our_piece_square_score_midgame,
738
+
piece_square_score_endgame: our_piece_square_score_endgame,
626
739
),
627
740
game.PieceInfo(
628
741
king_position: opposing_king_position,
629
742
non_pawn_material: opposing_non_pawn_material,
630
743
pawn_material: opposing_pawn_material,
631
-
piece_square_score: opposing_piece_square_score,
744
+
piece_square_score_midgame: opposing_piece_square_score_midgame,
745
+
piece_square_score_endgame: opposing_piece_square_score_endgame,
632
746
),
633
747
) = case to_move {
634
748
board.Black -> #(black_pieces, white_pieces)
635
749
board.White -> #(white_pieces, black_pieces)
636
750
}
637
751
638
-
let assert board.Occupied(piece:, colour:) = board.get(board, from)
639
-
as "Tried to apply move from invalid position"
752
+
let our_colour = to_move
753
+
let enemy_colour = case to_move {
754
+
board.Black -> board.White
755
+
board.White -> board.Black
756
+
}
640
757
641
758
let castling =
642
759
castling
643
760
|> remove_castling(from)
644
761
|> remove_castling(to)
645
762
646
-
let one_way_move = capture || piece == board.Pawn
763
+
let one_way_move = captured_piece != None || piece == board.Pawn
647
764
648
765
let zobrist_hash =
649
766
previous_hash
650
767
|> hash.toggle_to_move
651
-
|> hash.toggle_piece(from, piece, colour)
768
+
|> hash.toggle_piece(from, piece, our_colour)
652
769
653
-
let phase =
654
-
game.phase(white_pieces.non_pawn_material, black_pieces.non_pawn_material)
655
-
656
-
let our_piece_square_score =
657
-
our_piece_square_score - piece_table.piece_score(piece, colour, from, phase)
770
+
let our_piece_square_score_midgame =
771
+
our_piece_square_score_midgame
772
+
- piece_table.piece_score_midgame(piece, our_colour, from)
773
+
let our_piece_square_score_endgame =
774
+
our_piece_square_score_endgame
775
+
- piece_table.piece_score_endgame(piece, our_colour, from)
658
776
659
777
let #(piece, our_pawn_material, our_non_pawn_material) = case promotion {
660
778
None -> #(piece, our_pawn_material, our_non_pawn_material)
···
665
783
)
666
784
}
667
785
668
-
let our_piece_square_score =
669
-
our_piece_square_score + piece_table.piece_score(piece, colour, to, phase)
786
+
let our_piece_square_score_midgame =
787
+
our_piece_square_score_midgame
788
+
+ piece_table.piece_score_midgame(piece, our_colour, to)
789
+
let our_piece_square_score_endgame =
790
+
our_piece_square_score_endgame
791
+
+ piece_table.piece_score_endgame(piece, our_colour, to)
670
792
671
-
let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, colour)
793
+
let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, our_colour)
672
794
673
795
let #(
674
796
zobrist_hash,
675
797
opposing_pawn_material,
676
798
opposing_non_pawn_material,
677
-
opposing_piece_square_score,
678
-
) = case board.get(board, to) {
679
-
board.Occupied(piece: board.Pawn, colour:) -> #(
680
-
hash.toggle_piece(zobrist_hash, to, board.Pawn, colour),
799
+
opposing_piece_square_score_midgame,
800
+
opposing_piece_square_score_endgame,
801
+
) = case captured_piece {
802
+
Some(board.Pawn) -> #(
803
+
hash.toggle_piece(zobrist_hash, to, board.Pawn, enemy_colour),
681
804
opposing_pawn_material - board.pawn_value,
682
805
opposing_non_pawn_material,
683
-
opposing_piece_square_score
684
-
- piece_table.piece_score(board.Pawn, colour, to, phase),
806
+
opposing_piece_square_score_midgame
807
+
- piece_table.piece_score_midgame(board.Pawn, enemy_colour, to),
808
+
opposing_piece_square_score_endgame
809
+
- piece_table.piece_score_endgame(board.Pawn, enemy_colour, to),
685
810
)
686
-
board.Occupied(piece:, colour:) -> #(
687
-
hash.toggle_piece(zobrist_hash, to, piece, colour),
811
+
Some(piece) -> #(
812
+
hash.toggle_piece(zobrist_hash, to, piece, enemy_colour),
688
813
opposing_pawn_material,
689
814
opposing_non_pawn_material - board.piece_value(piece),
690
-
opposing_piece_square_score
691
-
- piece_table.piece_score(piece, colour, to, phase),
815
+
opposing_piece_square_score_midgame
816
+
- piece_table.piece_score_midgame(piece, enemy_colour, to),
817
+
opposing_piece_square_score_endgame
818
+
- piece_table.piece_score_endgame(piece, enemy_colour, to),
692
819
)
693
-
board.Empty | board.OffBoard -> #(
820
+
None -> #(
694
821
zobrist_hash,
695
822
opposing_pawn_material,
696
823
opposing_non_pawn_material,
697
-
opposing_piece_square_score,
824
+
opposing_piece_square_score_midgame,
825
+
opposing_piece_square_score_endgame,
698
826
)
699
827
}
700
828
701
829
let board =
702
830
board
703
831
|> dict.delete(from)
704
-
|> dict.insert(to, #(piece, colour))
832
+
|> dict.insert(to, #(piece, our_colour))
705
833
706
-
let #(board, zobrist_hash) = case en_passant, en_passant_square, colour {
834
+
let #(
835
+
board,
836
+
zobrist_hash,
837
+
opposing_pawn_material,
838
+
opposing_piece_square_score_midgame,
839
+
opposing_piece_square_score_endgame,
840
+
) = case en_passant, en_passant_square, our_colour {
707
841
True, Some(square), board.White -> {
708
842
let ep_square = square - 8
709
843
#(
710
844
dict.delete(board, ep_square),
711
845
hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.Black),
846
+
opposing_pawn_material - board.pawn_value,
847
+
opposing_piece_square_score_midgame
848
+
- piece_table.piece_score_midgame(board.Pawn, board.Black, ep_square),
849
+
opposing_piece_square_score_endgame
850
+
- piece_table.piece_score_endgame(board.Pawn, board.Black, ep_square),
712
851
)
713
852
}
714
853
True, Some(square), board.Black -> {
···
716
855
#(
717
856
dict.delete(board, ep_square),
718
857
hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.White),
858
+
opposing_pawn_material - board.pawn_value,
859
+
opposing_piece_square_score_midgame
860
+
- piece_table.piece_score_midgame(board.Pawn, board.White, ep_square),
861
+
opposing_piece_square_score_endgame
862
+
- piece_table.piece_score_endgame(board.Pawn, board.White, ep_square),
719
863
)
720
864
}
721
-
_, _, _ -> #(board, zobrist_hash)
865
+
_, _, _ -> #(
866
+
board,
867
+
zobrist_hash,
868
+
opposing_pawn_material,
869
+
opposing_piece_square_score_midgame,
870
+
opposing_piece_square_score_endgame,
871
+
)
722
872
}
723
873
724
874
let en_passant_square = case piece, to - from {
···
742
892
king_position: our_king_position,
743
893
non_pawn_material: our_non_pawn_material,
744
894
pawn_material: our_pawn_material,
745
-
piece_square_score: our_piece_square_score,
895
+
piece_square_score_midgame: our_piece_square_score_midgame,
896
+
piece_square_score_endgame: our_piece_square_score_endgame,
746
897
)
747
898
748
899
let opposing_pieces =
···
750
901
king_position: opposing_king_position,
751
902
non_pawn_material: opposing_non_pawn_material,
752
903
pawn_material: opposing_pawn_material,
753
-
piece_square_score: opposing_piece_square_score,
904
+
piece_square_score_midgame: opposing_piece_square_score_midgame,
905
+
piece_square_score_endgame: opposing_piece_square_score_endgame,
754
906
)
755
907
756
908
let #(white_pieces, black_pieces) = case to_move {
···
758
910
board.Black -> #(opposing_pieces, our_pieces)
759
911
}
760
912
761
-
let to_move = case to_move {
762
-
board.Black -> board.White
763
-
board.White -> board.Black
764
-
}
913
+
let to_move = enemy_colour
765
914
766
915
let #(half_moves, previous_positions) = case one_way_move {
767
916
True -> #(0, [])
···
823
972
}
824
973
825
974
pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String {
826
-
let assert board.Occupied(piece:, colour: _) =
827
-
board.get(game.board, move.from)
828
-
as "Legal moves should only move valid pieces"
975
+
let piece = moving_piece(move)
829
976
830
977
case move {
831
978
Castle(from: _, to:) -> {
···
835
982
True -> "O-O-O"
836
983
}
837
984
}
838
-
Capture(from:, to:) if piece == board.Pawn ->
985
+
Capture(from:, to:, ..) if piece == board.Pawn ->
839
986
pawn_move_to_san(from, to, True, None)
840
987
EnPassant(from:, to:) -> pawn_move_to_san(from, to, True, None)
841
-
Promotion(from:, to:, piece:) -> {
842
-
let is_capture = case board.get(game.board, move.to) {
843
-
board.Occupied(..) -> True
844
-
board.Empty | board.OffBoard -> False
845
-
}
846
-
pawn_move_to_san(from, to, is_capture, Some(piece))
847
-
}
848
-
Move(from:, to:) -> move_to_san(game, piece, from, to, False)
849
-
Capture(from:, to:) -> move_to_san(game, piece, from, to, True)
988
+
Promotion(from:, to:, piece:, captured_piece: None) ->
989
+
pawn_move_to_san(from, to, False, Some(piece))
990
+
Promotion(from:, to:, piece:, captured_piece: Some(_)) ->
991
+
pawn_move_to_san(from, to, True, Some(piece))
992
+
Move(from:, to:, ..) -> move_to_san(game, piece, from, to, False)
993
+
Capture(from:, to:, ..) -> move_to_san(game, piece, from, to, True)
850
994
}
851
995
}
852
996
···
886
1030
use <- bool.guard(move.to != to, disambiguation)
887
1031
use <- bool.guard(move.from == from, disambiguation)
888
1032
889
-
let assert board.Occupied(piece: moving_piece, colour: _) =
890
-
board.get(game.board, move.from)
891
-
as "Legal moves should only move valid pieces"
1033
+
let moving_piece = moving_piece(move)
892
1034
893
1035
use <- bool.guard(moving_piece != piece, disambiguation)
894
1036
···
1019
1161
use #(first, move) <- result.try(parse_move_part(move))
1020
1162
use #(second, move) <- result.try(parse_move_part(move))
1021
1163
1022
-
use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try(
1164
+
use #(from_file, from_rank, to_file, to_rank, move) <- result.try(
1023
1165
case first, second {
1024
1166
// `xx` is not an allowed move
1025
1167
CaptureSpecifier, CaptureSpecifier -> Error(Nil)
···
1027
1169
File(file), CaptureSpecifier -> {
1028
1170
let from_file = Some(file)
1029
1171
let from_rank = None
1030
-
let capture = True
1031
1172
use #(to_file, move) <- result.try(parse_file(move))
1032
1173
use #(to_rank, move) <- result.try(parse_rank(move))
1033
1174
1034
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1175
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1035
1176
}
1036
1177
// We disambiguate the rank and it's a capture (e.g. `R5xc4`)
1037
1178
Rank(rank), CaptureSpecifier -> {
1038
1179
let from_file = None
1039
1180
let from_rank = Some(rank)
1040
-
let capture = True
1041
1181
use #(to_file, move) <- result.try(parse_file(move))
1042
1182
use #(to_rank, move) <- result.try(parse_rank(move))
1043
1183
1044
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1184
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1045
1185
}
1046
1186
// It's a capture, and we've parsed the file of the destination (e.g.
1047
1187
// `Bxa5`)
1048
1188
CaptureSpecifier, File(to_file) -> {
1049
1189
let from_file = None
1050
1190
let from_rank = None
1051
-
let capture = True
1052
1191
use #(to_rank, move) <- result.try(parse_rank(move))
1053
1192
1054
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1193
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1055
1194
}
1056
1195
// We disambiguate the file and we've parsed the file of the destination
1057
1196
// (e.g. `Qhd4`)
1058
1197
File(from_file), File(to_file) -> {
1059
1198
let from_file = Some(from_file)
1060
1199
let from_rank = None
1061
-
let capture = False
1062
1200
use #(to_rank, move) <- result.try(parse_rank(move))
1063
1201
1064
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1202
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1065
1203
}
1066
1204
// We disambiguate the rank and we've parsed the file of the destination
1067
1205
// (e.g. `R7d2`)
1068
1206
Rank(rank), File(to_file) -> {
1069
1207
let from_file = None
1070
1208
let from_rank = Some(rank)
1071
-
let capture = False
1072
1209
use #(to_rank, move) <- result.try(parse_rank(move))
1073
1210
1074
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1211
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1075
1212
}
1076
1213
// Capture followed by a rank is not allowed, e.g. `Rx1`
1077
1214
CaptureSpecifier, Rank(_) -> Error(Nil)
1078
1215
// We've parsed the file and rank, and there's no more move to parse,
1079
1216
// so we're done. (e.g. `Nf3`)
1080
1217
File(file), Rank(rank) if move == "" ->
1081
-
Ok(#(None, None, False, file, rank, move))
1218
+
Ok(#(None, None, file, rank, move))
1082
1219
// We've disambiguated the rank and file, and we still need to parse
1083
1220
// the rest of the move. (e.g. `Qh4xe1`)
1084
1221
File(from_file), Rank(from_rank) ->
···
1086
1223
Ok(#(CaptureSpecifier, move)) -> {
1087
1224
let from_file = Some(from_file)
1088
1225
let from_rank = Some(from_rank)
1089
-
let capture = True
1090
1226
use #(to_file, move) <- result.try(parse_file(move))
1091
1227
use #(to_rank, move) <- result.try(parse_rank(move))
1092
1228
1093
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1229
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1094
1230
}
1095
1231
Ok(#(File(to_file), _)) -> {
1096
1232
let from_file = Some(from_file)
1097
1233
let from_rank = Some(from_rank)
1098
-
let capture = False
1099
1234
use #(to_rank, move) <- result.try(parse_rank(move))
1100
1235
1101
-
Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
1236
+
Ok(#(from_file, from_rank, to_file, to_rank, move))
1102
1237
}
1103
1238
Ok(#(Rank(_), _)) | Error(_) -> Error(Nil)
1104
1239
}
···
1111
1246
1112
1247
let to = to_rank * 8 + to_file
1113
1248
1114
-
case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) {
1115
-
[from] if capture -> Ok(Capture(from:, to:))
1116
-
[from] -> Ok(Move(from:, to:))
1249
+
case
1250
+
get_moves(game, piece_kind, legal_moves, from_file, from_rank, to, None)
1251
+
{
1252
+
[move] -> Ok(move)
1117
1253
// If there is more than one valid move, the notation is ambiguous, and
1118
1254
// so we error. If there are no valid moves, we also error.
1119
1255
_ -> Error(Nil)
···
1178
1314
) -> Result(Move, Nil) {
1179
1315
use #(file, move) <- result.try(parse_file(move))
1180
1316
1181
-
use #(from_file, is_capture, to_file, move) <- result.try(case move {
1317
+
use #(from_file, to_file, move) <- result.try(case move {
1182
1318
"x" <> move ->
1183
1319
parse_file(move)
1184
1320
|> result.map(fn(pair) {
1185
1321
let #(to_file, move) = pair
1186
-
#(Some(file), True, to_file, move)
1322
+
#(Some(file), to_file, move)
1187
1323
})
1188
-
_ -> Ok(#(None, False, file, move))
1324
+
_ -> Ok(#(None, file, move))
1189
1325
})
1190
1326
1191
1327
use #(rank, move) <- result.try(parse_rank(move))
···
1202
1338
let to = rank * 8 + to_file
1203
1339
1204
1340
case
1205
-
get_pieces(game, board.Pawn, legal_moves, from_file, None, to),
1206
-
promotion
1341
+
get_moves(game, board.Pawn, legal_moves, from_file, None, to, promotion)
1207
1342
{
1208
-
[from], Some(piece) -> Ok(Promotion(from:, to:, piece:))
1209
-
[from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:))
1210
-
[from], _ if is_capture -> Ok(Capture(from:, to:))
1211
-
[from], _ -> Ok(Move(from:, to:))
1212
-
_, _ -> Error(Nil)
1343
+
[move] -> Ok(move)
1344
+
_ -> Error(Nil)
1213
1345
}
1214
1346
}
1215
1347
1216
-
/// Gets the possible destination squares for a move, based on the information
1217
-
/// we know.
1218
-
fn get_pieces(
1348
+
/// Gets the possible moves for a piece, based on the information we know from
1349
+
/// SAN.
1350
+
fn get_moves(
1219
1351
game: Game,
1220
1352
find_piece: board.Piece,
1221
1353
legal_moves: List(Move),
1222
1354
from_file: option.Option(Int),
1223
1355
from_rank: option.Option(Int),
1224
1356
to: Int,
1225
-
) -> List(Int) {
1226
-
use pieces, position, #(piece, colour) <- dict.fold(game.board, [])
1357
+
promotion: Option(board.Piece),
1358
+
) -> List(Move) {
1359
+
use moves, position, #(piece, colour) <- dict.fold(game.board, [])
1227
1360
let is_valid =
1228
1361
colour == game.to_move
1229
1362
&& piece == find_piece
···
1235
1368
None -> True
1236
1369
Some(rank) -> rank == position / 8
1237
1370
}
1238
-
&& list.any(legal_moves, fn(move) { move.to == to && move.from == position })
1239
1371
1240
1372
case is_valid {
1241
-
False -> pieces
1242
-
True -> [position, ..pieces]
1373
+
False -> moves
1374
+
True ->
1375
+
case
1376
+
list.find(legal_moves, fn(move) {
1377
+
let valid = move.to == to && move.from == position
1378
+
case move, promotion {
1379
+
Promotion(piece:, ..), Some(promotion) if piece == promotion ->
1380
+
valid
1381
+
Promotion(..), _ -> False
1382
+
_, _ -> valid
1383
+
}
1384
+
})
1385
+
{
1386
+
Error(_) -> moves
1387
+
Ok(move) -> [move, ..moves]
1388
+
}
1243
1389
}
1244
1390
}
1245
1391
+133
-86
src/starfish/internal/piece_table.gleam
+133
-86
src/starfish/internal/piece_table.gleam
···
6
6
7
7
import starfish/internal/board
8
8
9
-
// Values taken from https://www.chessprogramming.org/Simplified_Evaluation_Function
9
+
// Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/
10
10
11
11
// We use tuples for constant time accessing. The values are constant so we don't
12
12
// need to worry about the cost of updating.
13
13
14
14
const pawn = #(
15
-
#(0, 0, 0, 0, 0, 0, 0, 0),
16
-
#(30, 30, 30, 30, 30, 30, 30, 30),
17
-
#(10, 10, 20, 30, 30, 20, 10, 10),
18
-
#(5, 5, 10, 25, 25, 10, 5, 5),
19
-
#(0, 0, 0, 20, 20, 0, 0, 0),
20
-
#(5, -5, -10, 0, 0, -10, -5, 5),
21
-
#(5, 10, 10, -20, -20, 10, 10, 5),
22
-
#(0, 0, 0, 0, 0, 0, 0, 0),
15
+
#(000, 000, 000, 000, 000, 000, 000, 000),
16
+
#(003, 003, 010, 019, 016, 019, 007, -005),
17
+
#(-009, -015, 011, 015, 032, 022, 005, -022),
18
+
#(-004, -023, 006, 020, 040, 017, 004, -008),
19
+
#(013, 000, -013, 001, 011, -002, -013, 005),
20
+
#(005, -012, -007, 022, -008, -005, -015, -008),
21
+
#(-007, 007, -003, -013, 005, -016, 010, -008),
22
+
#(000, 000, 000, 000, 000, 000, 000, 000),
23
23
)
24
24
25
25
const knight = #(
26
-
#(-50, -40, -30, -30, -30, -30, -40, -50),
27
-
#(-40, -20, 0, 0, 0, 0, -20, -40),
28
-
#(-30, 0, 10, 15, 15, 10, 0, -30),
29
-
#(-30, 5, 15, 20, 20, 15, 5, -30),
30
-
#(-30, 0, 15, 20, 20, 15, 0, -30),
31
-
#(-30, 5, 10, 15, 15, 10, 5, -30),
32
-
#(-40, -20, 0, 5, 5, 0, -20, -40),
33
-
#(-50, -40, -30, -30, -30, -30, -40, -50),
26
+
#(-175, -092, -074, -073, -073, -074, -092, -175),
27
+
#(-077, -041, -027, -015, -015, -027, -041, -077),
28
+
#(-061, -017, 006, 012, 012, 006, -017, -061),
29
+
#(-035, 008, 040, 049, 049, 040, 008, -035),
30
+
#(-034, 013, 044, 051, 051, 044, 013, -034),
31
+
#(-009, 022, 058, 053, 053, 058, 022, -009),
32
+
#(-067, -027, 004, 037, 037, 004, -027, -067),
33
+
#(-201, -083, -056, -026, -026, -056, -083, -201),
34
34
)
35
35
36
36
const bishop = #(
37
-
#(-20, -10, -10, -10, -10, -10, -10, -20),
38
-
#(-10, 0, 0, 0, 0, 0, 0, -10),
39
-
#(-10, 0, 5, 10, 10, 5, 0, -10),
40
-
#(-10, 5, 5, 10, 10, 5, 5, -10),
41
-
#(-10, 0, 10, 10, 10, 10, 0, -10),
42
-
#(-10, 10, 10, 10, 10, 10, 10, -10),
43
-
#(-10, 5, 0, 0, 0, 0, 5, -10),
44
-
#(-20, -10, -10, -10, -10, -10, -10, -20),
37
+
#(-053, -005, -008, -023, -023, -008, -005, -053),
38
+
#(-015, 008, 019, 004, 004, 019, 008, -015),
39
+
#(-007, 021, -005, 017, 017, -005, 021, -007),
40
+
#(-005, 011, 025, 039, 039, 025, 011, -005),
41
+
#(-012, 029, 022, 031, 031, 022, 029, -012),
42
+
#(-016, 006, 001, 011, 011, 001, 006, -016),
43
+
#(-017, -014, 005, 000, 000, 005, -014, -017),
44
+
#(-048, 001, -014, -023, -023, -014, 001, -048),
45
45
)
46
46
47
47
const rook = #(
48
-
#(0, 0, 0, 0, 0, 0, 0, 0),
49
-
#(5, 10, 10, 10, 10, 10, 10, 5),
50
-
#(-5, 0, 0, 0, 0, 0, 0, -5),
51
-
#(-5, 0, 0, 0, 0, 0, 0, -5),
52
-
#(-5, 0, 0, 0, 0, 0, 0, -5),
53
-
#(-5, 0, 0, 0, 0, 0, 0, -5),
54
-
#(-5, 0, 0, 0, 0, 0, 0, -5),
55
-
#(0, 0, 0, 5, 5, 0, 0, 0),
48
+
#(-031, -020, -014, -005, -005, -014, -020, -031),
49
+
#(-021, -013, -008, 006, 006, -008, -013, -021),
50
+
#(-025, -011, -001, 003, 003, -001, -011, -025),
51
+
#(-013, -005, -004, -006, -006, -004, -005, -013),
52
+
#(-027, -015, -004, 003, 003, -004, -015, -027),
53
+
#(-022, -002, 006, 012, 012, 006, -002, -022),
54
+
#(-002, 012, 016, 018, 018, 016, 012, -002),
55
+
#(-017, -019, -001, 009, 009, -001, -019, -017),
56
56
)
57
57
58
58
const queen = #(
59
-
#(-20, -10, -10, -5, -5, -10, -10, -20),
60
-
#(-10, 0, 0, 0, 0, 0, 0, -10),
61
-
#(-10, 0, 5, 5, 5, 5, 0, -10),
62
-
#(-5, 0, 5, 5, 5, 5, 0, -5),
63
-
#(0, 0, 5, 5, 5, 5, 0, -5),
64
-
#(-10, 5, 5, 5, 5, 5, 0, -10),
65
-
#(-10, 0, 5, 0, 0, 0, 0, -10),
66
-
#(-20, -10, -10, -5, -5, -10, -10, -20),
59
+
#(003, -005, -005, 004, 004, -005, -005, 003),
60
+
#(-003, 005, 008, 012, 012, 008, 005, -003),
61
+
#(-003, 006, 013, 007, 007, 013, 006, -003),
62
+
#(004, 005, 009, 008, 008, 009, 005, 004),
63
+
#(000, 014, 012, 005, 005, 012, 014, 000),
64
+
#(-004, 010, 006, 008, 008, 006, 010, -004),
65
+
#(-005, 006, 010, 008, 008, 010, 006, -005),
66
+
#(-002, -002, 001, -002, -002, 001, -002, -002),
67
67
)
68
68
69
69
const king = #(
70
-
#(-30, -40, -40, -50, -50, -40, -40, -30),
71
-
#(-30, -40, -40, -50, -50, -40, -40, -30),
72
-
#(-30, -40, -40, -50, -50, -40, -40, -30),
73
-
#(-30, -40, -40, -50, -50, -40, -40, -30),
74
-
#(-20, -30, -30, -40, -40, -30, -30, -20),
75
-
#(-10, -20, -20, -20, -20, -20, -20, -10),
76
-
#(20, 20, 0, 0, 0, 0, 20, 20),
77
-
#(20, 30, 10, 0, 0, 10, 30, 20),
70
+
#(271, 327, 271, 198, 198, 271, 327, 271),
71
+
#(278, 303, 234, 179, 179, 234, 303, 278),
72
+
#(195, 258, 169, 120, 120, 169, 258, 195),
73
+
#(164, 190, 138, 098, 098, 138, 190, 164),
74
+
#(154, 179, 105, 070, 070, 105, 179, 154),
75
+
#(123, 145, 081, 031, 031, 081, 145, 123),
76
+
#(088, 120, 065, 033, 033, 065, 120, 088),
77
+
#(059, 089, 045, -001, -001, 045, 089, 059),
78
+
)
79
+
80
+
/// In the middlegame, pawns are encouraged to protect the king's castling
81
+
/// squares. In the endgame though, they no longer need to protect the king and
82
+
/// instead should promote. Therefore, we use a different table to encourage this.
83
+
const pawn_endgame = #(
84
+
#(000, 000, 000, 000, 000, 000, 000, 000),
85
+
#(-010, -006, 010, 000, 014, 007, -005, -019),
86
+
#(-010, -010, -010, 004, 004, 003, -006, -004),
87
+
#(006, -002, -008, -004, -013, -012, -010, -009),
88
+
#(010, 005, 004, -005, -005, -005, 014, 009),
89
+
#(028, 020, 021, 028, 030, 007, 006, 013),
90
+
#(000, -011, 012, 021, 025, 019, 004, 007),
91
+
#(000, 000, 000, 000, 000, 000, 000, 000),
92
+
)
93
+
94
+
const knight_endgame = #(
95
+
#(-096, -065, -049, -021, -021, -049, -065, -096),
96
+
#(-067, -054, -018, 008, 008, -018, -054, -067),
97
+
#(-040, -027, -008, 029, 029, -008, -027, -040),
98
+
#(-035, -002, 013, 028, 028, 013, -002, -035),
99
+
#(-045, -016, 009, 039, 039, 009, -016, -045),
100
+
#(-051, -044, -016, 017, 017, -016, -044, -051),
101
+
#(-069, -050, -051, 012, 012, -051, -050, -069),
102
+
#(-100, -088, -056, -017, -017, -056, -088, -100),
103
+
)
104
+
105
+
const bishop_endgame = #(
106
+
#(-057, -030, -037, -012, -012, -037, -030, -057),
107
+
#(-037, -013, -017, 001, 001, -017, -013, -037),
108
+
#(-016, -001, -002, 010, 010, -002, -001, -016),
109
+
#(-020, -006, 000, 017, 017, 000, -006, -020),
110
+
#(-017, -001, -014, 015, 015, -014, -001, -017),
111
+
#(-030, 006, 004, 006, 006, 004, 006, -030),
112
+
#(-031, -020, -001, 001, 001, -001, -020, -031),
113
+
#(-046, -042, -037, -024, -024, -037, -042, -046),
114
+
)
115
+
116
+
const rook_endgame = #(
117
+
#(-009, -013, -010, -009, -009, -010, -013, -009),
118
+
#(-012, -009, -001, -002, -002, -001, -009, -012),
119
+
#(006, -008, -002, -006, -006, -002, -008, 006),
120
+
#(-006, 001, -009, 007, 007, -009, 001, -006),
121
+
#(-005, 008, 007, -006, -006, 007, 008, -005),
122
+
#(006, 001, -007, 010, 010, -007, 001, 006),
123
+
#(004, 005, 020, -005, -005, 020, 005, 004),
124
+
#(018, 000, 019, 013, 013, 019, 000, 018),
125
+
)
126
+
127
+
const queen_endgame = #(
128
+
#(-069, -057, -047, -026, -026, -047, -057, -069),
129
+
#(-055, -031, -022, -004, -004, -022, -031, -055),
130
+
#(-039, -018, -009, 003, 003, -009, -018, -039),
131
+
#(-023, -003, 013, 024, 024, 013, -003, -023),
132
+
#(-029, -006, 009, 021, 021, 009, -006, -029),
133
+
#(-038, -018, -012, 001, 001, -012, -018, -038),
134
+
#(-050, -027, -024, -008, -008, -024, -027, -050),
135
+
#(-075, -052, -043, -036, -036, -043, -052, -075),
78
136
)
79
137
80
138
/// In the beginning and middle of the game, the king must be kept safe. However
81
139
/// as the game progresses towards the end, the king should become more aggressive
82
140
/// so we use a different set of scores for kings in the endgame.
83
141
const king_endgame = #(
84
-
#(-50, -40, -30, -20, -20, -30, -40, -50),
85
-
#(-30, -20, -10, 0, 0, -10, -20, -30),
86
-
#(-30, -10, 20, 30, 30, 20, -10, -30),
87
-
#(-30, -10, 30, 40, 40, 30, -10, -30),
88
-
#(-30, -10, 30, 40, 40, 30, -10, -30),
89
-
#(-30, -10, 20, 30, 30, 20, -10, -30),
90
-
#(-30, -30, 0, 0, 0, 0, -30, -30),
91
-
#(-50, -30, -30, -30, -30, -30, -30, -50),
92
-
)
93
-
94
-
/// In the middlegame, pawns are encouraged to protect the king's castling
95
-
/// squares. In the endgame though, they no longer need to protect the king and
96
-
/// instead should promote. Therefore, we use a different table to encourage this.
97
-
const pawn_endgame = #(
98
-
#(100, 100, 100, 100, 100, 100, 100, 100),
99
-
#(80, 80, 80, 80, 80, 80, 80, 80),
100
-
#(50, 50, 50, 50, 50, 50, 50, 50),
101
-
#(30, 30, 30, 30, 30, 30, 30, 30),
102
-
#(10, 10, 10, 10, 10, 10, 10, 10),
103
-
// Since pawns can double-move, the first two ranks are equivalent from the
104
-
// pawn's perspective.
105
-
#(-10, -10, -10, -10, -10, -10, -10, -10),
106
-
#(-10, -10, -10, -10, -10, -10, -10, -10),
107
-
#(-10, -10, -10, -10, -10, -10, -10, -10),
142
+
#(001, 045, 085, 076, 076, 085, 045, 001),
143
+
#(053, 100, 133, 135, 135, 133, 100, 053),
144
+
#(088, 130, 169, 175, 175, 169, 130, 088),
145
+
#(103, 156, 172, 172, 172, 172, 156, 103),
146
+
#(096, 166, 199, 199, 199, 199, 166, 096),
147
+
#(092, 172, 184, 191, 191, 184, 172, 092),
148
+
#(047, 121, 116, 131, 131, 116, 121, 047),
149
+
#(011, 059, 073, 078, 078, 073, 059, 011),
108
150
)
109
151
110
152
type Table =
···
151
193
}
152
194
}
153
195
154
-
/// Calculate the score for a given piece at a position at some point in the game
155
-
pub fn piece_score(
196
+
/// Calculate the score for a given piece at a position during the middlegame
197
+
pub fn piece_score_midgame(
156
198
piece: board.Piece,
157
199
colour: board.Colour,
158
200
position: Int,
159
-
phase: Int,
160
201
) -> Int {
161
202
let table = case piece {
162
203
board.Pawn -> pawn
···
167
208
board.King -> king
168
209
}
169
210
170
-
let middlegame_value = get(table, position, colour)
211
+
get(table, position, colour)
212
+
}
171
213
172
-
case piece {
173
-
board.King if phase > 0 ->
174
-
interpolate(middlegame_value, get(king_endgame, position, colour), phase)
175
-
board.Pawn if phase > 0 ->
176
-
interpolate(middlegame_value, get(pawn_endgame, position, colour), phase)
177
-
_ -> middlegame_value
214
+
/// Calculate the score for a given piece at a position in the endgame
215
+
pub fn piece_score_endgame(
216
+
piece: board.Piece,
217
+
colour: board.Colour,
218
+
position: Int,
219
+
) -> Int {
220
+
let table = case piece {
221
+
board.Pawn -> pawn_endgame
222
+
board.King -> king_endgame
223
+
board.Bishop -> bishop_endgame
224
+
board.Knight -> knight_endgame
225
+
board.Queen -> queen_endgame
226
+
board.Rook -> rook_endgame
178
227
}
179
-
}
180
228
181
-
fn interpolate(middlegame_value: Int, endgame_value: Int, phase: Int) -> Int {
182
-
{ middlegame_value * { 128 - phase } + endgame_value * phase } / 128
229
+
get(table, position, colour)
183
230
}
+17
-18
src/starfish/internal/search.gleam
+17
-18
src/starfish/internal/search.gleam
···
3
3
import gleam/list
4
4
import gleam/option.{type Option, None, Some}
5
5
import starfish/internal/board
6
-
import starfish/internal/evaluate
7
6
import starfish/internal/game.{type Game}
8
7
import starfish/internal/hash
9
8
import starfish/internal/move.{type Move}
···
361
360
/// in order to save iterating the list a second time. The guesses are discarded
362
361
/// after this point.
363
362
fn order_moves(game: Game) -> List(#(Move, Int)) {
364
-
let phase =
365
-
game.phase(
366
-
game.white_pieces.non_pawn_material,
367
-
game.black_pieces.non_pawn_material,
368
-
)
363
+
let phase = game.phase(game)
369
364
game
370
365
|> move.legal
371
366
|> collect_guessed_eval(game, phase, [])
···
408
403
/// order than random. Searching better moves first improves alpha-beta pruning,
409
404
/// allowing us to search more positions.
410
405
fn guess_eval(game: Game, move: Move, phase: Int) -> Int {
411
-
let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from)
412
-
as "Invalid move trying to move empty piece"
406
+
let piece = move.moving_piece(move)
407
+
let colour = game.to_move
413
408
414
409
let moving_piece = case move {
415
410
move.Promotion(piece:, ..) -> piece
···
417
412
piece
418
413
}
419
414
420
-
let from_score = piece_table.piece_score(piece, colour, move.from, phase)
421
-
let to_score = piece_table.piece_score(moving_piece, colour, move.to, phase)
415
+
let from_score_midgame =
416
+
piece_table.piece_score_midgame(piece, colour, move.from)
417
+
let from_score_endgame =
418
+
piece_table.piece_score_endgame(piece, colour, move.from)
419
+
let from_score =
420
+
game.interpolate_phase(from_score_midgame, from_score_endgame, phase)
421
+
422
+
let to_score_midgame = piece_table.piece_score_midgame(piece, colour, move.to)
423
+
let to_score_endgame = piece_table.piece_score_endgame(piece, colour, move.to)
424
+
let to_score =
425
+
game.interpolate_phase(to_score_midgame, to_score_endgame, phase)
426
+
422
427
let position_improvement = to_score - from_score
423
428
424
429
let move_specific_score = case move {
425
-
// TODO store information in moves so we don't have to retrieve it from the
426
-
// board every time.
427
-
move.Capture(..) -> {
428
-
let assert board.Occupied(piece: captured_piece, colour: _) =
429
-
board.get(game.board, move.to)
430
-
as "Invalid capture moving to empty square"
431
-
430
+
move.Capture(captured_piece:, ..) -> {
432
431
capture_promotion_bonus
433
432
// Capturing a more valuable piece is better, and using a less valuable
434
433
// piece to capture is usually better. However, we prioritise the value of
···
467
466
best_eval: Int,
468
467
best_opponent_move: Int,
469
468
) -> Int {
470
-
let evaluation = evaluate.evaluate(game, moves)
469
+
let evaluation = game.evaluation(game, moves)
471
470
472
471
use <- bool.guard(evaluation >= best_opponent_move, evaluation)
473
472
+35
-19
src/starfish.gleam
+35
-19
src/starfish.gleam
···
1
-
import birl
2
1
import gleam/bool
2
+
import gleam/list
3
3
import gleam/result
4
4
import starfish/internal/board
5
5
import starfish/internal/game
6
6
import starfish/internal/move
7
7
import starfish/internal/search
8
8
9
-
pub type Game =
10
-
game.Game
9
+
pub opaque type Game {
10
+
Game(game: game.Game)
11
+
}
11
12
12
13
/// A single legal move on the chess board.
13
-
pub type Move =
14
-
move.Move
14
+
pub opaque type Move {
15
+
Move(move: move.Move)
16
+
}
17
+
18
+
@internal
19
+
pub fn get_move(move: Move) -> move.Move {
20
+
move.move
21
+
}
15
22
16
23
/// The [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
17
24
/// representing the initial position of a chess game.
···
36
43
/// starfish.from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")
37
44
/// ```
38
45
pub fn from_fen(fen: String) -> Game {
39
-
game.from_fen(fen)
46
+
Game(game.from_fen(fen))
40
47
}
41
48
42
49
pub type FenParseError {
···
84
91
/// starfish.try_from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")
85
92
/// ```
86
93
pub fn try_from_fen(fen: String) -> Result(Game, FenParseError) {
87
-
result.map_error(game.try_from_fen(fen), convert_fen_parse_error)
94
+
game.try_from_fen(fen)
95
+
|> result.map_error(convert_fen_parse_error)
96
+
|> result.map(Game)
88
97
}
89
98
90
99
// Since circular imports are not allowed, but we want the user to be able to
···
107
116
108
117
/// Returns a game representing the initial position.
109
118
pub fn new() -> Game {
110
-
game.initial_position()
119
+
Game(game.initial_position())
111
120
}
112
121
113
122
/// Convert a game into its FEN string representation.
···
118
127
/// assert starfish.to_fen(starfish.new()) == starfish.starting_fen
119
128
/// ```
120
129
pub fn to_fen(game: Game) -> String {
121
-
game.to_fen(game)
130
+
game.to_fen(game.game)
122
131
}
123
132
124
133
pub fn legal_moves(game: Game) -> List(Move) {
125
-
move.legal(game)
134
+
list.map(move.legal(game.game), Move)
126
135
}
127
136
128
137
/// Used to determine how long to search positions
···
143
152
let until = case cutoff {
144
153
Depth(depth:) -> fn(current_depth) { current_depth > depth }
145
154
Time(milliseconds:) -> {
146
-
let end_time = birl.monotonic_now() + milliseconds * 1000
147
-
fn(_) { birl.monotonic_now() >= end_time }
155
+
let end_time = monotonic_time() + milliseconds
156
+
fn(_) { monotonic_time() >= end_time }
148
157
}
149
158
}
150
159
151
-
search.best_move(game, until)
160
+
result.map(search.best_move(game.game, until), Move)
152
161
}
153
162
163
+
@external(erlang, "starfish_ffi", "monotonic_time")
164
+
@external(javascript, "./starfish_ffi.mjs", "monotonic_time")
165
+
fn monotonic_time() -> Int
166
+
154
167
pub fn apply_move(game: Game, move: Move) -> Game {
155
-
move.apply(game, move)
168
+
Game(move.apply(game.game, move.move))
156
169
}
157
170
158
171
pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String {
159
-
move.to_standard_algebraic_notation(move, game)
172
+
move.to_standard_algebraic_notation(move.move, game.game)
160
173
}
161
174
162
175
/// Convert a move to [long algebraic notation](
···
164
177
/// specifically the UCI format, containing the start and end positions. For
165
178
/// example, `e2e4` or `c7d8q`.
166
179
pub fn to_long_algebraic_notation(move: Move) -> String {
167
-
move.to_long_algebraic_notation(move)
180
+
move.to_long_algebraic_notation(move.move)
168
181
}
169
182
170
183
/// Parses a move from either long algebraic notation, in the same format as
···
173
186
/// Returns an error if the syntax is invalid or the move is not legal on the
174
187
/// board.
175
188
pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) {
176
-
let legal_moves = legal_moves(game)
189
+
let legal_moves = move.legal(game.game)
177
190
case move.from_long_algebraic_notation(move, legal_moves) {
178
-
Ok(move) -> Ok(move)
179
-
Error(_) -> move.from_standard_algebraic_notation(move, game, legal_moves)
191
+
Ok(move) -> Ok(Move(move))
192
+
Error(_) ->
193
+
move.from_standard_algebraic_notation(move, game.game, legal_moves)
194
+
|> result.map(Move)
180
195
}
181
196
}
182
197
···
196
211
197
212
/// Returns the current game state: A win, draw or neither.
198
213
pub fn state(game: Game) -> GameState {
214
+
let game = game.game
199
215
use <- bool.guard(game.half_moves >= 50, Draw(FiftyMoves))
200
216
use <- bool.guard(
201
217
game.is_insufficient_material(game),
+9
src/starfish_ffi.erl
+9
src/starfish_ffi.erl
+3
src/starfish_ffi.mjs
+3
src/starfish_ffi.mjs
+133
-127
test/starfish_test.gleam
+133
-127
test/starfish_test.gleam
···
1
1
import gleam/int
2
2
import gleam/io
3
3
import gleam/list
4
+
import gleam/option.{None, Some}
4
5
import gleeunit
5
6
import pocket_watch
6
7
import starfish
···
12
13
gleeunit.main()
13
14
}
14
15
15
-
/// Compare the state of two games, ignoring additional fields
16
-
fn game_equal(a: game.Game, b: game.Game) -> Bool {
17
-
a.board == b.board
18
-
&& a.to_move == b.to_move
19
-
&& a.castling == b.castling
20
-
&& a.en_passant_square == b.en_passant_square
21
-
&& a.half_moves == b.half_moves
22
-
&& a.full_moves == b.full_moves
23
-
}
24
-
25
16
pub fn from_fen_test() {
26
17
let initial = starfish.new()
27
18
let parsed = starfish.from_fen(starfish.starting_fen)
28
-
assert game_equal(initial, parsed)
19
+
assert initial == parsed
29
20
30
21
let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
31
22
let parsed = starfish.from_fen(initial_with_only_position)
32
-
assert game_equal(initial, parsed)
23
+
assert initial == parsed
33
24
}
34
25
35
26
pub fn try_from_fen_test() {
36
27
let initial = starfish.new()
37
28
let assert Ok(parsed) = starfish.try_from_fen(starfish.starting_fen)
38
-
assert game_equal(parsed, initial)
29
+
assert parsed == initial
39
30
40
31
let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
41
32
let assert Error(error) = starfish.try_from_fen(initial_with_only_position)
···
43
34
}
44
35
45
36
pub fn to_fen_test() {
46
-
let fen = game.to_fen(starfish.new())
37
+
let fen = game.to_fen(game.initial_position())
47
38
assert fen == starfish.starting_fen
48
39
49
40
let fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"
···
51
42
}
52
43
53
44
pub fn to_long_algebraic_notation_test() {
54
-
assert move.Move(from: 8, to: 24) |> starfish.to_long_algebraic_notation
45
+
assert move.Move(board.Pawn, from: 8, to: 24)
46
+
|> move.to_long_algebraic_notation
55
47
== "a2a4"
56
-
assert move.Move(from: 6, to: 21) |> starfish.to_long_algebraic_notation
48
+
assert move.Move(board.Pawn, from: 6, to: 21)
49
+
|> move.to_long_algebraic_notation
57
50
== "g1f3"
58
-
assert move.Move(from: 57, to: 42) |> starfish.to_long_algebraic_notation
51
+
assert move.Move(board.Pawn, from: 57, to: 42)
52
+
|> move.to_long_algebraic_notation
59
53
== "b8c6"
60
-
assert move.Move(from: 49, to: 33) |> starfish.to_long_algebraic_notation
54
+
assert move.Move(board.Pawn, from: 49, to: 33)
55
+
|> move.to_long_algebraic_notation
61
56
== "b7b5"
62
-
assert move.EnPassant(from: 32, to: 41) |> starfish.to_long_algebraic_notation
57
+
assert move.EnPassant(from: 32, to: 41) |> move.to_long_algebraic_notation
63
58
== "a5b6"
64
-
assert move.Castle(from: 4, to: 6) |> starfish.to_long_algebraic_notation
59
+
assert move.Castle(from: 4, to: 6) |> move.to_long_algebraic_notation
65
60
== "e1g1"
66
-
assert move.Castle(from: 4, to: 2) |> starfish.to_long_algebraic_notation
61
+
assert move.Castle(from: 4, to: 2) |> move.to_long_algebraic_notation
67
62
== "e1c1"
68
-
assert move.Castle(from: 60, to: 62) |> starfish.to_long_algebraic_notation
63
+
assert move.Castle(from: 60, to: 62) |> move.to_long_algebraic_notation
69
64
== "e8g8"
70
-
assert move.Castle(from: 60, to: 58) |> starfish.to_long_algebraic_notation
65
+
assert move.Castle(from: 60, to: 58) |> move.to_long_algebraic_notation
71
66
== "e8c8"
72
-
assert move.Promotion(from: 51, to: 58, piece: board.Queen)
73
-
|> starfish.to_long_algebraic_notation
67
+
assert move.Promotion(
68
+
from: 51,
69
+
to: 58,
70
+
piece: board.Queen,
71
+
captured_piece: None,
72
+
)
73
+
|> move.to_long_algebraic_notation
74
74
== "d7c8q"
75
-
assert move.Promotion(from: 11, to: 2, piece: board.Knight)
76
-
|> starfish.to_long_algebraic_notation
75
+
assert move.Promotion(
76
+
from: 11,
77
+
to: 2,
78
+
piece: board.Knight,
79
+
captured_piece: Some(board.Rook),
80
+
)
81
+
|> move.to_long_algebraic_notation
77
82
== "d2c1n"
78
-
assert move.Capture(from: 49, to: 7) |> starfish.to_long_algebraic_notation
83
+
assert move.Capture(board.Bishop, from: 49, to: 7, captured_piece: board.Pawn)
84
+
|> move.to_long_algebraic_notation
79
85
== "b7h1"
80
86
}
81
87
82
88
pub fn parse_long_algebraic_notation_test() {
83
89
let assert Ok(move) = starfish.parse_move("a2a4", starfish.new())
84
-
assert move == move.Move(from: 8, to: 24)
90
+
assert starfish.get_move(move) == move.Move(board.Pawn, from: 8, to: 24)
85
91
let assert Ok(move) = starfish.parse_move("g1f3", starfish.new())
86
-
assert move == move.Move(from: 6, to: 21)
92
+
assert starfish.get_move(move) == move.Move(board.Knight, from: 6, to: 21)
87
93
let assert Ok(move) =
88
94
starfish.parse_move(
89
95
"b8c6",
···
91
97
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
92
98
),
93
99
)
94
-
assert move == move.Move(from: 57, to: 42)
100
+
assert starfish.get_move(move) == move.Move(board.Knight, from: 57, to: 42)
95
101
let assert Ok(move) =
96
102
starfish.parse_move(
97
103
"B7b5",
···
99
105
"rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
100
106
),
101
107
)
102
-
assert move == move.Move(from: 49, to: 33)
108
+
assert starfish.get_move(move) == move.Move(board.Pawn, from: 49, to: 33)
103
109
let assert Ok(move) =
104
110
starfish.parse_move(
105
111
"a5b6",
···
107
113
"rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
108
114
),
109
115
)
110
-
assert move == move.EnPassant(from: 32, to: 41)
116
+
assert starfish.get_move(move) == move.EnPassant(from: 32, to: 41)
111
117
let assert Ok(move) =
112
118
starfish.parse_move(
113
119
"e1G1",
···
115
121
"rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
116
122
),
117
123
)
118
-
assert move == move.Castle(from: 4, to: 6)
124
+
assert starfish.get_move(move) == move.Castle(from: 4, to: 6)
119
125
let assert Ok(move) =
120
126
starfish.parse_move(
121
127
"e1c1",
···
123
129
"rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
124
130
),
125
131
)
126
-
assert move == move.Castle(from: 4, to: 2)
132
+
assert starfish.get_move(move) == move.Castle(from: 4, to: 2)
127
133
let assert Ok(move) =
128
134
starfish.parse_move(
129
135
"e8g8",
···
131
137
"rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
132
138
),
133
139
)
134
-
assert move == move.Castle(from: 60, to: 62)
140
+
assert starfish.get_move(move) == move.Castle(from: 60, to: 62)
135
141
let assert Ok(move) =
136
142
starfish.parse_move(
137
143
"E8C8",
···
139
145
"r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
140
146
),
141
147
)
142
-
assert move == move.Castle(from: 60, to: 58)
148
+
assert starfish.get_move(move) == move.Castle(from: 60, to: 58)
143
149
let assert Ok(move) =
144
150
starfish.parse_move(
145
151
"d7c8q",
···
147
153
"rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
148
154
),
149
155
)
150
-
assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
156
+
assert starfish.get_move(move)
157
+
== move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)
151
158
let assert Ok(move) =
152
159
starfish.parse_move(
153
160
"d2c1N",
···
155
162
"rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
156
163
),
157
164
)
158
-
assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
165
+
assert starfish.get_move(move)
166
+
== move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)
159
167
let assert Ok(move) =
160
168
starfish.parse_move(
161
169
"b7h1",
···
163
171
"rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
164
172
),
165
173
)
166
-
assert move == move.Capture(from: 49, to: 7)
174
+
assert starfish.get_move(move)
175
+
== move.Capture(board.Bishop, board.Rook, from: 49, to: 7)
167
176
168
177
let assert Error(Nil) = starfish.parse_move("abcd", starfish.new())
169
178
let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new())
···
172
181
173
182
pub fn parse_standard_algebraic_notation_test() {
174
183
let assert Ok(move) = starfish.parse_move("a4", starfish.new())
175
-
assert move == move.Move(from: 8, to: 24)
184
+
assert starfish.get_move(move) == move.Move(board.Pawn, from: 8, to: 24)
176
185
let assert Ok(move) = starfish.parse_move("Nf3", starfish.new())
177
-
assert move == move.Move(from: 6, to: 21)
186
+
assert starfish.get_move(move) == move.Move(board.Knight, from: 6, to: 21)
178
187
let assert Ok(move) =
179
188
starfish.parse_move(
180
189
"Nc6",
···
182
191
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
183
192
),
184
193
)
185
-
assert move == move.Move(from: 57, to: 42)
194
+
assert starfish.get_move(move) == move.Move(board.Knight, from: 57, to: 42)
186
195
let assert Ok(move) =
187
196
starfish.parse_move(
188
197
"b5",
···
190
199
"rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
191
200
),
192
201
)
193
-
assert move == move.Move(from: 49, to: 33)
202
+
assert starfish.get_move(move) == move.Move(board.Pawn, from: 49, to: 33)
194
203
let assert Ok(move) =
195
204
starfish.parse_move(
196
205
"axb6",
···
198
207
"rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
199
208
),
200
209
)
201
-
assert move == move.EnPassant(from: 32, to: 41)
210
+
assert starfish.get_move(move) == move.EnPassant(from: 32, to: 41)
202
211
let assert Ok(move) =
203
212
starfish.parse_move(
204
213
"O-O",
···
206
215
"rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
207
216
),
208
217
)
209
-
assert move == move.Castle(from: 4, to: 6)
218
+
assert starfish.get_move(move) == move.Castle(from: 4, to: 6)
210
219
let assert Ok(move) =
211
220
starfish.parse_move(
212
221
"O-O-O",
···
214
223
"rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
215
224
),
216
225
)
217
-
assert move == move.Castle(from: 4, to: 2)
226
+
assert starfish.get_move(move) == move.Castle(from: 4, to: 2)
218
227
let assert Ok(move) =
219
228
starfish.parse_move(
220
229
"O-O",
···
222
231
"rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
223
232
),
224
233
)
225
-
assert move == move.Castle(from: 60, to: 62)
234
+
assert starfish.get_move(move) == move.Castle(from: 60, to: 62)
226
235
let assert Ok(move) =
227
236
starfish.parse_move(
228
237
"0-0-0",
···
230
239
"r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
231
240
),
232
241
)
233
-
assert move == move.Castle(from: 60, to: 58)
242
+
assert starfish.get_move(move) == move.Castle(from: 60, to: 58)
234
243
let assert Ok(move) =
235
244
starfish.parse_move(
236
245
"c8q",
···
238
247
"rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
239
248
),
240
249
)
241
-
assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
250
+
assert starfish.get_move(move)
251
+
== move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)
242
252
let assert Ok(move) =
243
253
starfish.parse_move(
244
254
"c1=N",
···
246
256
"rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
247
257
),
248
258
)
249
-
assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
259
+
assert starfish.get_move(move)
260
+
== move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)
250
261
let assert Ok(move) =
251
262
starfish.parse_move(
252
263
"Bxh1",
···
254
265
"rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
255
266
),
256
267
)
257
-
assert move == move.Capture(from: 49, to: 7)
268
+
assert starfish.get_move(move)
269
+
== move.Capture(board.Bishop, board.Rook, from: 49, to: 7)
258
270
let assert Ok(move) =
259
271
starfish.parse_move(
260
272
"Rac4",
261
273
starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
262
274
)
263
-
assert move == move.Move(from: 24, to: 26)
275
+
assert starfish.get_move(move) == move.Move(board.Rook, from: 24, to: 26)
264
276
265
277
let assert Ok(move) =
266
278
starfish.parse_move(
267
279
"R7c6",
268
280
starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
269
281
)
270
-
assert move == move.Move(from: 50, to: 42)
282
+
assert starfish.get_move(move) == move.Move(board.Rook, from: 50, to: 42)
271
283
272
284
let assert Error(Nil) = starfish.parse_move("e2", starfish.new())
273
285
let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new())
···
275
287
}
276
288
277
289
pub fn to_standard_algebraic_notation_test() {
278
-
assert starfish.to_standard_algebraic_notation(
279
-
move.Move(from: 8, to: 24),
280
-
starfish.new(),
290
+
assert move.to_standard_algebraic_notation(
291
+
move.Move(board.Pawn, from: 8, to: 24),
292
+
game.initial_position(),
281
293
)
282
294
== "a4"
283
-
assert starfish.to_standard_algebraic_notation(
284
-
move.Move(from: 6, to: 21),
285
-
starfish.new(),
295
+
assert move.to_standard_algebraic_notation(
296
+
move.Move(board.Knight, from: 6, to: 21),
297
+
game.initial_position(),
286
298
)
287
299
== "Nf3"
288
-
assert starfish.to_standard_algebraic_notation(
289
-
move.Move(from: 57, to: 42),
290
-
starfish.from_fen(
300
+
assert move.to_standard_algebraic_notation(
301
+
move.Move(board.Knight, from: 57, to: 42),
302
+
game.from_fen(
291
303
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
292
304
),
293
305
)
294
306
== "Nc6"
295
307
296
-
assert starfish.to_standard_algebraic_notation(
297
-
move.Move(from: 49, to: 33),
298
-
starfish.from_fen(
299
-
"rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
300
-
),
308
+
assert move.to_standard_algebraic_notation(
309
+
move.Move(board.Pawn, from: 49, to: 33),
310
+
game.from_fen("rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1"),
301
311
)
302
312
== "b5"
303
313
304
-
assert starfish.to_standard_algebraic_notation(
314
+
assert move.to_standard_algebraic_notation(
305
315
move.EnPassant(from: 32, to: 41),
306
-
starfish.from_fen(
316
+
game.from_fen(
307
317
"rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
308
318
),
309
319
)
310
320
== "axb6"
311
321
312
-
assert starfish.to_standard_algebraic_notation(
322
+
assert move.to_standard_algebraic_notation(
313
323
move.Castle(from: 4, to: 6),
314
-
starfish.from_fen(
324
+
game.from_fen(
315
325
"rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
316
326
),
317
327
)
318
328
== "O-O"
319
329
320
-
assert starfish.to_standard_algebraic_notation(
330
+
assert move.to_standard_algebraic_notation(
321
331
move.Castle(from: 4, to: 2),
322
-
starfish.from_fen(
332
+
game.from_fen(
323
333
"rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
324
334
),
325
335
)
326
336
== "O-O-O"
327
337
328
-
assert starfish.to_standard_algebraic_notation(
338
+
assert move.to_standard_algebraic_notation(
329
339
move.Castle(from: 60, to: 62),
330
-
starfish.from_fen(
340
+
game.from_fen(
331
341
"rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
332
342
),
333
343
)
334
344
== "O-O"
335
345
336
-
assert starfish.to_standard_algebraic_notation(
346
+
assert move.to_standard_algebraic_notation(
337
347
move.Castle(from: 60, to: 58),
338
-
starfish.from_fen(
348
+
game.from_fen(
339
349
"r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
340
350
),
341
351
)
342
352
== "O-O-O"
343
353
344
-
assert starfish.to_standard_algebraic_notation(
345
-
move.Promotion(from: 51, to: 58, piece: board.Queen),
346
-
starfish.from_fen(
347
-
"rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
348
-
),
354
+
assert move.to_standard_algebraic_notation(
355
+
move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen),
356
+
game.from_fen("rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5"),
349
357
)
350
358
== "dxc8=Q"
351
359
352
-
assert starfish.to_standard_algebraic_notation(
353
-
move.Promotion(from: 11, to: 2, piece: board.Knight),
354
-
starfish.from_fen(
355
-
"rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
356
-
),
360
+
assert move.to_standard_algebraic_notation(
361
+
move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight),
362
+
game.from_fen("rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5"),
357
363
)
358
364
== "dxc1=N"
359
365
360
-
assert starfish.to_standard_algebraic_notation(
361
-
move.Capture(from: 49, to: 7),
362
-
starfish.from_fen(
366
+
assert move.to_standard_algebraic_notation(
367
+
move.Capture(board.Bishop, board.Rook, from: 49, to: 7),
368
+
game.from_fen(
363
369
"rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
364
370
),
365
371
)
366
372
== "Bxh1"
367
373
368
-
assert starfish.to_standard_algebraic_notation(
369
-
move.Move(from: 24, to: 26),
370
-
starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
374
+
assert move.to_standard_algebraic_notation(
375
+
move.Move(board.Rook, from: 24, to: 26),
376
+
game.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
371
377
)
372
378
== "Rac4"
373
379
374
-
assert starfish.to_standard_algebraic_notation(
375
-
move.Move(from: 50, to: 42),
376
-
starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
380
+
assert move.to_standard_algebraic_notation(
381
+
move.Move(board.Rook, from: 50, to: 42),
382
+
game.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
377
383
)
378
384
== "R7c6"
379
385
380
-
assert starfish.to_standard_algebraic_notation(
381
-
move.Capture(from: 31, to: 13),
382
-
starfish.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"),
386
+
assert move.to_standard_algebraic_notation(
387
+
move.Capture(board.Queen, board.Bishop, from: 31, to: 13),
388
+
game.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"),
383
389
)
384
390
== "Qh4xf2"
385
391
···
389
395
}
390
396
391
397
pub fn phase_test() {
392
-
let game = starfish.new()
393
-
assert game.phase(
394
-
game.white_pieces.non_pawn_material,
395
-
game.black_pieces.non_pawn_material,
396
-
)
397
-
== 0
398
-
let game = starfish.from_fen("k7/8/8/8/8/8/8/K7")
399
-
assert game.phase(
400
-
game.white_pieces.non_pawn_material,
401
-
game.black_pieces.non_pawn_material,
402
-
)
403
-
== 128
398
+
assert game.phase(game.initial_position()) == 0
399
+
assert game.phase(game.from_fen("k7/8/8/8/8/8/8/K7")) == 128
404
400
}
405
401
406
402
fn apply_move(game: starfish.Game, move: String) -> starfish.Game {
···
467
463
}
468
464
469
465
fn perft(fen: String, depth: Int, expected_moves: Int) {
470
-
assert do_perft(game.from_fen(fen), depth - 1) == expected_moves
466
+
assert do_perft(starfish.from_fen(fen), depth - 1) == expected_moves
471
467
}
472
468
473
-
fn do_perft(game: game.Game, depth: Int) -> Int {
469
+
fn do_perft(game: starfish.Game, depth: Int) -> Int {
474
470
let legal_moves = starfish.legal_moves(game)
475
471
case depth {
476
472
0 -> list.length(legal_moves)
···
501
497
until: starfish.Depth(5),
502
498
)
503
499
// b4f4
504
-
assert move == move.Capture(from: 25, to: 29)
500
+
assert starfish.get_move(move)
501
+
== move.Capture(board.Rook, board.Pawn, from: 25, to: 29)
505
502
506
503
let assert Ok(move) =
507
504
starfish.search(
508
505
starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"),
509
506
until: starfish.Depth(10),
510
507
)
511
-
assert move == move.Move(from: 29, to: 31)
508
+
assert starfish.get_move(move) == move.Move(board.Rook, from: 29, to: 31)
512
509
}
513
510
514
511
pub fn perft_initial_position_test_() {
···
713
710
moves: List(move.Move),
714
711
expected_fen: String,
715
712
) {
716
-
let final_fen =
713
+
let game =
717
714
starting_fen
718
715
|> game.from_fen
719
-
|> list.fold(moves, _, starfish.apply_move)
720
-
|> game.to_fen
716
+
|> list.fold(moves, _, move.apply)
721
717
722
-
assert final_fen == expected_fen
718
+
let game = game.Game(..game, previous_positions: [])
719
+
720
+
let expected_game = game.from_fen(expected_fen)
721
+
722
+
assert game == expected_game
723
723
}
724
724
725
725
pub fn apply_move_test() {
726
726
test_apply_move(
727
727
starfish.starting_fen,
728
728
// a2a4
729
-
[move.Move(from: 8, to: 24)],
729
+
[move.Move(board.Pawn, from: 8, to: 24)],
730
730
"rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq a3 0 1",
731
731
)
732
732
733
733
test_apply_move(
734
734
starfish.starting_fen,
735
735
// g1f3
736
-
[move.Move(from: 6, to: 21)],
736
+
[move.Move(board.Knight, from: 6, to: 21)],
737
737
"rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1",
738
738
)
739
739
740
740
test_apply_move(
741
741
starfish.starting_fen,
742
742
// a2a4, b8c6
743
-
[move.Move(from: 8, to: 24), move.Move(from: 57, to: 42)],
743
+
[
744
+
move.Move(board.Pawn, from: 8, to: 24),
745
+
move.Move(board.Knight, from: 57, to: 42),
746
+
],
744
747
"r1bqkbnr/pppppppp/2n5/8/P7/8/1PPPPPPP/RNBQKBNR w KQkq - 1 2",
745
748
)
746
749
747
750
test_apply_move(
748
751
starfish.starting_fen,
749
752
// a2a4, b7b5
750
-
[move.Move(from: 8, to: 24), move.Move(from: 49, to: 33)],
753
+
[
754
+
move.Move(board.Pawn, from: 8, to: 24),
755
+
move.Move(board.Pawn, from: 49, to: 33),
756
+
],
751
757
"rnbqkbnr/p1pppppp/8/1p6/P7/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 2",
752
758
)
753
759
···
789
795
test_apply_move(
790
796
"rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
791
797
// d7c8q
792
-
[move.Promotion(from: 51, to: 58, piece: board.Queen)],
798
+
[move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)],
793
799
"rnQq1bnr/ppp1kpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR b KQ - 0 5",
794
800
)
795
801
796
802
test_apply_move(
797
803
"rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
798
804
// d2c1n
799
-
[move.Promotion(from: 11, to: 2, piece: board.Knight)],
805
+
[move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)],
800
806
"rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPP1KPP1/RNnQ1BNR w kq - 0 6",
801
807
)
802
808
···
804
810
test_apply_move(
805
811
"rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
806
812
// b7h1
807
-
[move.Capture(from: 49, to: 7)],
813
+
[move.Capture(board.Bishop, board.Rook, from: 49, to: 7)],
808
814
"rn1qkbnr/p1pppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNb w Qkq - 0 4",
809
815
)
810
816
}