all repos

tyess @ 87a9beffa14cfa0c1076fd245cc29e7c606683de

chess in typst
5 files changed, 142 insertions(+), 141 deletions(-)
only export chess as part of public api; merge moves.typ and rules.typ
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-03 23:52:08 +0300
Authored at: 2026-07-03 23:50:26 +0300
Change ID: pxqqmoutptyvpyoyqoqzqtvzpzuxupos
Parent: 3f3048b
M game.typ
···
        1
        1
         #import "board.typ": apply-move, at, copy-grid, file-to-col, find-pieces, is-black, is-white, on-board, piece-type, render-board, square-to-coords, starting-grid

      
        2
        2
         #import "notation.typ": parse-move

      
        3
        
        -#import "moves.typ": generate-moves-for, pawn-moves

      
        4
        
        -#import "rules.typ": has-legal-move, is-attacked, is-in-check

      
        
        3
        +#import "rules.typ": generate-moves-for, has-legal-move, is-attacked, is-in-check, pawn-moves

      
        5
        4
         

      
        6
        5
         #let initial-state() = (

      
        7
        6
           grid: starting-grid,

      ···
        180
        179
           out.push(render-board(state.grid))

      
        181
        180
           out.join()

      
        182
        181
         }

      
        
        182
        +

      
        
        183
        +// Usage:

      
        
        184
        +//   #show: chess.with()

      
        
        185
        +//   g4 d5 Bg5 Bxg4 c4 c6 cxd5 cxd5 Qb3

      
        
        186
        +//

      
        
        187
        +#let chess(body) = {

      
        
        188
        +  set page(height: auto, width: auto, margin: (top: 0.5in, bottom: 0.5in, rest: 0.5in))

      
        
        189
        +  let moves = parse-moves(body)

      
        
        190
        +  let states = apply-moves(moves)

      
        
        191
        +  render(states.last())

      
        
        192
        +}

      
M lib.typ
···
        1
        
        -#import "game.typ": render, apply-moves, parse-moves

      
        2
        
        -

      
        3
        
        -// Usage:

      
        4
        
        -//   #show: chess.with()

      
        5
        
        -//   g4 d5 Bg5 Bxg4 c4 c6 cxd5 cxd5 Qb3

      
        6
        
        -//

      
        7
        
        -#let chess(body) = {

      
        8
        
        -  set page(height: auto, width: auto, margin: (top: 0.5in, bottom: 0.5in, rest: 0.5in))

      
        9
        
        -  let moves = parse-moves(body)

      
        10
        
        -  let states = apply-moves(moves)

      
        11
        
        -  render(states.last())

      
        12
        
        -}

      
        
        1
        +#import "game.typ": chess

      
