Games

Chess rules

Chess rules through shakmaty: FEN read and written, legal moves in SAN and UCI, moves played and checked, PGN games read with tags, comments and final positions, an illegal move named, game state, perft, and Chess960 start positions.

Version 1.0.0 · 10 functions · 3 crates

#games#chess#pgn#fen

Functions

What use chess.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use chess_position, chess_moves from chess (Quillo 0.6).

chess_position(fen: str) → anyor an error

A position's state from FEN (empty for the start): { fen, turn, check, checkmate, stalemate, insufficient_material, game_over, result, reason, halfmove_clock, fullmove, fifty_move_claim, legal_moves, en_passant }. FEN that cannot be or a position that cannot arise is an error.

chess_moves(fen: str) → listor an error

Every legal move: [{ san, uci, from, to, piece, capture, promotion, castle, en_passant }]. chess_moves("") lists the 20 of the start.

chess_play(fen: str, mv: str) → anyor an error

Plays a move, SAN (Nf3) or UCI (g1f3): { san, uci, ..., position }. An illegal move is an error saying so.

chess_line(fen: str, moves: list) → anyor an error

Plays a list of moves from fen: { san, uci, position }, the moves as both notations. An illegal one is an error naming its number, such as "move 3 (white)".

chess_perft(fen: str, depth: int) → intor an error

Nodes of a move tree depth plies deep, the standard test of a move generator: from the start, depth 4 is 197281.

chess_board(fen: str) → stror an error

A position as text, rank 8 first, for logs and terminals.

chess_960(number: int) → anyor an error

The FEN of a Chess960 start position, 0 to 959, by Scharnagl's numbering: 518 is the usual start. Its castling is written X-FEN style, with KQkq.

chess_pgn(text: str) → listor an error

Reads games from PGN text: [{ tags, moves: [{ number, color, san, uci }], comments, variations, result, fen, position, error }]. Each move is checked against the rules, from the FEN tag where there is one. A game with an illegal move stops there, with error naming the move, and the rest are still read. Side lines are counted and skipped.

chess_pgn_write(tags: map, moves: list, result: str) → stror an error

PGN text for a game: tags a map, moves SAN or UCI, result 1-0, 0-1, 1/2-1/2 or *. The seven standard tags come first, moves are checked, lines wrap at 80 columns. A FEN tag starts from that position.

chess_fen_check(fen: str) → any

Whether a text is FEN that can be played: { ok, reason }.

Get it

$ quillo install chess               # chess.qwer into this folder$ quillo install chess --example     # and its example, chess.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/chess.qwer$ curl -O https://quillolang.org/plugins/files/chess.qwe

Then, in your program, all of it or only what you call:

main.qwe
use chess.qweruse chess_position, chess_moves from chess      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }shakmaty = "=0.30.2"pgn-reader = "=0.29.1"

Example

