Current section
Files
Jump to
Current section
Files
src/move_san.gleam
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import gleam/string_builder
import piece.{type Kind}
import position.{type File, type Position, type Rank}
pub type MoveSan {
Normal(
from: Option(PositionSan),
to: Position,
moving_piece: Kind,
capture: Bool,
promotion: Option(Kind),
maybe_check_or_checkmate: Option(CheckOrCheckMate),
)
Castle(side: CastleSide, maybe_check_or_checkmate: Option(CheckOrCheckMate))
EnPassant(
from: Option(PositionSan),
to: Position,
maybe_check_or_checkmate: Option(CheckOrCheckMate),
)
}
pub type PositionSan {
PositionSan(file: Option(File), rank: Option(Rank))
}
pub type ErrorSan {
InvalidMoveString
InvalidCastleString
InvalidPositionalInformation
}
pub type CastleSide {
KingSide
QueenSide
}
pub type CheckOrCheckMate {
Check
CheckMate
}
pub fn from_string(san: String) -> Result(MoveSan, ErrorSan) {
case string.to_graphemes(san) {
[] -> panic as "Cannot parse empty string."
[piece_letter, ..rest] if piece_letter == "K"
|| piece_letter == "Q"
|| piece_letter == "R"
|| piece_letter == "B"
|| piece_letter == "N" -> {
let check_or_checkmate = case list.last(rest) {
Ok("+") -> Some(Check)
Ok("#") -> Some(CheckMate)
_ -> None
}
let moving_piece = case piece_letter {
"K" -> piece.King
"Q" -> piece.Queen
"R" -> piece.Rook
"B" -> piece.Bishop
"N" -> piece.Knight
_ -> panic as "Invalid piece letter."
}
let promotion = None
let capture = list.contains(rest, "x")
let positional_information =
list.filter(rest, fn(grapheme) {
grapheme != "+" && grapheme != "#" && grapheme != "x"
})
case list.length(positional_information) {
4 -> {
let assert [from_file, from_rank, to_file, to_rank] =
positional_information
let from_file = case from_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let from_rank = case from_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
Ok(Normal(
from: Some(PositionSan(file: Some(from_file), rank: Some(from_rank))),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: moving_piece,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: check_or_checkmate,
))
}
3 -> {
let assert [maybe_from_file_or_from_rank, to_file, to_rank] =
positional_information
case maybe_from_file_or_from_rank {
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" -> {
let from_file = case maybe_from_file_or_from_rank {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file"
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
Ok(Normal(
from: Some(PositionSan(file: Some(from_file), rank: None)),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: moving_piece,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: check_or_checkmate,
))
}
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" -> {
let from_rank = case maybe_from_file_or_from_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
Ok(Normal(
from: Some(PositionSan(file: None, rank: Some(from_rank))),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: moving_piece,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: check_or_checkmate,
))
}
_ -> Error(InvalidPositionalInformation)
}
}
2 -> {
let assert [to_file, to_rank] = positional_information
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
Ok(Normal(
from: None,
to: position.Position(file: to_file, rank: to_rank),
moving_piece: moving_piece,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: check_or_checkmate,
))
}
_ -> {
Error(InvalidPositionalInformation)
}
}
}
["O", ..rest] | ["0", ..rest] -> {
case rest {
["-", "O", "-", "O", ..checks_or_checkmates]
| ["-", "0", "-", "0", ..checks_or_checkmates] -> {
case checks_or_checkmates {
[] -> {
Ok(Castle(side: QueenSide, maybe_check_or_checkmate: None))
}
["+", ..] -> {
Ok(Castle(side: QueenSide, maybe_check_or_checkmate: Some(Check)))
}
["#", ..] -> {
Ok(Castle(
side: QueenSide,
maybe_check_or_checkmate: Some(CheckMate),
))
}
_ -> Error(InvalidCastleString)
}
}
["-", "O", ..checks_or_checkmates] | ["-", "0", ..checks_or_checkmates] -> {
case checks_or_checkmates {
[] -> {
Ok(Castle(side: KingSide, maybe_check_or_checkmate: None))
}
["+", ..] -> {
Ok(Castle(side: KingSide, maybe_check_or_checkmate: Some(Check)))
}
["#", ..] -> {
Ok(Castle(
side: KingSide,
maybe_check_or_checkmate: Some(CheckMate),
))
}
_ -> Error(InvalidCastleString)
}
}
_ -> Error(InvalidCastleString)
}
}
[pawn_move_first_grapheme, ..rest] if pawn_move_first_grapheme == "a"
|| pawn_move_first_grapheme == "b"
|| pawn_move_first_grapheme == "c"
|| pawn_move_first_grapheme == "d"
|| pawn_move_first_grapheme == "e"
|| pawn_move_first_grapheme == "f"
|| pawn_move_first_grapheme == "g"
|| pawn_move_first_grapheme == "h" -> {
let is_en_passant =
string.contains(
string_builder.to_string(string_builder.from_strings(rest)),
"e.p.",
)
let rest =
string.replace(
string_builder.to_string(string_builder.from_strings(rest)),
"e.p.",
"",
)
let rest = string.trim(rest)
let rest = string.to_graphemes(rest)
let capture = list.contains(rest, "x")
let rest = list.filter(rest, fn(grapheme) { grapheme != "x" })
let #(promotion_segment, rest) =
list.partition([pawn_move_first_grapheme, ..rest], fn(grapheme) {
case grapheme {
"=" | "Q" | "R" | "B" | "N" -> True
_ -> False
}
})
let promotion = case promotion_segment {
[] -> None
["=", "Q", ..] -> Some(piece.Queen)
["=", "R", ..] -> Some(piece.Rook)
["=", "B", ..] -> Some(piece.Bishop)
["=", "N", ..] -> Some(piece.Knight)
_ -> panic as "Invalid promotion segment."
}
let maybe_check_or_checkmate = case list.last(rest) {
Ok("+") -> Some(Check)
Ok("#") -> Some(CheckMate)
_ -> None
}
let positional_information =
list.filter(rest, fn(grapheme) { grapheme != "+" && grapheme != "#" })
case list.length(positional_information) {
4 -> {
let assert [from_file, from_rank, to_file, to_rank] =
positional_information
let from_file = case from_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let from_rank = case from_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
case is_en_passant {
True -> {
Ok(EnPassant(
from: Some(PositionSan(
file: Some(from_file),
rank: Some(from_rank),
)),
to: position.Position(file: to_file, rank: to_rank),
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
False -> {
Ok(Normal(
from: Some(PositionSan(
file: Some(from_file),
rank: Some(from_rank),
)),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: piece.Pawn,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
}
}
3 -> {
let assert [maybe_from_file_or_from_rank, to_file, to_rank] =
positional_information
case maybe_from_file_or_from_rank {
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" -> {
let from_file = case maybe_from_file_or_from_rank {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file"
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
case is_en_passant {
True -> {
Ok(EnPassant(
from: Some(PositionSan(file: Some(from_file), rank: None)),
to: position.Position(file: to_file, rank: to_rank),
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
False -> {
Ok(Normal(
from: Some(PositionSan(file: Some(from_file), rank: None)),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: piece.Pawn,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
}
}
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" -> {
let from_rank = case maybe_from_file_or_from_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
case is_en_passant {
True -> {
Ok(EnPassant(
from: Some(PositionSan(file: None, rank: Some(from_rank))),
to: position.Position(file: to_file, rank: to_rank),
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
False -> {
Ok(Normal(
from: Some(PositionSan(file: None, rank: Some(from_rank))),
to: position.Position(file: to_file, rank: to_rank),
moving_piece: piece.Pawn,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
}
}
_ -> Error(InvalidPositionalInformation)
}
}
2 -> {
let assert [to_file, to_rank] = positional_information
let to_file = case to_file {
"a" -> position.A
"b" -> position.B
"c" -> position.C
"d" -> position.D
"e" -> position.E
"f" -> position.F
"g" -> position.G
"h" -> position.H
_ -> panic as "Invalid file."
}
let to_rank = case to_rank {
"1" -> position.One
"2" -> position.Two
"3" -> position.Three
"4" -> position.Four
"5" -> position.Five
"6" -> position.Six
"7" -> position.Seven
"8" -> position.Eight
_ -> panic as "Invalid rank."
}
case is_en_passant {
True -> {
Ok(EnPassant(
from: None,
to: position.Position(file: to_file, rank: to_rank),
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
False -> {
Ok(Normal(
from: None,
to: position.Position(file: to_file, rank: to_rank),
moving_piece: piece.Pawn,
capture: capture,
promotion: promotion,
maybe_check_or_checkmate: maybe_check_or_checkmate,
))
}
}
}
_ -> Error(InvalidPositionalInformation)
}
}
_ -> Error(InvalidMoveString)
}
}