D moves.typ
···
        1
        
        -#import "board.typ": at, is-black, is-white, on-board, piece-type

      
        2
        
        -

      
        3
        
        -#let enemy(turn, target) = if turn == "white" { is-black(target) } else { is-white(target) }

      
        4
        
        -

      
        5
        
        -#let pawn-dir(turn) = if turn == "white" { -1 } else { 1 }

      
        6
        
        -#let pawn-moves(grid, col, row, turn, ep: none) = { // returns list of (dest-col, dest-row, prom?), ep == en passant

      
        7
        
        -  let moves = ()

      
        8
        
        -  let start-row = if turn == "white" { 7 } else { 2 }

      
        9
        
        -  let dir = pawn-dir(turn)

      
        10
        
        -

      
        11
        
        -  // forward one

      
        12
        
        -  let nr = row + dir

      
        13
        
        -  if on-board(col, nr) and at(grid, col, nr) == "." {

      
        14
        
        -    if nr == 1 or nr == 8 {

      
        15
        
        -      for prom in ("Q", "R", "B", "N") { moves.push((col, nr, prom)) }

      
        16
        
        -    } else { moves.push((col, nr)) }

      
        17
        
        -

      
        18
        
        -    // forward two from start

      
        19
        
        -    let nr2 = row + 2 * dir

      
        20
        
        -    if row == start-row and at(grid, col, nr2) == "." { moves.push((col, nr2)) }

      
        21
        
        -  }

      
        22
        
        -

      
        23
        
        -  // captures

      
        24
        
        -  for dc in (-1, 1) {

      
        25
        
        -    let nc = col + dc

      
        26
        
        -    let nr = row + dir

      
        27
        
        -    if on-board(nc, nr) {

      
        28
        
        -      if enemy(turn, at(grid, nc, nr)) {

      
        29
        
        -        if nr == 1 or nr == 8 {

      
        30
        
        -          for prom in ("Q", "R", "B", "N") { moves.push((nc, nr, prom)) }

      
        31
        
        -        } else {

      
        32
        
        -          moves.push((nc, nr))

      
        33
        
        -        }

      
        34
        
        -      }

      
        35
        
        -

      
        36
        
        -      // en passant capture: pawn ends on ep square, captured pawn is one rank behind

      
        37
        
        -      if ep != none and nc == ep.at(0) {

      
        38
        
        -        let captured-row = ep.at(1) - dir

      
        39
        
        -        if enemy(turn, at(grid, nc, captured-row)) { moves.push((ep.at(0), ep.at(1))) }

      
        40
        
        -      }

      
        41
        
        -    }

      
        42
        
        -  }

      
        43
        
        -

      
        44
        
        -  moves

      
        45
        
        -}

      
        46
        
        -

      
        47
        
        -#let knight-offsets = ((-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1))

      
        48
        
        -#let knight-moves(grid, col, row, turn) = {

      
        49
        
        -  let moves = ()

      
        50
        
        -  for (dc, dr) in knight-offsets {

      
        51
        
        -    let nc = col + dc

      
        52
        
        -    let nr = row + dr

      
        53
        
        -    if on-board(nc, nr) {

      
        54
        
        -      let target = at(grid, nc, nr)

      
        55
        
        -      if target == "." or enemy(turn, target) {

      
        56
        
        -        moves.push((nc, nr))

      
        57
        
        -      }

      
        58
        
        -    }

      
        59
        
        -  }

      
        60
        
        -  moves

      
        61
        
        -}

      
        62
        
        -

      
        63
        
        -#let sliding-moves(grid, col, row, turn, dirs) = {

      
        64
        
        -  let moves = ()

      
        65
        
        -  for (dc, dr) in dirs {

      
        66
        
        -    let nc = col + dc

      
        67
        
        -    let nr = row + dr

      
        68
        
        -    while on-board(nc, nr) {

      
        69
        
        -      let target = at(grid, nc, nr)

      
        70
        
        -      if target == "." {

      
        71
        
        -        moves.push((nc, nr))

      
        72
        
        -      } else if enemy(turn, target) {

      
        73
        
        -        moves.push((nc, nr))

      
        74
        
        -        break

      
        75
        
        -      } else { break }

      
        76
        
        -      nc = nc + dc

      
        77
        
        -      nr = nr + dr

      
        78
        
        -    }

      
        79
        
        -  }

      
        80
        
        -  moves

      
        81
        
        -}

      
        82
        
        -

      
        83
        
        -#let bishop-dirs = ((-1, -1), (-1, 1), (1, -1), (1, 1))

      
        84
        
        -#let bishop-moves(grid, col, row, turn) = sliding-moves(grid, col, row, turn, bishop-dirs)

      
        85
        
        -

      
        86
        
        -#let rook-dirs = ((-1, 0), (1, 0), (0, -1), (0, 1))

      
        87
        
        -#let rook-moves(grid, col, row, turn) = sliding-moves(grid, col, row, turn, rook-dirs)

      
        88
        
        -

      
        89
        
        -#let queen-moves(grid, col, row, turn) = {

      
        90
        
        -  let moves = ()

      
        91
        
        -  for m in bishop-moves(grid, col, row, turn) { moves.push(m) }

      
        92
        
        -  for m in rook-moves(grid, col, row, turn) { moves.push(m) }

      
        93
        
        -  moves

      
        94
        
        -}

      
        95
        
        -

      
        96
        
        -#let king-dirs = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1))

      
        97
        
        -#let king-moves(grid, col, row, turn) = {

      
        98
        
        -  let moves = ()

      
        99
        
        -  for (dc, dr) in king-dirs {

      
        100
        
        -    let nc = col + dc

      
        101
        
        -    let nr = row + dr

      
        102
        
        -    if on-board(nc, nr) {

      
        103
        
        -      if at(grid, nc, nr) == "." or enemy(turn, at(grid, nc, nr)) {

      
        104
        
        -        moves.push((nc, nr))

      
        105
        
        -      }

      
        106
        
        -    }

      
        107
        
        -  }

      
        108
        
        -  moves

      
        109
        
        -}

      
        110
        
        -

      
        111
        
        -#let generate-moves-for(grid, col, row, turn, ep: none) = {

      
        112
        
        -  let p = at(grid, col, row)

      
        113
        
        -  if p == "." { return () }

      
        114
        
        -  if turn == "white" and not is-white(p) { return () }

      
        115
        
        -  if turn == "black" and not is-black(p) { return () }

      
        116
        
        -

      
        117
        
        -  let type = piece-type(p)

      
        118
        
        -  if type == "P" { return pawn-moves(grid, col, row, turn, ep: ep) }

      
        119
        
        -  if type == "N" { return knight-moves(grid, col, row, turn) }

      
        120
        
        -  if type == "B" { return bishop-moves(grid, col, row, turn) }

      
        121
        
        -  if type == "R" { return rook-moves(grid, col, row, turn) }

      
        122
        
        -  if type == "Q" { return queen-moves(grid, col, row, turn) }

      
        123
        
        -  if type == "K" { return king-moves(grid, col, row, turn) }

      
        124
        
        -  ()

      
        125
        
        -}

      