chess.qwe
// A chess server's rules: a position read from FEN, its legal moves, moves played and refused,// a PGN game read back, perft to prove the move generator, and Chess960 start positions.////   quillo run chess.qwe      then open http://localhost:8080/start, /moves, /play, /mate, /stalemate, /pgn, /write, /960 and /refuseduse "chess.qwer"let KIWIPETE = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"route GET /start = {    position: chess_position("")?,    perft: [chess_perft("", 3)?, chess_perft("", 4)?, chess_perft(KIWIPETE, 3)?],    board: chess_board("")?,}route GET /moves {    let m = chess_moves(KIWIPETE)?    {        count: len(m),        castles: m | filter((x) => x.castle != null) | map((x) => [x.san, x.uci, x.castle]),        captures: len(m | filter((x) => x.capture != null)),        promotion: find(chess_moves("8/P6k/8/8/8/8/8/K7 w - - 0 1")?, (x) => x.promotion == "knight"),        en_passant: find(chess_moves("rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 3")?, (x) => x.en_passant),    }}route GET /play {    let a = chess_play("", "e4")?    let b = chess_play(a.position.fen, "c7c5")?    {        first: [a.san, a.uci, a.piece],        second: [b.san, b.uci],        fen: b.position.fen,        line: chess_line("", ["e4", "e5", "Nf3", "Nc6", "Bb5"])?.uci,        as_uci: chess_line("", ["e2e4", "e7e5", "g1f3"])?.san,    }}// Scholar's mate, a stalemate, bare kings.route GET /mate = chess_line("", ["e4", "e5", "Bc4", "Nc6", "Qh5", "Nf6", "Qxf7#"])?route GET /stalemate = {    stalemate: chess_position("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1")?,    bare: chess_position("8/8/8/4k3/8/4K3/8/8 w - - 0 1")?.reason,    fifty: chess_position("8/8/8/4k3/8/4K3/7R/8 w - - 100 80")?.fifty_move_claim,}let PGN = `[Event "Casual game"][Site "London"][Date "1851.06.21"][White "Anderssen, Adolf"][Black "Kieseritzky, Lionel"][Result "1-0"][Annotator "A \\"quoted\\" name"]{The Immortal Game, its start.}1. e4 e5 2. f4 $1 exf4 {The gambit taken} 3. Bc4 Qh4+ (3... d5 4. Bxd5) 4. Kf1 b5?! 1-0[Event "Second"][White "A"][Black "B"][Result "*"]1. e4 e5 2. Ke3 *[Event "From a position"][FEN "7k/8/8/8/8/8/8/R3K3 w Q - 0 1"][SetUp "1"][Result "1/2-1/2"]1. O-O-O Kg8 1/2-1/2`route GET /pgn {    let g = chess_pgn(PGN)?    {        games: len(g),        tags: g[0].tags,        moves: g[0].moves | map((m) => m.san),        comments: g[0].comments, intro: g[0].intro, side_lines: g[0].variations, result: g[0].result,        after: g[0].position.fen,        illegal: g[1].error, kept: len(g[1].moves),        from_fen: g[2].moves | map((m) => [m.number, m.color, m.uci]),    }}route GET /write {    let text = chess_pgn_write({ White: "Ada", Black: "Grace", Date: "2026.11.01", Opening: "Sicilian" }, ["e4", "c5", "Nf3"], "*")?    let back = chess_pgn(text)?[0]    { text: text, back: back.moves | map((m) => m.uci), opening: back.tags.Opening }}route GET /960 = {    usual: chess_960(518)?,    first: chess_960(0)?,    last: chess_960(959)?,    another: chess_960(100)?.pieces,    castle: chess_moves("bqnbrkrn/pppppppp/8/8/8/8/PPPPPPPP/BQNBRKRN w KQkq - 0 1")? | filter((m) => m.castle != null),    out_of_range: chess_960(960).error,}route GET /refused = {    illegal: chess_play("", "e5").error,    nonsense: chess_play("", "hello").error,    line: chess_line("", ["e4", "e5", "Ke3"]).error,    bad_fen: chess_position("not a fen").error,    two_kings: chess_fen_check("rnbqkbnk/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"),    no_king: chess_fen_check("8/8/8/8/8/8/8/8 w - - 0 1").ok,    deep: chess_perft("", 7).error,    result: chess_pgn_write({}, [], "win").error,}
$ quillo run chess.qwe$ curl 'localhost:8080/start'$ curl 'localhost:8080/moves'$ curl 'localhost:8080/play'$ curl 'localhost:8080/mate'$ curl 'localhost:8080/stalemate'$ curl 'localhost:8080/pgn'$ curl 'localhost:8080/write'$ curl 'localhost:8080/960'$ curl 'localhost:8080/refused'

Source

