A chess library for Gleam
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Calculate and store attack information

+642
+8
src/starfish/internal/game.gleam
··· 6 6 import gleam/string 7 7 import starfish/internal/board.{Black, White} 8 8 import starfish/internal/hash 9 + import starfish/internal/move/attack 9 10 import starfish/internal/piece_table 10 11 11 12 pub type Castling { ··· 31 32 hash_data: hash.HashData, 32 33 piece_tables: piece_table.PieceTables, 33 34 previous_positions: Set(Int), 35 + attack_information: attack.AttackInformation, 34 36 ) 35 37 } 36 38 ··· 42 44 let piece_tables = piece_table.construct_tables() 43 45 let board = board.initial_position() 44 46 let zobrist_hash = hash.hash(hash_data, board, to_move) 47 + let attack_information = attack.calculate(board, to_move) 45 48 46 49 Game( 47 50 board:, ··· 54 57 hash_data:, 55 58 piece_tables:, 56 59 previous_positions: set.new(), 60 + attack_information:, 57 61 ) 58 62 } 59 63 ··· 98 102 let hash_data = hash.generate_data() 99 103 let piece_tables = piece_table.construct_tables() 100 104 let zobrist_hash = hash.hash(hash_data, board, to_move) 105 + let attack_information = attack.calculate(board, to_move) 101 106 102 107 Game( 103 108 board:, ··· 110 115 hash_data:, 111 116 piece_tables:, 112 117 previous_positions: set.new(), 118 + attack_information:, 113 119 ) 114 120 } 115 121 ··· 270 276 let hash_data = hash.generate_data() 271 277 let piece_tables = piece_table.construct_tables() 272 278 let zobrist_hash = hash.hash(hash_data, board, to_move) 279 + let attack_information = attack.calculate(board, to_move) 273 280 274 281 Ok(Game( 275 282 board:, ··· 282 289 hash_data:, 283 290 piece_tables:, 284 291 previous_positions: set.new(), 292 + attack_information:, 285 293 )) 286 294 } 287 295
+13
src/starfish/internal/move.gleam
··· 5 5 import starfish/internal/board 6 6 import starfish/internal/game.{type Game, Game} 7 7 import starfish/internal/hash 8 + import starfish/internal/move/attack 8 9 import starfish/internal/move/direction.{type Direction} 9 10 10 11 pub type Valid ··· 259 260 hash_data:, 260 261 piece_tables:, 261 262 previous_positions:, 263 + attack_information: _, 262 264 ) = game 263 265 264 266 let assert board.Occupied(piece:, colour:) = 265 267 iv.get_or_default(board, from, board.Empty) 268 + as "Tried to apply castle move from invalid position" 266 269 267 270 let castling = case colour { 268 271 board.Black -> ··· 308 311 // TODO: Update incrementally 309 312 let zobrist_hash = hash.hash(hash_data, board, to_move) 310 313 314 + // TODO: Maybe we can update this incrementally too? 315 + let attack_information = attack.calculate(board, to_move) 316 + 311 317 Game( 312 318 board:, 313 319 to_move:, ··· 319 325 hash_data:, 320 326 piece_tables:, 321 327 previous_positions:, 328 + attack_information:, 322 329 ) 323 330 } 324 331 ··· 341 348 hash_data:, 342 349 piece_tables:, 343 350 previous_positions:, 351 + attack_information: _, 344 352 ) = game 345 353 346 354 let assert board.Occupied(piece:, colour:) = 347 355 iv.get_or_default(board, from, board.Empty) 356 + as "Tried to apply move from invalid position" 348 357 349 358 let castling = 350 359 castling ··· 395 404 // TODO: Update incrementally 396 405 let zobrist_hash = hash.hash(hash_data, board, to_move) 397 406 407 + // TODO: Maybe we can update this incrementally too? 408 + let attack_information = attack.calculate(board, to_move) 409 + 398 410 Game( 399 411 board:, 400 412 to_move:, ··· 406 418 hash_data:, 407 419 piece_tables:, 408 420 previous_positions:, 421 + attack_information:, 409 422 ) 410 423 } 411 424
+617
src/starfish/internal/move/attack.gleam
··· 1 + import gleam/bool 2 + import gleam/dict.{type Dict} 3 + import gleam/list 4 + import iv 5 + import starfish/internal/board.{type Board} 6 + import starfish/internal/move/direction.{type Direction} 7 + 8 + /// Information about which squares on the board are attacked. 9 + pub type AttackInformation { 10 + AttackInformation( 11 + /// All squares which are immediately attacked by pieces of the specified 12 + /// colour. The king cannot move here, as that would be moving into check. 13 + attacks: List(Int), 14 + /// Whether the king is currently in check. 15 + in_check: Bool, 16 + // TODO: Maybe just merge this with `attacks`? 17 + /// Squares which would become attacked if the king were to move. The king 18 + /// also can't move here. 19 + check_attack_squares: List(Int), 20 + /// Squares which another piece can move to in order to block check. If 21 + /// multiple pieces are delivering check, or the king is not in check, this 22 + /// is empty. 23 + check_block_line: List(Int), 24 + /// A map of pieces to pin lines. To check if a piece can move, first look 25 + /// it up in this map. If it's not present, it is not pinned at all. If it 26 + /// is, the piece can only move to one of the specified squares in the list. 27 + pin_lines: Dict(Int, List(Int)), 28 + ) 29 + } 30 + 31 + pub fn calculate(board: Board, to_move: board.Colour) -> AttackInformation { 32 + // TODO: keep track of this 33 + let assert Ok(king_position) = 34 + iv.find_index(board, fn(square) { 35 + square == board.Occupied(board.King, to_move) 36 + }) 37 + as "Failed to find king on board" 38 + 39 + let attacking = case to_move { 40 + board.Black -> board.White 41 + board.White -> board.Black 42 + } 43 + 44 + let attacks = get_attacks(board, attacking) 45 + 46 + let in_check = list.contains(attacks, king_position) 47 + 48 + let #(check_attack_squares, check_block_line) = case in_check { 49 + False -> #([], []) 50 + True -> #( 51 + get_check_attack_squares(board, attacking, king_position), 52 + get_check_block_line(board, attacking, king_position), 53 + ) 54 + } 55 + 56 + let pin_lines = get_pin_lines(board, attacking, king_position) 57 + 58 + AttackInformation( 59 + attacks:, 60 + in_check:, 61 + check_attack_squares:, 62 + check_block_line:, 63 + pin_lines:, 64 + ) 65 + } 66 + 67 + fn get_pin_lines( 68 + board: Board, 69 + attacking: board.Colour, 70 + king_position: Int, 71 + ) -> Dict(Int, List(Int)) { 72 + use lines, square, position <- iv.index_fold(board, dict.new()) 73 + 74 + case square { 75 + board.Occupied(piece:, colour:) if colour == attacking -> 76 + case piece { 77 + board.Bishop -> 78 + get_sliding_pin_lines( 79 + board, 80 + attacking, 81 + position, 82 + king_position, 83 + direction.bishop_directions, 84 + lines, 85 + ) 86 + board.Queen -> 87 + get_sliding_pin_lines( 88 + board, 89 + attacking, 90 + position, 91 + king_position, 92 + direction.queen_directions, 93 + lines, 94 + ) 95 + board.Rook -> 96 + get_sliding_pin_lines( 97 + board, 98 + attacking, 99 + position, 100 + king_position, 101 + direction.rook_directions, 102 + lines, 103 + ) 104 + _ -> lines 105 + } 106 + _ -> lines 107 + } 108 + } 109 + 110 + fn get_sliding_pin_lines( 111 + board: Board, 112 + attacking: board.Colour, 113 + position: Int, 114 + king_position: Int, 115 + directions: List(Direction), 116 + lines: Dict(Int, List(Int)), 117 + ) -> Dict(Int, List(Int)) { 118 + case directions { 119 + [] -> lines 120 + [direction, ..directions] -> 121 + get_sliding_pin_lines( 122 + board, 123 + attacking, 124 + position, 125 + king_position, 126 + directions, 127 + get_sliding_pin_lines_loop( 128 + board, 129 + attacking, 130 + position, 131 + king_position, 132 + direction, 133 + lines, 134 + [], 135 + -1, 136 + ), 137 + ) 138 + } 139 + } 140 + 141 + fn get_sliding_pin_lines_loop( 142 + board: Board, 143 + attacking: board.Colour, 144 + position: Int, 145 + king_position: Int, 146 + direction: Direction, 147 + lines: Dict(Int, List(Int)), 148 + line: List(Int), 149 + pinned_piece: Int, 150 + ) -> Dict(Int, List(Int)) { 151 + let position = direction.in_direction(position, direction) 152 + 153 + // If we hit the king and we have already passed a piece, that means that piece 154 + // is pinned to the king. 155 + use <- bool.lazy_guard(pinned_piece != -1 && position == king_position, fn() { 156 + dict.insert(lines, pinned_piece, line) 157 + }) 158 + 159 + case iv.get(board, position) { 160 + Ok(board.Empty) -> 161 + get_sliding_pin_lines_loop( 162 + board, 163 + attacking, 164 + position, 165 + king_position, 166 + direction, 167 + lines, 168 + [position, ..line], 169 + pinned_piece, 170 + ) 171 + // If we hit a piece of the opposite colour, and haven't yet encountered any 172 + // pieces, this could be a pin, so we remember the position and continue. 173 + Ok(board.Occupied(colour:, ..)) 174 + if colour != attacking && pinned_piece == -1 175 + -> 176 + get_sliding_pin_lines_loop( 177 + board, 178 + attacking, 179 + position, 180 + king_position, 181 + direction, 182 + lines, 183 + line, 184 + position, 185 + ) 186 + // Otherwise, we have either hit a piece of the same colour, in which case 187 + // it isn't a pin, or we have hit the edge of the board, meaning there is 188 + // also no pin, or we have hit more than one piece of the opposite colour, 189 + // also meaning there is no pin. 190 + _ -> lines 191 + } 192 + } 193 + 194 + type Line { 195 + NoLine 196 + Single(List(Int)) 197 + Multiple 198 + } 199 + 200 + fn get_check_block_line( 201 + board: Board, 202 + attacking: board.Colour, 203 + king_position: Int, 204 + ) -> List(Int) { 205 + // This cursed piece of code just lets us map the final value after all the 206 + // `use` statements. Because of function inlining, this is equivalent to using 207 + // a block, then mapping at the end. 208 + use <- 209 + fn(f) { 210 + let line = f() 211 + case line { 212 + Single(line) -> line 213 + Multiple | NoLine -> [] 214 + } 215 + } 216 + 217 + use line, square, position <- iv.index_fold(board, NoLine) 218 + 219 + // If multiple difference pieces are putting the king in check, it cannot be 220 + // blocked, and the king must move instead. In that case, there's no point 221 + // computing any more attacks, since no matter what, they cannot be blocked. 222 + use <- bool.guard(line == Multiple, line) 223 + 224 + case square { 225 + board.Occupied(piece:, colour:) if colour == attacking -> 226 + case piece { 227 + // If a sliding piece is causing check, moving anywhere in the line 228 + // between that piece and the king (including taking the piece) prevents 229 + // check. 230 + board.Rook -> 231 + case 232 + sliding_check_block_line( 233 + board, 234 + position, 235 + king_position, 236 + direction.rook_directions, 237 + ), 238 + line 239 + { 240 + [], _ -> line 241 + line, NoLine -> Single(line) 242 + _, _ -> Multiple 243 + } 244 + board.Bishop -> 245 + case 246 + sliding_check_block_line( 247 + board, 248 + position, 249 + king_position, 250 + direction.bishop_directions, 251 + ), 252 + line 253 + { 254 + [], _ -> line 255 + line, NoLine -> Single(line) 256 + _, _ -> Multiple 257 + } 258 + board.Queen -> 259 + case 260 + sliding_check_block_line( 261 + board, 262 + position, 263 + king_position, 264 + direction.queen_directions, 265 + ), 266 + line 267 + { 268 + [], _ -> line 269 + line, NoLine -> Single(line) 270 + _, _ -> Multiple 271 + } 272 + // For pieces which only move in a set number of directions, the only way 273 + // to prevent check is to take that piece. 274 + board.King -> 275 + case 276 + piece_attacks_square( 277 + position, 278 + king_position, 279 + direction.queen_directions, 280 + ), 281 + line 282 + { 283 + False, _ -> line 284 + True, NoLine -> Single([position]) 285 + _, _ -> Multiple 286 + } 287 + board.Knight -> 288 + case 289 + piece_attacks_square( 290 + position, 291 + king_position, 292 + direction.knight_directions, 293 + ), 294 + line 295 + { 296 + False, _ -> line 297 + True, NoLine -> Single([position]) 298 + _, _ -> Multiple 299 + } 300 + board.Pawn if attacking == board.Black -> 301 + case 302 + piece_attacks_square( 303 + position, 304 + king_position, 305 + direction.black_pawn_captures, 306 + ), 307 + line 308 + { 309 + False, _ -> line 310 + True, NoLine -> Single([position]) 311 + _, _ -> Multiple 312 + } 313 + board.Pawn -> 314 + case 315 + piece_attacks_square( 316 + position, 317 + king_position, 318 + direction.white_pawn_captures, 319 + ), 320 + line 321 + { 322 + False, _ -> line 323 + True, NoLine -> Single([position]) 324 + _, _ -> Multiple 325 + } 326 + } 327 + _ -> line 328 + } 329 + } 330 + 331 + fn piece_attacks_square( 332 + position: Int, 333 + target: Int, 334 + directions: List(Direction), 335 + ) -> Bool { 336 + case directions { 337 + [] -> False 338 + [direction, ..directions] -> 339 + case direction.in_direction(position, direction) == target { 340 + True -> True 341 + False -> piece_attacks_square(position, target, directions) 342 + } 343 + } 344 + } 345 + 346 + fn sliding_check_block_line( 347 + board: Board, 348 + position: Int, 349 + king_position: Int, 350 + directions: List(Direction), 351 + ) -> List(Int) { 352 + case directions { 353 + [] -> [] 354 + [direction, ..directions] -> 355 + case 356 + sliding_check_block_line_loop( 357 + board, 358 + position, 359 + king_position, 360 + False, 361 + direction, 362 + [position], 363 + ) 364 + { 365 + [] -> 366 + sliding_check_block_line(board, position, king_position, directions) 367 + line -> line 368 + } 369 + } 370 + } 371 + 372 + fn sliding_check_block_line_loop( 373 + board: Board, 374 + position: Int, 375 + king_position: Int, 376 + found_king: Bool, 377 + direction: Direction, 378 + line: List(Int), 379 + ) -> List(Int) { 380 + let new_position = direction.in_direction(position, direction) 381 + use <- bool.guard(new_position == king_position, line) 382 + 383 + case iv.get(board, new_position) { 384 + Ok(board.Empty) -> 385 + sliding_check_block_line_loop( 386 + board, 387 + new_position, 388 + king_position, 389 + found_king, 390 + direction, 391 + [new_position, ..line], 392 + ) 393 + _ -> [] 394 + } 395 + } 396 + 397 + // TODO: We can probably merge this with `get_check_block_line`, to avoid 398 + // iterating the board twice. 399 + /// When the king is in check, it cannot move into any squares that are attacked 400 + /// by other pieces. However, there's another case too. Imagine one row of the 401 + /// board looks like this: 402 + /// 403 + /// ```txt 404 + /// | R | | | | k | B | | | 405 + /// ``` 406 + /// 407 + /// Here, the white bishop isn't protected by the rook. However, if the king were 408 + /// to capture the bishop, the rook now is attacking that square, so the king is 409 + /// still in check. For this reason, we need to calculate all the squares just 410 + /// past the king in a check line, and prevent the king from moving there. 411 + fn get_check_attack_squares( 412 + board: Board, 413 + attacking: board.Colour, 414 + king_position: Int, 415 + ) -> List(Int) { 416 + use squares, square, position <- iv.index_fold(board, []) 417 + 418 + case square { 419 + board.Occupied(piece:, colour:) if colour == attacking -> { 420 + case piece { 421 + board.Bishop -> 422 + get_sliding_check_attack_squares( 423 + board, 424 + position, 425 + king_position, 426 + direction.bishop_directions, 427 + squares, 428 + ) 429 + board.Queen -> 430 + get_sliding_check_attack_squares( 431 + board, 432 + position, 433 + king_position, 434 + direction.queen_directions, 435 + squares, 436 + ) 437 + board.Rook -> 438 + get_sliding_check_attack_squares( 439 + board, 440 + position, 441 + king_position, 442 + direction.rook_directions, 443 + squares, 444 + ) 445 + _ -> squares 446 + } 447 + } 448 + _ -> squares 449 + } 450 + } 451 + 452 + fn get_sliding_check_attack_squares( 453 + board: Board, 454 + position: Int, 455 + king_position: Int, 456 + directions: List(Direction), 457 + squares: List(Int), 458 + ) -> List(Int) { 459 + case directions { 460 + [] -> squares 461 + [direction, ..directions] -> 462 + get_sliding_check_attack_squares( 463 + board, 464 + position, 465 + king_position, 466 + directions, 467 + get_sliding_check_attack_squares_loop( 468 + board, 469 + position, 470 + king_position, 471 + direction, 472 + squares, 473 + ), 474 + ) 475 + } 476 + } 477 + 478 + fn get_sliding_check_attack_squares_loop( 479 + board: Board, 480 + position: Int, 481 + king_position: Int, 482 + direction: Direction, 483 + squares: List(Int), 484 + ) -> List(Int) { 485 + let new_position = direction.in_direction(position, direction) 486 + 487 + case new_position == king_position { 488 + // We've encountered the king. The next square would be attacked if the king 489 + // were to move there. 490 + True -> 491 + case direction.in_direction(new_position, direction) { 492 + // If it's off the board, we don't need to track it 493 + -1 -> squares 494 + position -> [position, ..squares] 495 + } 496 + False -> 497 + case iv.get(board, new_position) { 498 + Ok(board.Empty) -> 499 + get_sliding_check_attack_squares_loop( 500 + board, 501 + new_position, 502 + king_position, 503 + direction, 504 + squares, 505 + ) 506 + // If we hit another piece (or the edge of the board) before the king, 507 + // we can stop. 508 + _ -> squares 509 + } 510 + } 511 + } 512 + 513 + fn get_attacks(board: Board, attacking: board.Colour) -> List(Int) { 514 + use attacks, square, position <- iv.index_fold(board, []) 515 + case square { 516 + board.Occupied(piece:, colour:) if colour == attacking -> 517 + get_attacks_for_piece(board, piece, position, attacking, attacks) 518 + board.Occupied(..) | board.Empty -> attacks 519 + } 520 + } 521 + 522 + fn get_attacks_for_piece( 523 + board: Board, 524 + piece: board.Piece, 525 + position: Int, 526 + colour: board.Colour, 527 + positions: List(Int), 528 + ) -> List(Int) { 529 + case piece { 530 + board.Bishop -> 531 + get_sliding_attacks( 532 + board, 533 + position, 534 + direction.bishop_directions, 535 + positions, 536 + ) 537 + board.Queen -> 538 + get_sliding_attacks( 539 + board, 540 + position, 541 + direction.queen_directions, 542 + positions, 543 + ) 544 + board.Rook -> 545 + get_sliding_attacks(board, position, direction.rook_directions, positions) 546 + board.King -> 547 + get_single_move_attacks(position, positions, direction.queen_directions) 548 + board.Pawn if colour == board.Black -> 549 + get_single_move_attacks( 550 + position, 551 + positions, 552 + direction.black_pawn_captures, 553 + ) 554 + board.Pawn -> 555 + get_single_move_attacks( 556 + position, 557 + positions, 558 + direction.white_pawn_captures, 559 + ) 560 + board.Knight -> 561 + get_single_move_attacks(position, positions, direction.knight_directions) 562 + } 563 + } 564 + 565 + fn get_sliding_attacks( 566 + board: Board, 567 + position: Int, 568 + directions: List(Direction), 569 + positions: List(Int), 570 + ) -> List(Int) { 571 + case directions { 572 + [] -> positions 573 + [direction, ..directions] -> 574 + get_sliding_attacks( 575 + board, 576 + position, 577 + directions, 578 + get_sliding_attacks_loop(board, position, direction, positions), 579 + ) 580 + } 581 + } 582 + 583 + fn get_sliding_attacks_loop( 584 + board: Board, 585 + position: Int, 586 + direction: Direction, 587 + positions: List(Int), 588 + ) -> List(Int) { 589 + let new_position = direction.in_direction(position, direction) 590 + 591 + case iv.get(board, new_position) { 592 + Error(Nil) -> positions 593 + Ok(board.Empty) -> 594 + get_sliding_attacks_loop(board, new_position, direction, [ 595 + new_position, 596 + ..positions 597 + ]) 598 + _ -> [new_position, ..positions] 599 + } 600 + } 601 + 602 + fn get_single_move_attacks( 603 + position: Int, 604 + positions: List(Int), 605 + directions: List(Direction), 606 + ) -> List(Int) { 607 + case directions { 608 + [] -> positions 609 + [direction, ..directions] -> { 610 + let positions = case direction.in_direction(position, direction) { 611 + -1 -> positions 612 + position -> [position, ..positions] 613 + } 614 + get_single_move_attacks(position, positions, directions) 615 + } 616 + } 617 + }
+4
src/starfish/internal/move/direction.gleam
··· 54 54 down_right, 55 55 ] 56 56 57 + pub const white_pawn_captures = [up_left, up_right] 58 + 59 + pub const black_pawn_captures = [up_left, up_right] 60 + 57 61 pub const knight_directions = [ 58 62 Direction(-1, -2), 59 63 Direction(1, -2),