M rules.typ
···
        1
        1
         #import "board.typ": at, copy-grid, find-pieces, is-black, is-white, on-board, piece-type

      
        2
        
        -#import "moves.typ": bishop-dirs, generate-moves-for, king-dirs, knight-offsets, pawn-moves, rook-dirs

      
        
        2
        +

      
        
        3
        +// move generation

      
        
        4
        +

      
        
        5
        +#let enemy(turn, target) = if turn == "white" { is-black(target) } else { is-white(target) }

      
        
        6
        +

      
        
        7
        +#let pawn-dir(turn) = if turn == "white" { -1 } else { 1 }

      
        
        8
        +#let pawn-moves(grid, col, row, turn, ep: none) = {

      
        
        9
        +  let moves = ()

      
        
        10
        +  let start-row = if turn == "white" { 7 } else { 2 }

      
        
        11
        +  let dir = pawn-dir(turn)

      
        
        12
        +

      
        
        13
        +  // forward one

      
        
        14
        +  let nr = row + dir

      
        
        15
        +  if on-board(col, nr) and at(grid, col, nr) == "." {

      
        
        16
        +    if nr == 1 or nr == 8 {

      
        
        17
        +      for prom in ("Q", "R", "B", "N") { moves.push((col, nr, prom)) }

      
        
        18
        +    } else { moves.push((col, nr)) }

      
        
        19
        +

      
        
        20
        +    // forward two from start

      
        
        21
        +    let nr2 = row + 2 * dir

      
        
        22
        +    if row == start-row and at(grid, col, nr2) == "." { moves.push((col, nr2)) }

      
        
        23
        +  }

      
        
        24
        +

      
        
        25
        +  // captures

      
        
        26
        +  for dc in (-1, 1) {

      
        
        27
        +    let nc = col + dc

      
        
        28
        +    let nr = row + dir

      
        
        29
        +    if on-board(nc, nr) {

      
        
        30
        +      if enemy(turn, at(grid, nc, nr)) {

      
        
        31
        +        if nr == 1 or nr == 8 {

      
        
        32
        +          for prom in ("Q", "R", "B", "N") { moves.push((nc, nr, prom)) }

      
        
        33
        +        } else {

      
        
        34
        +          moves.push((nc, nr))

      
        
        35
        +        }

      
        
        36
        +      }

      
        
        37
        +

      
        
        38
        +      // en passant capture: pawn ends on ep square, captured pawn is one rank behind

      
        
        39
        +      if ep != none and nc == ep.at(0) {

      
        
        40
        +        let captured-row = ep.at(1) - dir

      
        
        41
        +        if enemy(turn, at(grid, nc, captured-row)) { moves.push((ep.at(0), ep.at(1))) }

      
        
        42
        +      }

      
        
        43
        +    }

      
        
        44
        +  }

      
        
        45
        +

      
        
        46
        +  moves

      
        
        47
        +}

      
        
        48
        +

      
        
        49
        +#let knight-offsets = ((-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1))

      
        
        50
        +#let knight-moves(grid, col, row, turn) = {

      
        
        51
        +  let moves = ()

      
        
        52
        +  for (dc, dr) in knight-offsets {

      
        
        53
        +    let nc = col + dc

      
        
        54
        +    let nr = row + dr

      
        
        55
        +    if on-board(nc, nr) {

      
        
        56
        +      let target = at(grid, nc, nr)

      
        
        57
        +      if target == "." or enemy(turn, target) {

      
        
        58
        +        moves.push((nc, nr))

      
        
        59
        +      }

      
        
        60
        +    }

      
        
        61
        +  }

      
        
        62
        +  moves

      
        
        63
        +}

      
        
        64
        +

      
        
        65
        +#let sliding-moves(grid, col, row, turn, dirs) = {

      
        
        66
        +  let moves = ()

      
        
        67
        +  for (dc, dr) in dirs {

      
        
        68
        +    let nc = col + dc

      
        
        69
        +    let nr = row + dr

      
        
        70
        +    while on-board(nc, nr) {

      
        
        71
        +      let target = at(grid, nc, nr)

      
        
        72
        +      if target == "." {

      
        
        73
        +        moves.push((nc, nr))

      
        
        74
        +      } else if enemy(turn, target) {

      
        
        75
        +        moves.push((nc, nr))

      
        
        76
        +        break

      
        
        77
        +      } else { break }

      
        
        78
        +      nc = nc + dc

      
        
        79
        +      nr = nr + dr

      
        
        80
        +    }

      
        
        81
        +  }

      
        
        82
        +  moves

      
        
        83
        +}

      
        
        84
        +

      
        
        85
        +#let bishop-dirs = ((-1, -1), (-1, 1), (1, -1), (1, 1))

      
        
        86
        +#let bishop-moves(grid, col, row, turn) = sliding-moves(grid, col, row, turn, bishop-dirs)

      
        
        87
        +

      
        
        88
        +#let rook-dirs = ((-1, 0), (1, 0), (0, -1), (0, 1))

      
        
        89
        +#let rook-moves(grid, col, row, turn) = sliding-moves(grid, col, row, turn, rook-dirs)

      
        
        90
        +

      
        
        91
        +#let queen-moves(grid, col, row, turn) = {

      
        
        92
        +  let moves = ()

      
        
        93
        +  for m in bishop-moves(grid, col, row, turn) { moves.push(m) }

      
        
        94
        +  for m in rook-moves(grid, col, row, turn) { moves.push(m) }

      
        
        95
        +  moves

      
        
        96
        +}

      
        
        97
        +

      
        
        98
        +#let king-dirs = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1))

      
        
        99
        +#let king-moves(grid, col, row, turn) = {

      
        
        100
        +  let moves = ()

      
        
        101
        +  for (dc, dr) in king-dirs {

      
        
        102
        +    let nc = col + dc

      
        
        103
        +    let nr = row + dr

      
        
        104
        +    if on-board(nc, nr) {

      
        
        105
        +      if at(grid, nc, nr) == "." or enemy(turn, at(grid, nc, nr)) {

      
        
        106
        +        moves.push((nc, nr))

      
        
        107
        +      }

      
        
        108
        +    }

      
        
        109
        +  }

      
        
        110
        +  moves

      
        
        111
        +}

      
        
        112
        +

      
        
        113
        +#let generate-moves-for(grid, col, row, turn, ep: none) = {

      
        
        114
        +  let p = at(grid, col, row)

      
        
        115
        +  if p == "." { return () }

      
        
        116
        +  if turn == "white" and not is-white(p) { return () }

      
        
        117
        +  if turn == "black" and not is-black(p) { return () }

      
        
        118
        +

      
        
        119
        +  let type = piece-type(p)

      
        
        120
        +  if type == "P" { return pawn-moves(grid, col, row, turn, ep: ep) }

      
        
        121
        +  if type == "N" { return knight-moves(grid, col, row, turn) }

      
        
        122
        +  if type == "B" { return bishop-moves(grid, col, row, turn) }

      
        
        123
        +  if type == "R" { return rook-moves(grid, col, row, turn) }

      
        
        124
        +  if type == "Q" { return queen-moves(grid, col, row, turn) }

      
        
        125
        +  if type == "K" { return king-moves(grid, col, row, turn) }

      
        
        126
        +  ()

      
        
        127
        +}

      
        
        128
        +

      
        
        129
        +// chess rules

      
        3
        130
         

      
        4
        131
         #let is-attacked(grid, col, row, by-color) = {

      
        5
        132
           let enemy(piece) = if by-color == "white" { is-white(piece) } else { is-black(piece) }

      
M tests/move-gen.typ
···
        1
        
        -#import "../moves.typ": generate-moves-for

      
        
        1
        +#import "../rules.typ": generate-moves-for

      
        2
        2
         #import "../board.typ": starting-grid

      
        3
        3
         #import "testutil.typ": assert-eq

      
        4
        4