chess.qwer
//! Chess rules through shakmaty: FEN read and written, legal moves in SAN and UCI, moves played and checked, PGN games read with their tags, comments and final positions, game state, perft, and Chess960 start positions.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! shakmaty = "=0.30.2"//! pgn-reader = "=0.29.1"use pgn_reader::{Outcome as PgnOutcome, RawComment, RawTag, Reader, SanPlus, Skip, Visitor};use serde_json::{json, Map, Value};use shakmaty::fen::Fen;use shakmaty::san::San;use shakmaty::uci::UciMove;use shakmaty::{CastlingMode, Chess, Color, EnPassantMode, KnownOutcome, Move, Outcome, Position, Rank, Role, Square};use std::ops::ControlFlow;const START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";fn fen_of(p: &Chess) -> String {    Fen::from_position(p, EnPassantMode::Legal).to_string()}/// A position from FEN, empty for the start. Standard castling first, then Chess960's.fn load(fen: &str) -> Result<(Chess, CastlingMode), String> {    let t = if fen.trim().is_empty() { START } else { fen.trim() };    let f: Fen = t.parse().map_err(|e| format!("`{}` is not FEN: {}", t, e))?;    if let Ok(p) = f.clone().into_position::<Chess>(CastlingMode::Standard) {        return Ok((p, CastlingMode::Standard));    }    match f.into_position::<Chess>(CastlingMode::Chess960) {        Ok(p) => Ok((p, CastlingMode::Chess960)),        Err(e) => Err(format!("that position cannot arise: {}", e)),    }}fn outcome(p: &Chess) -> Value {    match p.outcome() {        Outcome::Known(KnownOutcome::Decisive { winner: Color::White }) => json!("1-0"),        Outcome::Known(KnownOutcome::Decisive { winner: Color::Black }) => json!("0-1"),        Outcome::Known(KnownOutcome::Draw) => json!("1/2-1/2"),        Outcome::Unknown => Value::Null,    }}fn state(p: &Chess) -> Value {    let reason = if p.is_checkmate() { "checkmate" } else if p.is_stalemate() { "stalemate" } else if p.is_insufficient_material() { "insufficient material" } else if p.halfmoves() >= 150 { "seventy-five moves" } else { "" };    json!({        "fen": fen_of(p),        "turn": if p.turn() == Color::White { "white" } else { "black" },        "check": p.is_check(), "checkmate": p.is_checkmate(), "stalemate": p.is_stalemate(), "insufficient_material": p.is_insufficient_material(),        "game_over": p.is_game_over(), "result": outcome(p), "reason": reason,        "halfmove_clock": p.halfmoves(), "fullmove": p.fullmoves().get(), "fifty_move_claim": p.halfmoves() >= 100,        "legal_moves": p.legal_moves().len(),        "en_passant": p.legal_ep_square().map(|s| s.to_string()),    })}/// A position's state from FEN (empty for the start): `{ fen, turn, check, checkmate, stalemate, insufficient_material, game_over, result, reason, halfmove_clock, fullmove, fifty_move_claim, legal_moves, en_passant }`. FEN that cannot be or a position that cannot arise is an error.pub fn chess_position(fen: &str) -> Result<Value, String> {    Ok(state(&load(fen)?.0))}fn role_name(r: Role) -> &'static str {    match r {        Role::Pawn => "pawn",        Role::Knight => "knight",        Role::Bishop => "bishop",        Role::Rook => "rook",        Role::Queen => "queen",        Role::King => "king",    }}fn san_with_suffix(p: &Chess, m: Move) -> String {    let mut after = p.clone();    SanPlus::from_move_and_play_unchecked(&mut after, m).to_string()}fn move_json(p: &Chess, m: Move, mode: CastlingMode) -> Value {    let promotion = m.promotion().map(role_name);    json!({        "san": san_with_suffix(p, m), "uci": UciMove::from_move(m, mode).to_string(),        "from": m.from().map(|s| s.to_string()), "to": m.to().to_string(), "piece": role_name(m.role()),        "capture": m.capture().map(role_name).or(if m.is_en_passant() { Some("pawn") } else { None }), "promotion": promotion, "castle": m.castling_side().map(|c| if c.is_king_side() { "king" } else { "queen" }),        "en_passant": m.is_en_passant(),    })}/// Every legal move: `[{ san, uci, from, to, piece, capture, promotion, castle, en_passant }]`. `chess_moves("")` lists the 20 of the start.pub fn chess_moves(fen: &str) -> Result<Vec<Value>, String> {    let (p, mode) = load(fen)?;    Ok(p.legal_moves().iter().map(|m| move_json(&p, *m, mode)).collect())}/// A move written either way, checked against the position.fn read_move(p: &Chess, mv: &str) -> Result<Move, String> {    let t = mv.trim();    if let Ok(u) = t.parse::<UciMove>() {        if let Ok(m) = u.to_move(p) {            return Ok(m);        }    }    let clean = t.trim_end_matches(['+', '#', '!', '?']);    match clean.parse::<San>() {        Ok(s) => s.to_move(p).map_err(|e| format!("`{}` cannot be played here: {}", t, e)),        Err(_) => Err(format!("`{}` is not a move in SAN or UCI", t)),    }}/// Plays a move, SAN (`Nf3`) or UCI (`g1f3`): `{ san, uci, ..., position }`. An illegal move is an error saying so.pub fn chess_play(fen: &str, mv: &str) -> Result<Value, String> {    let (p, mode) = load(fen)?;    let m = read_move(&p, mv)?;    let mut v = move_json(&p, m, mode);    let after = p.play(m).map_err(|e| e.to_string())?;    v["position"] = state(&after);    Ok(v)}/// Plays a list of moves from `fen`: `{ san, uci, position }`, the moves as both notations. An illegal one is an error naming its number, such as "move 3 (white)".pub fn chess_line(fen: &str, moves: Vec<String>) -> Result<Value, String> {    let (mut p, mode) = load(fen)?;    let (mut san, mut uci) = (vec![], vec![]);    for mv in &moves {        let side = if p.turn() == Color::White { "white" } else { "black" };        let m = read_move(&p, mv).map_err(|e| format!("move {} ({}): {}", p.fullmoves().get(), side, e))?;        san.push(san_with_suffix(&p, m));        uci.push(UciMove::from_move(m, mode).to_string());        p = p.play(m).map_err(|e| e.to_string())?;    }    Ok(json!({ "san": san, "uci": uci, "position": state(&p) }))}/// Nodes of a move tree `depth` plies deep, the standard test of a move generator: from the start, depth 4 is 197281.pub fn chess_perft(fen: &str, depth: i64) -> Result<i64, String> {    if !(0..=6).contains(&depth) {        return Err("depth should be 0 to 6: it grows thirtyfold with each".into());    }    Ok(shakmaty::perft(&load(fen)?.0, depth as u32) as i64)}/// A position as text, rank 8 first, for logs and terminals.pub fn chess_board(fen: &str) -> Result<String, String> {    let (p, _) = load(fen)?;    let mut out = String::new();    for r in (0..8).rev() {        out.push_str(&format!("{} ", r + 1));        for f in 0..8 {            let s = Square::from_coords(shakmaty::File::new(f), Rank::new(r));            out.push(' ');            out.push(p.board().piece_at(s).map_or('.', |x| x.char()));        }        out.push('\n');    }    out.push_str("   a b c d e f g h\n");    Ok(out)}/// The FEN of a Chess960 start position, 0 to 959, by Scharnagl's numbering: 518 is the usual start. Its castling is written X-FEN style, with `KQkq`.pub fn chess_960(number: i64) -> Result<Value, String> {    if !(0..960).contains(&number) {        return Err(format!("{} is not a Chess960 position: they are numbered 0 to 959", number));    }    let mut rank = ['.'; 8];    let mut n = number as usize;    rank[n % 4 * 2 + 1] = 'B';    n /= 4;    rank[n % 4 * 2] = 'B';    n /= 4;    let free = |rank: &[char; 8]| -> Vec<usize> { (0..8).filter(|i| rank[*i] == '.').collect() };    let q = free(&rank)[n % 6];    rank[q] = 'Q';    n /= 6;    const KNIGHTS: [(usize, usize); 10] = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)];    let (a, b) = KNIGHTS[n];    let f = free(&rank);    rank[f[a]] = 'N';    rank[f[b]] = 'N';    let f = free(&rank);    rank[f[0]] = 'R';    rank[f[1]] = 'K';    rank[f[2]] = 'R';    let white: String = rank.iter().collect();    let fen = format!("{}/pppppppp/8/8/8/8/PPPPPPPP/{} w KQkq - 0 1", white.to_lowercase(), white);    let (p, mode) = load(&fen)?;    Ok(json!({ "number": number, "pieces": white, "fen": fen, "chess960": mode == CastlingMode::Chess960 || number != 518, "legal_moves": p.legal_moves().len() }))}#[derive(Default)]struct Game {    tags: Map<String, Value>,    moves: Vec<Value>,    comments: Vec<Value>,    variations: usize,    outcome: Option<String>,    start: Option<(Chess, CastlingMode)>,    pos: Option<Chess>,    error: Option<String>,    prologue: Vec<String>,}struct Games;impl Visitor for Games {    type Tags = Game;    type Movetext = Game;    type Output = Game;    fn begin_tags(&mut self) -> ControlFlow<Self::Output, Self::Tags> {        ControlFlow::Continue(Game::default())    }    fn tag(&mut self, g: &mut Self::Tags, name: &[u8], value: RawTag<'_>) -> ControlFlow<Self::Output> {        g.tags.insert(String::from_utf8_lossy(name).into_owned(), json!(value.decode_utf8_lossy()));        ControlFlow::Continue(())    }    fn begin_movetext(&mut self, mut g: Self::Tags) -> ControlFlow<Self::Output, Self::Movetext> {        let fen = g.tags.get("FEN").and_then(Value::as_str).unwrap_or("").to_string();        match load(&fen) {            Ok((p, mode)) => {                g.pos = Some(p.clone());                g.start = Some((p, mode));                ControlFlow::Continue(g)            }            Err(e) => {                g.error = Some(format!("tag FEN: {}", e));                ControlFlow::Break(g)            }        }    }    fn san(&mut self, g: &mut Self::Movetext, san: SanPlus) -> ControlFlow<Self::Output> {        let pos = g.pos.as_mut().unwrap();        let number = pos.fullmoves().get();        let side = if pos.turn() == Color::White { "white" } else { "black" };        match san.san.to_move(pos) {            Ok(m) => {                let mode = g.start.as_ref().unwrap().1;                g.moves.push(json!({ "number": number, "color": side, "san": san_with_suffix(pos, m), "uci": UciMove::from_move(m, mode).to_string() }));                pos.play_unchecked(m);                ControlFlow::Continue(())            }            Err(e) => {                g.error = Some(format!("move {} ({}): `{}` cannot be played: {}", number, side, san, e));                ControlFlow::Break(std::mem::take(g))            }        }    }    fn comment(&mut self, g: &mut Self::Movetext, comment: RawComment<'_>) -> ControlFlow<Self::Output> {        let t = String::from_utf8_lossy(comment.as_bytes()).trim().to_string();        if g.moves.is_empty() {            g.prologue.push(t);        } else {            g.comments.push(json!({ "after": g.moves.len(), "text": t }));        }        ControlFlow::Continue(())    }    fn begin_variation(&mut self, g: &mut Self::Movetext) -> ControlFlow<Self::Output, Skip> {        g.variations += 1;        ControlFlow::Continue(Skip(true))    }    fn outcome(&mut self, g: &mut Self::Movetext, outcome: PgnOutcome) -> ControlFlow<Self::Output> {        g.outcome = Some(outcome.to_string());        ControlFlow::Continue(())    }    fn end_game(&mut self, g: Self::Movetext) -> Self::Output {        g    }}/// Reads games from PGN text: `[{ tags, moves: [{ number, color, san, uci }], comments, variations, result, fen, position, error }]`. Each move is checked against the rules, from the `FEN` tag where there is one. A game with an illegal move stops there, with `error` naming the move, and the rest are still read. Side lines are counted and skipped.pub fn chess_pgn(text: &str) -> Result<Vec<Value>, String> {    let mut r = Reader::new(std::io::Cursor::new(text.as_bytes()));    let mut out = vec![];    while let Some(g) = r.read_game(&mut Games).map_err(|e| e.to_string())? {        let position = g.pos.as_ref().map(state).unwrap_or(Value::Null);        out.push(json!({            "tags": g.tags, "moves": g.moves, "comments": g.comments, "intro": g.prologue, "variations": g.variations,            "result": g.outcome.clone().or(g.tags.get("Result").and_then(Value::as_str).map(str::to_string)), "position": position, "error": g.error,        }));    }    Ok(out)}/// PGN text for a game: `tags` a map, `moves` SAN or UCI, `result` `1-0`, `0-1`, `1/2-1/2` or `*`. The seven standard tags come first, moves are checked, lines wrap at 80 columns. A `FEN` tag starts from that position.pub fn chess_pgn_write(tags: std::collections::BTreeMap<String, String>, moves: Vec<String>, result: &str) -> Result<String, String> {    if !["1-0", "0-1", "1/2-1/2", "*"].contains(&result) {        return Err(format!("result `{}` should be 1-0, 0-1, 1/2-1/2 or *", result));    }    let start = tags.get("FEN").cloned().unwrap_or_default();    let (mut p, _) = load(&start)?;    let mut out = String::new();    let seven = ["Event", "Site", "Date", "Round", "White", "Black"];    let tag = |k: &str, v: &str| format!("[{} \"{}\"]\n", k, v.replace('\\', "\\\\").replace('"', "\\\""));    for k in seven {        out.push_str(&tag(k, tags.get(k).map_or("?", |v| v.as_str())));    }    out.push_str(&tag("Result", result));    for (k, v) in &tags {        if !seven.contains(&k.as_str()) && k != "Result" {            out.push_str(&tag(k, v));        }    }    out.push('\n');    let mut words = vec![];    for (i, mv) in moves.iter().enumerate() {        let n = p.fullmoves().get();        let white = p.turn() == Color::White;        let m = read_move(&p, mv).map_err(|e| format!("move {} ({}): {}", n, if white { "white" } else { "black" }, e))?;        if white {            words.push(format!("{}.", n));        } else if i == 0 {            words.push(format!("{}...", n));        }        words.push(san_with_suffix(&p, m));        p = p.play(m).map_err(|e| e.to_string())?;    }    words.push(result.to_string());    let mut line = String::new();    for w in words {        if !line.is_empty() && line.len() + 1 + w.len() > 80 {            out.push_str(&line);            out.push('\n');            line.clear();        }        if !line.is_empty() {            line.push(' ');        }        line.push_str(&w);    }    out.push_str(&line);    out.push('\n');    Ok(out)}/// Whether a text is FEN that can be played: `{ ok, reason }`.pub fn chess_fen_check(fen: &str) -> Value {    match load(fen) {        Ok(_) => json!({ "ok": true, "reason": null }),        Err(e) => json!({ "ok": false, "reason": e }),    }}
Thread
25Useful? Like it, or add to its thread below.

Thread 0 posts

On Chess rules: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.