game: chess
label: Chess
players: "2 players"
mode: competitive
num_players: 2
demonstrates: "board geometry · custom functions · fully executable"
summary: >
  Full chess: castling, en passant, promotion, check, checkmate and stalemate, the fifty-move rule,
  threefold repetition and insufficient material. Move generation lives in `functions` and uses
  the board functions (ray, leap, neighbours); the perft counts match the known values.
scope: >
  Draws by repetition and by the fifty-move rule happen automatically (no claim needed). For
  repetition, the en passant chance counts whenever an enemy pawn stands next to the pawn that just
  moved two squares (not whether that capture is also legal). No clock, no draw offers, no resigning.
description: >
  Pieces: 'w' or 'b' plus K (king), Q (queen), R (rook), B (bishop), N (knight), P (pawn).
  A move is named in computer-chess notation: e2e4, e7e8q (promotion), e1g1 (kingside castling).

boards:
  board: {kind: square, width: 8, height: 8, names: letters, neighbours: 8, label: chessboard}

constants:
  COLOR: [w, b]
  STRAIGHT: [n, e, s, w]
  DIAGONAL: [ne, se, sw, nw]
  KNIGHT_JUMPS: [[1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1], [-2, 1], [-1, 2]]
  PROMOTION: [Q, R, B, N]
  LOWER: {Q: q, R: r, B: b, N: n}
  CASTLINGS:
    w:
      - {right: K, from: e1, to: g1, rook: h1, rook_to: f1, empty: [f1, g1], safe: [e1, f1, g1]}
      - {right: Q, from: e1, to: c1, rook: a1, rook_to: d1, empty: [b1, c1, d1], safe: [e1, d1, c1]}
    b:
      - {right: k, from: e8, to: g8, rook: h8, rook_to: f8, empty: [f8, g8], safe: [e8, f8, g8]}
      - {right: q, from: e8, to: c8, rook: a8, rook_to: d8, empty: [b8, c8, d8], safe: [e8, d8, c8]}
  RIGHTS_LOST: {e1: [K, Q], h1: [K], a1: [Q], e8: [k, q], h8: [k], a8: [q]}
  START:
    a1: wR
    b1: wN
    c1: wB
    d1: wQ
    e1: wK
    f1: wB
    g1: wN
    h1: wR
    a2: wP
    b2: wP
    c2: wP
    d2: wP
    e2: wP
    f2: wP
    g2: wP
    h2: wP
    a7: bP
    b7: bP
    c7: bP
    d7: bP
    e7: bP
    f7: bP
    g7: bP
    h7: bP
    a8: bR
    b8: bN
    c8: bB
    d8: bQ
    e8: bK
    f8: bB
    g8: bN
    h8: bR

