···88pub type Game =
99 game.Game
10101111-/// A single move on the chess board. `Move(Legal)` represents a move which has
1212-/// been verified to be legal for a particular position. `Move(Valid)` is a move
1313-/// which is syntactically valid, but is not necessarily legal to play.
1414-pub type Move(validity) =
1515- move.Move(validity)
1616-1717-pub type Legal =
1818- move.Legal
1919-2020-pub type Valid =
2121- move.Valid
1111+/// A single legal move on the chess board.
1212+pub type Move =
1313+ move.Move
22142315/// The [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
2416/// representing the initial position of a chess game.
···2820/// the input, meaning if a FEN string is partially incomplete (e.g. missing the
2921/// half-move and full-move counters at the end), it will fill it in with the
3022/// default values of the starting position.
3131-///
2323+///
3224/// For strict parsing, see [`try_from_fen`](#try_from_fen).
3333-///
2525+///
3426/// ## Examples
3535-///
2727+///
3628/// The following expressions are all equivalent:
3737-///
2929+///
3830/// ```gleam
3931/// starfish.new()
4032/// starfish.from_fen(starfish.starting_fen)
···7668/// Tries to parse a game from a FEN string, returning an error if it doesn't
7769/// follow standard FEN notation. For more lenient parsing, see [`from_fen`](
7870/// #from_fen).
7979-///
7171+///
8072/// ## Examples
8181-///
7373+///
8274/// ```gleam
8375/// let assert Ok(start_pos) = starfish.try_from_fen(starfish.starting_fen)
8476/// assert start_pos == starfish.new()
8585-///
7777+///
8678/// let assert Error(starfish.ExpectedSpaceAfterSegment) =
8779/// starfish.try_from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")
8880/// ```
···112104}
113105114106/// Convert a game into its FEN string representation.
115115-///
107107+///
116108/// ## Examples
117117-///
109109+///
118110/// ```gleam
119111/// assert starfish.to_fen(starfish.new()) == starfish.starting_fen
120112/// ```
···122114 game.to_fen(game)
123115}
124116125125-pub fn legal_moves(game: Game) -> List(Move(Legal)) {
117117+pub fn legal_moves(game: Game) -> List(Move) {
126118 move.legal(game)
127119}
128120129129-pub fn search(game: Game, to_depth depth: Int) -> Result(Move(Legal), Nil) {
121121+pub fn search(game: Game, to_depth depth: Int) -> Result(Move, Nil) {
130122 search.best_move(game, depth)
131123}
132124133133-pub fn apply_move(game: Game, move: Move(Legal)) -> Game {
125125+pub fn apply_move(game: Game, move: Move) -> Game {
134126 move.apply(game, move)
135127}
136128137137-pub fn to_standard_algebraic_notation(move: Move(a)) -> String {
129129+pub fn to_standard_algebraic_notation(move: Move) -> String {
138130 todo
139131}
140132···142134/// https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Long_algebraic_notation),
143135/// specifically the UCI format, containing the start and end positions. For
144136/// example, `e2e4` or `c7d8q`.
145145-pub fn to_long_algebraic_notation(move: Move(a)) -> String {
137137+pub fn to_long_algebraic_notation(move: Move) -> String {
146138 move.to_long_algebraic_notation(move)
147139}
148140149141/// Parses a move from long algebraic notation, in the same format as
150150-/// [`to_long_algebraic_notation`](#to_long_algebraic_notation).
151151-pub fn parse_long_algebraic_notation(string: String) -> Result(Move(Valid), Nil) {
152152- move.from_long_algebraic_notation(string)
142142+/// [`to_long_algebraic_notation`](#to_long_algebraic_notation). Returns an error
143143+/// if the syntax is invalid or the move is not legal.
144144+pub fn parse_long_algebraic_notation(
145145+ string: String,
146146+ game: Game,
147147+) -> Result(Move, Nil) {
148148+ move.from_long_algebraic_notation(string, game)
153149}
154150155155-pub fn parse_move(move: String, game: Game) -> Result(Move(Valid), Nil) {
156156- todo
157157-}
158158-159159-pub fn parse_legal_move(string: String, game: Game) -> Result(Move(Legal), Nil) {
160160- string |> parse_long_algebraic_notation |> result.try(validate_move(_, game))
161161-}
162162-163163-pub fn validate_move(move: Move(a), game: Game) -> Result(Move(Legal), Nil) {
151151+pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) {
164152 todo
165153}
166154
+1-4
src/starfish/internal/evaluate.gleam
···7788/// Statically evaluates a position. Does not take into account checkmate or
99/// stalemate, those must be accounted for beforehand.
1010-pub fn evaluate(
1111- game: game.Game,
1212- legal_moves: List(move.Move(move.Legal)),
1313-) -> Int {
1010+pub fn evaluate(game: game.Game, legal_moves: List(move.Move)) -> Int {
1411 evaluate_position(game) + list.length(legal_moves)
1512}
1613
+53-38
src/starfish/internal/move.gleam
···1010import starfish/internal/move/attack
1111import starfish/internal/move/direction.{type Direction}
12121313-pub type Valid
1414-1515-pub type Legal
1616-1717-pub type Move(validity) {
1313+pub type Move {
1814 Castle(from: Int, to: Int)
1915 Move(from: Int, to: Int)
2016 Capture(from: Int, to: Int)
···2218 Promotion(from: Int, to: Int, piece: board.Piece)
2319}
24202525-pub fn legal(game: Game) -> List(Move(Legal)) {
2121+pub fn legal(game: Game) -> List(Move) {
2622 use moves, position, #(piece, colour) <- dict.fold(game.board, [])
27232824 case colour == game.to_move {
···8278 game: Game,
8379 position: Int,
8480 piece: board.Piece,
8585- moves: List(Move(Legal)),
8686-) -> List(Move(Legal)) {
8181+ moves: List(Move),
8282+) -> List(Move) {
8783 case piece {
8884 board.Bishop ->
8985 sliding_moves(game, position, moves, direction.bishop_directions)
···9894 }
9995}
10096101101-fn pawn_moves(
102102- game: Game,
103103- position: Int,
104104- moves: List(Move(Legal)),
105105-) -> List(Move(Legal)) {
9797+fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) {
10698 let #(forward, left, right, promotion_rank) = case game.to_move {
10799 board.Black -> #(
108100 direction.down,
···216208/// En passant needs to be checked slightly different to other moves. For example,
217209/// if a row of the board looks something like this, after the black pawn having
218210/// moved two squares:
219219-///
211211+///
220212/// ```txt
221213/// | K | | p | P | | | r | |
222214/// ```
223223-///
215215+///
224216/// Here, the white pawn is not pinned by the black rook, so it can safely move
225217/// forwards. However, if it were to perform en passant and capture the black
226218/// pawn, the king would be in check. Here, we check for this case.
227227-///
219219+///
228220/// To perform the check, we cast out rays on either side of the en passant pair.
229221/// If we hit the king, as well as a rook or a queen of the opposite colour, then
230222/// en passant is not valid.
231231-///
223223+///
232224fn in_check_after_en_passant(
233225 game: Game,
234226 position: Int,
···296288fn add_promotions(
297289 from: Int,
298290 to: Int,
299299- moves: List(Move(Legal)),
291291+ moves: List(Move),
300292 pieces: List(board.Piece),
301301-) -> List(Move(Legal)) {
293293+) -> List(Move) {
302294 case pieces {
303295 [] -> moves
304296 [piece, ..pieces] ->
···309301fn knight_moves(
310302 game: Game,
311303 position: Int,
312312- moves: List(Move(Legal)),
304304+ moves: List(Move),
313305 directions: List(Direction),
314314-) -> List(Move(Legal)) {
306306+) -> List(Move) {
315307 case directions {
316308 [] -> moves
317309 [direction, ..directions] -> {
···338330fn king_moves(
339331 game: Game,
340332 position: Int,
341341- moves: List(Move(Legal)),
333333+ moves: List(Move),
342334 directions: List(Direction),
343343-) -> List(Move(Legal)) {
335335+) -> List(Move) {
344336 let moves = regular_king_moves(game, position, moves, directions)
345337346338 // If we're in check, castling is not valid.
···385377fn regular_king_moves(
386378 game: Game,
387379 position: Int,
388388- moves: List(Move(Legal)),
380380+ moves: List(Move),
389381 directions: List(Direction),
390390-) -> List(Move(Legal)) {
382382+) -> List(Move) {
391383 case directions {
392384 [] -> moves
393385 [direction, ..directions] -> {
···414406fn sliding_moves(
415407 game: Game,
416408 position: Int,
417417- moves: List(Move(Legal)),
409409+ moves: List(Move),
418410 directions: List(Direction),
419419-) -> List(Move(Legal)) {
411411+) -> List(Move) {
420412 case directions {
421413 [] -> moves
422414 [direction, ..directions] ->
···434426 start_position: Int,
435427 position: Int,
436428 direction: Direction,
437437- moves: List(Move(Legal)),
438438-) -> List(Move(Legal)) {
429429+ moves: List(Move),
430430+) -> List(Move) {
439431 let new_position = direction.in_direction(position, direction)
440432 case board.get(game.board, new_position) {
441433 board.Empty ->
···458450 }
459451}
460452461461-pub fn apply(game: Game, move: Move(Legal)) -> game.Game {
453453+pub fn apply(game: Game, move: Move) -> game.Game {
462454 case move {
463455 Capture(from:, to:) -> do_apply(game, from, to, False, None, True)
464456 Castle(from:, to:) -> apply_castle(game, from, to, board.file(to) == 2)
···644636 }
645637}
646638647647-pub fn to_long_algebraic_notation(move: Move(a)) -> String {
639639+pub fn to_long_algebraic_notation(move: Move) -> String {
648640 let from = board.position_to_string(move.from)
649641 let to = board.position_to_string(move.to)
650642 let extra = case move {
···663655 from <> to <> extra
664656}
665657666666-pub fn from_long_algebraic_notation(string: String) -> Result(Move(Valid), Nil) {
658658+// TODO: can we do this without calculating every legal move? We can probably just
659659+// calculate legal moves for the specific square we need.
660660+pub fn from_long_algebraic_notation(
661661+ string: String,
662662+ game: Game,
663663+) -> Result(Move, Nil) {
667664 use #(from, string) <- result.try(board.parse_position(string))
668665 use #(to, string) <- result.try(board.parse_position(string))
669669- case string {
670670- "" -> Ok(Move(from:, to:))
671671- "b" | "B" -> Ok(Promotion(from:, to:, piece: board.Bishop))
672672- "n" | "N" -> Ok(Promotion(from:, to:, piece: board.Knight))
673673- "q" | "Q" -> Ok(Promotion(from:, to:, piece: board.Queen))
674674- "r" | "R" -> Ok(Promotion(from:, to:, piece: board.Rook))
666666+ use promotion_piece <- result.try(case string {
667667+ "" -> Ok(None)
668668+ "b" | "B" -> Ok(Some(board.Bishop))
669669+ "n" | "N" -> Ok(Some(board.Knight))
670670+ "q" | "Q" -> Ok(Some(board.Queen))
671671+ "r" | "R" -> Ok(Some(board.Rook))
672672+ _ -> Error(Nil)
673673+ })
674674+675675+ let legal_moves = legal(game)
676676+677677+ let valid_moves =
678678+ list.filter(legal_moves, fn(move) {
679679+ move.from == from
680680+ && move.to == to
681681+ && case move, promotion_piece {
682682+ Promotion(..), None -> False
683683+ Promotion(piece:, ..), Some(promotion) -> piece == promotion
684684+ _, _ -> promotion_piece == None
685685+ }
686686+ })
687687+688688+ case valid_moves {
689689+ [move] -> Ok(move)
675690 _ -> Error(Nil)
676691 }
677692}
+3-3
src/starfish/internal/move/attack.gleam
···390390/// When the king is in check, it cannot move into any squares that are attacked
391391/// by other pieces. However, there's another case too. Imagine one row of the
392392/// board looks like this:
393393-///
393393+///
394394/// ```txt
395395/// | R | | | | k | B | | |
396396/// ```
397397-///
397397+///
398398/// Here, the white bishop isn't protected by the rook. However, if the king were
399399/// to capture the bishop, the rook now is attacking that square, so the king is
400400/// still in check. For this reason, we need to calculate all the squares just
401401-/// past the king in a check line, and prevent the king from moving there.
401401+/// past the king in a check line, and prevent the king from moving there.
402402fn get_check_attack_squares(
403403 board: Board,
404404 attacking: board.Colour,
+2-5
src/starfish/internal/search.gleam
···33import starfish/internal/evaluate
44import starfish/internal/game.{type Game}
55import starfish/internal/hash
66-import starfish/internal/move
66+import starfish/internal/move.{type Move}
7788-/// Not really infinity, but a high enough number that nothing but explicit
88+/// Not really infinity, but a high enough number that nothing but explicit
99/// references to it will reach it.
1010const infinity = 1_000_000_000
11111212const checkmate = -1_000_000
1313-1414-type Move =
1515- move.Move(move.Legal)
16131714pub fn best_move(game: Game, depth: Int) -> Result(Move, Nil) {
1815 use <- bool.guard(depth < 1, Error(Nil))
+89-23
test/starfish_test.gleam
···8080}
81818282pub fn parse_long_algebraic_notation_test() {
8383- let assert Ok(move) = starfish.parse_long_algebraic_notation("a2a4")
8383+ let assert Ok(move) =
8484+ starfish.parse_long_algebraic_notation("a2a4", starfish.new())
8485 assert move == move.Move(from: 8, to: 24)
8585- let assert Ok(move) = starfish.parse_long_algebraic_notation("g1f3")
8686+ let assert Ok(move) =
8787+ starfish.parse_long_algebraic_notation("g1f3", starfish.new())
8688 assert move == move.Move(from: 6, to: 21)
8787- let assert Ok(move) = starfish.parse_long_algebraic_notation("b8c6")
8989+ let assert Ok(move) =
9090+ starfish.parse_long_algebraic_notation(
9191+ "b8c6",
9292+ starfish.from_fen(
9393+ "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
9494+ ),
9595+ )
8896 assert move == move.Move(from: 57, to: 42)
8989- let assert Ok(move) = starfish.parse_long_algebraic_notation("B7b5")
9797+ let assert Ok(move) =
9898+ starfish.parse_long_algebraic_notation(
9999+ "B7b5",
100100+ starfish.from_fen(
101101+ "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
102102+ ),
103103+ )
90104 assert move == move.Move(from: 49, to: 33)
9191- let assert Ok(move) = starfish.parse_long_algebraic_notation("a5b6")
9292- assert move == move.Move(from: 32, to: 41)
9393- let assert Ok(move) = starfish.parse_long_algebraic_notation("e1G1")
9494- assert move == move.Move(from: 4, to: 6)
9595- let assert Ok(move) = starfish.parse_long_algebraic_notation("e1c1")
9696- assert move == move.Move(from: 4, to: 2)
9797- let assert Ok(move) = starfish.parse_long_algebraic_notation("e8g8")
9898- assert move == move.Move(from: 60, to: 62)
9999- let assert Ok(move) = starfish.parse_long_algebraic_notation("E8C8")
100100- assert move == move.Move(from: 60, to: 58)
101101- let assert Ok(move) = starfish.parse_long_algebraic_notation("d7c8q")
105105+ let assert Ok(move) =
106106+ starfish.parse_long_algebraic_notation(
107107+ "a5b6",
108108+ starfish.from_fen(
109109+ "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
110110+ ),
111111+ )
112112+ assert move == move.EnPassant(from: 32, to: 41)
113113+ let assert Ok(move) =
114114+ starfish.parse_long_algebraic_notation(
115115+ "e1G1",
116116+ starfish.from_fen(
117117+ "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
118118+ ),
119119+ )
120120+ assert move == move.Castle(from: 4, to: 6)
121121+ let assert Ok(move) =
122122+ starfish.parse_long_algebraic_notation(
123123+ "e1c1",
124124+ starfish.from_fen(
125125+ "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
126126+ ),
127127+ )
128128+ assert move == move.Castle(from: 4, to: 2)
129129+ let assert Ok(move) =
130130+ starfish.parse_long_algebraic_notation(
131131+ "e8g8",
132132+ starfish.from_fen(
133133+ "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
134134+ ),
135135+ )
136136+ assert move == move.Castle(from: 60, to: 62)
137137+ let assert Ok(move) =
138138+ starfish.parse_long_algebraic_notation(
139139+ "E8C8",
140140+ starfish.from_fen(
141141+ "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
142142+ ),
143143+ )
144144+ assert move == move.Castle(from: 60, to: 58)
145145+ let assert Ok(move) =
146146+ starfish.parse_long_algebraic_notation(
147147+ "d7c8q",
148148+ starfish.from_fen(
149149+ "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
150150+ ),
151151+ )
102152 assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
103103- let assert Ok(move) = starfish.parse_long_algebraic_notation("d2c1N")
153153+ let assert Ok(move) =
154154+ starfish.parse_long_algebraic_notation(
155155+ "d2c1N",
156156+ starfish.from_fen(
157157+ "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
158158+ ),
159159+ )
104160 assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
105105- let assert Ok(move) = starfish.parse_long_algebraic_notation("b7h1")
106106- assert move == move.Move(from: 49, to: 7)
161161+ let assert Ok(move) =
162162+ starfish.parse_long_algebraic_notation(
163163+ "b7h1",
164164+ starfish.from_fen(
165165+ "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
166166+ ),
167167+ )
168168+ assert move == move.Capture(from: 49, to: 7)
107169108108- let assert Error(Nil) = starfish.parse_long_algebraic_notation("abcd")
109109- let assert Error(Nil) = starfish.parse_long_algebraic_notation("e2e4extra")
110110- let assert Error(Nil) = starfish.parse_long_algebraic_notation("e2")
111111- let assert Error(Nil) = starfish.parse_long_algebraic_notation("Bxe4")
170170+ let assert Error(Nil) =
171171+ starfish.parse_long_algebraic_notation("abcd", starfish.new())
172172+ let assert Error(Nil) =
173173+ starfish.parse_long_algebraic_notation("e2e4extra", starfish.new())
174174+ let assert Error(Nil) =
175175+ starfish.parse_long_algebraic_notation("e2", starfish.new())
176176+ let assert Error(Nil) =
177177+ starfish.parse_long_algebraic_notation("Bxe4", starfish.new())
112178}
113179114180pub fn state_test() {
···376442377443fn test_apply_move(
378444 starting_fen: String,
379379- moves: List(move.Move(move.Legal)),
445445+ moves: List(move.Move),
380446 expected_fen: String,
381447) {
382448 let final_fen =