all repos

tyess @ 3e5d02242460a2d2359ff700bbbdd56c76483869

chess in typst

tyess/rules.typ (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
only export chess as part of public api; merge moves.typ and rules.typ, 27 days ago
1
#import "board.typ": at, copy-grid, find-pieces, is-black, is-white, on-board, piece-type
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
130
131
#let is-attacked(grid, col, row, by-color) = {
132
  let enemy(piece) = if by-color == "white" { is-white(piece) } else { is-black(piece) }
133
134
  // knight attacks
135
  for (dc, dr) in knight-offsets {
136
    let p = at(grid, col + dc, row + dr)
137
    if p != "." and piece-type(p) == "N" and enemy(p) { return true }
138
  }
139
140
  // king attacks
141
  for (dc, dr) in king-dirs {
142
    let p = at(grid, col + dc, row + dr)
143
    if p != "." and piece-type(p) == "K" and enemy(p) { return true }
144
  }
145
146
  // pawn attacks
147
  let pawn-checks = if by-color == "white" { ((-1, 1), (1, 1)) } else { ((-1, -1), (1, -1)) }
148
  for (dc, dr) in pawn-checks {
149
    let p = at(grid, col + dc, row + dr)
150
    if p != "." and piece-type(p) == "P" and enemy(p) { return true }
151
  }
152
153
  // sliding pieces
154
  for (dirs, types) in ((bishop-dirs, ("B", "Q")), (rook-dirs, ("R", "Q"))) {
155
    for (dc, dr) in dirs {
156
      let nc = col + dc
157
      let nr = row + dr
158
      while on-board(nc, nr) {
159
        let p = at(grid, nc, nr)
160
        if p != "." {
161
          if enemy(p) and types.contains(piece-type(p)) { return true }
162
          break // blocked by any piece
163
        }
164
        nc += dc
165
        nr += dr
166
      }
167
    }
168
  }
169
170
  false
171
}
172
173
#let is-in-check(grid, turn) = {
174
  let king-char = if turn == "white" { "K" } else { "k" }
175
  let enemy = if turn == "white" { "black" } else { "white" }
176
  for r in range(8) {
177
    for c in range(8) {
178
      if grid.at(r).at(c) == king-char {
179
        return is-attacked(grid, c + 1, r + 1, enemy)
180
      }
181
    }
182
  }
183
  false
184
}
185
186
#let can-castle(grid, row, empty-cols, check-cols, enemy) = {
187
  if is-attacked(grid, 5, row, enemy) { return false }
188
  for c in empty-cols { if at(grid, c, row) != "." { return false } }
189
  for c in check-cols { if is-attacked(grid, c, row, enemy) { return false } }
190
  true
191
}
192
193
#let has-legal-move(grid, turn, ep: none, castles: "") = {
194
  for r in range(8) {
195
    for c in range(8) {
196
      let p = grid.at(r).at(c)
197
      if p == "." { continue }
198
      if turn == "white" and not is-white(p) { continue }
199
      if turn == "black" and not is-black(p) { continue }
200
201
      let type = piece-type(p)
202
203
      let col = c + 1
204
      let row = r + 1
205
      let raw-moves = if type == "P" { pawn-moves(grid, col, row, turn, ep: ep) } else {
206
        generate-moves-for(grid, col, row, turn, ep: ep)
207
      }
208
209
      for m in raw-moves {
210
        let (dc, dr) = (m.at(0), m.at(1))
211
        let new-grid = copy-grid(grid)
212
        new-grid.at(r).at(c) = "."
213
        let mover = p
214
        let is-pawn = type == "P"
215
        if is-pawn and (dr == 1 or dr == 8) {
216
          mover = if turn == "white" { "Q" } else { "q" }
217
        }
218
        new-grid.at(dr - 1).at(dc - 1) = mover
219
        if is-pawn and ep != none and dc == ep.at(0) and dr == ep.at(1) {
220
          let captured-row = if turn == "white" { dr + 1 } else { dr - 1 }
221
          new-grid.at(captured-row - 1).at(dc - 1) = "."
222
        }
223
        if not is-in-check(new-grid, turn) { return true }
224
      }
225
    }
226
  }
227
228
  let enemy = if turn == "white" { "black" } else { "white" }
229
  let options = if turn == "white" {
230
    (("K", 8, (6, 7), (6, 7)), ("Q", 8, (2, 3, 4), (3, 4)))
231
  } else {
232
    (("k", 1, (6, 7), (6, 7)), ("q", 1, (2, 3, 4), (3, 4)))
233
  }
234
235
  for (flag, row, empty, check) in options {
236
    if castles.contains(flag) and can-castle(grid, row, empty, check, enemy) { return true }
237
  }
238
239
  false
240
}