functions:
  enemy:
    text: "the other colour"
    params: [k]
    x: "'b' if k == 'w' else 'w'"
  move:
    text: "a plain move as a dict"
    params: [src, dst, piece, capture]
    x: "{'label': src + dst, 'from': src, 'to': dst, 'piece': piece, 'capture': capture, 'promo': None, 'rook': None, 'ep': None}"
  knight_moves:
    text: "knight: jump to a square that is empty or holds an enemy"
    params: [s, k, c, t]
    x: >-
      [move(c, n, k + t, n if n in s else None)
       for n in [leap('board', c, v) for v in KNIGHT_JUMPS] if n is not None and s.get(n, '-')[0] != k]
  king_moves:
    text: "king: one square in any direction"
    params: [s, k, c]
    x: "[move(c, n, k + 'K', n if n in s else None) for n in neighbours('board', c) if s.get(n, '-')[0] != k]"
  slide_moves:
    text: "bishop, rook, queen: slide up to the first piece; an enemy piece may be captured"
    params: [s, k, c, t, dirs]
    x: >-
      [move(c, n, k + t, n if n in s else None)
       for r in dirs for n in ray('board', c, r, s) if s.get(n, '-')[0] != k]
  pawn_moves:
    text: "pawn: one forward, two from the starting rank, capture diagonally (also en passant), promote on the last rank"
    params: [s, k, c, ep]
    let:
      fwd: "'n' if k == 'w' else 's'"
      one: "step('board', c, fwd)"
      two: "step('board', one, fwd) if one is not None and one not in s and xy('board', c)[1] == (1 if k == 'w' else 6) else None"
      captures: >-
        [n for n in [step('board', c, r) for r in (['nw', 'ne'] if k == 'w' else ['sw', 'se'])]
         if n is not None and (s.get(n, k)[0] != k or n == ep)]
      base: >-
        (([move(c, one, k + 'P', None)] if one is not None and one not in s else [])
        + ([{**move(c, two, k + 'P', None),
             'ep': one if any(s.get(step('board', two, r)) == enemy(k) + 'P' for r in ['e', 'w']) else None}]
           if two is not None and two not in s else [])
        + [move(c, n, k + 'P', n if n in s else step('board', n, 's' if k == 'w' else 'n')) for n in captures])
    x: >-
      [{**mv, 'promo': k + q, 'label': mv['label'] + LOWER[q]} if q else mv
       for mv in base for q in (PROMOTION if xy('board', mv['to'])[1] in (0, 7) else [None])]
  castle_moves:
    text: "castling: king and rook unmoved (rights), squares between empty, king not in check and does not pass through check"
    params: [s, k, rights]
    x: >-
      [{**move(r['from'], r['to'], k + 'K', None), 'rook': [r['rook'], r['rook_to']]}
       for r in CASTLINGS[k]
       if r['right'] in rights and s.get(r['from']) == k + 'K' and s.get(r['rook']) == k + 'R'
       and not any(v in s for v in r['empty'])
       and not any(attacked(s, v, enemy(k)) for v in r['safe'])]
  pseudo:
    text: "all moves of colour k, before checking whether the own king is attacked afterwards"
    params: [s, k, ep, rights]
    x: >-
      ([mv for c, p in s.items() if p[0] == k
       for mv in (pawn_moves(s, k, c, ep) if p[1] == 'P'
                 else knight_moves(s, k, c, 'N') if p[1] == 'N'
                 else slide_moves(s, k, c, 'B', DIAGONAL) if p[1] == 'B'
                 else slide_moves(s, k, c, 'R', STRAIGHT) if p[1] == 'R'
                 else slide_moves(s, k, c, 'Q', STRAIGHT + DIAGONAL) if p[1] == 'Q'
                 else king_moves(s, k, c))]
      + castle_moves(s, k, rights))
  after:
    text: "the position after move mv"
    params: [s, mv]
    x: >-
      {**{c: p for c, p in s.items()
          if c != mv['from'] and c != mv['capture'] and (mv['rook'] is None or c != mv['rook'][0])},
       mv['to']: mv['promo'] or mv['piece'],
       **({mv['rook'][1]: mv['piece'][0] + 'R'} if mv['rook'] else {})}
  king:
    params: [s, k]
    x: "[c for c, p in s.items() if p == k + 'K'][0]"
  attacked:
    text: "is square v attacked by colour `by`? Look outward from v: knight jumps, king, pawns, lines"
    params: [s, v, by]
    x: >-
      (any(s.get(n) == by + 'N' for n in [leap('board', v, d) for d in KNIGHT_JUMPS] if n is not None)
      or any(s.get(n) == by + 'K' for n in neighbours('board', v))
      or any(s.get(n) == by + 'P'
             for n in [step('board', v, r) for r in (['sw', 'se'] if by == 'w' else ['nw', 'ne'])] if n is not None)
      or any(s.get(l[-1]) in (by + 'R', by + 'Q') for l in [ray('board', v, r, s) for r in STRAIGHT] if l)
      or any(s.get(l[-1]) in (by + 'B', by + 'Q') for l in [ray('board', v, r, s) for r in DIAGONAL] if l))
  legal:
    text: "the moves after which the own king is not attacked"
    params: [s, k, ep, rights]
    x: "[mv for mv in pseudo(s, k, ep, rights) for t in [after(s, mv)] if not attacked(t, king(t, k), enemy(k))]"
  key:
    text: "the position for repetition: pieces, side to move, castling rights, en passant"
    params: [s, side, rights, ep]
    x: "str(sorted(s.items())) + str(side) + str(rights) + str(ep)"
  square_color:
    params: [c]
    x: "sum(xy('board', c)) % 2"
  insufficient:
    text: "nobody can checkmate any more: only kings, one minor piece, or bishops on squares of one colour"
    params: [s]
    let:
      rest: "[(c, p) for c, p in s.items() if p[1] != 'K']"
    x: >-
      (len(rest) == 0
      or (len(rest) == 1 and rest[0][1][1] in ('B', 'N'))
      or (all(p[1] == 'B' for c, p in rest) and len({square_color(c) for c, p in rest}) == 1))

state:
  zones: {}
  vars:
    to_move: {scope: global, type: ref, init: 0, note: "0 = white, 1 = black"}
    pieces: {scope: global, type: dict, init_x: START, note: "square → piece, occupied squares only"}
    castling: {scope: global, type: list, init: [K, Q, k, q], note: "castling rights as in FEN"}
    ep: {scope: global, type: str, init: null, note: "square where an en passant capture can land"}
    halfmoves: {scope: global, type: int, init: 0, note: "moves since the last capture or pawn move"}
    move_number: {scope: global, type: int, init: 1}
    last_move: {scope: global, type: str, init: null, note: "the last move"}
    legal_moves:
      scope: global
      type: list
      init_x: "legal(pieces, COLOR[to_move], ep, castling)"
      note: "legal moves of the side to move (recomputed after every move)"
    in_check:
      scope: global
      type: bool
      init_x: "attacked(pieces, king(pieces, COLOR[to_move]), COLOR[1 - to_move])"
    positions:
      scope: global
      type: list
      init_x: "[key(pieces, to_move, castling, ep)]"
      note: "positions since the last irreversible move"

flow:
  id: match
  label: Match
  kind: loop
  note: "stops through the end conditions"
  children:
    - id: turn
      label: The side to move makes a move
      kind: step
      actor_x: to_move
      do: move

actions:
  move:
    pre: "there is a legal move"
    pre_x: "len(legal_moves) > 0"
    effect:
      - {op: choose, options: "legal moves", options_x: legal_moves, bind: mv}
      - {op: set, var: halfmoves, value_x: "0 if mv['piece'][1] == 'P' or mv['capture'] else halfmoves + 1"}
      - {op: set, var: castling, value_x: "[r for r in castling if r not in RIGHTS_LOST.get(mv['from'], []) and r not in RIGHTS_LOST.get(mv['to'], [])]"}
      - {op: set, var: ep, value_x: "mv['ep']"}
      - {op: set, var: pieces, value_x: "after(pieces, mv)"}
      - {op: set, var: move_number, value_x: "move_number + to_move"}
      - {op: set, var: to_move, value_x: "1 - to_move"}
      - {op: set, var: last_move, value_x: "mv['label']"}
      - {op: set, var: legal_moves, value_x: "legal(pieces, COLOR[to_move], ep, castling)"}
      - {op: set, var: in_check, value_x: "attacked(pieces, king(pieces, COLOR[to_move]), COLOR[1 - to_move])"}
      - op: set
        var: positions
        value_x: "([] if halfmoves == 0 else positions) + [key(pieces, to_move, castling, ep)]"
      - {op: emit, event: moved}

end:
  - id: checkmate
    check: immediate
    if: "the side to move has no move and is in check"
    if_x: "not legal_moves and in_check"
    who_x: "1 - to_move"
    result: win
  - id: stalemate
    check: immediate
    if: "the side to move has no move and is not in check"
    if_x: "not legal_moves and not in_check"
    result: draw
  - id: insufficient_material
    check: immediate
    if: "nobody can checkmate any more"
    if_x: "insufficient(pieces)"
    result: draw
  - id: repetition
    check: immediate
    if: "the same position for the third time"
    if_x: "positions.count(positions[-1]) >= 3"
    result: draw
  - id: fifty_moves
    check: immediate
    if: "fifty moves by each side without a capture or pawn move"
    if_x: "halfmoves >= 100"
    result: draw

