board.gno
8.08 Kb · 384 lines
1// Package minichess implements standard chess (8x8) for gno.land realms:
2// position representation, FEN, full move legality (castling, en passant,
3// promotion), game-end detection and a gas-bounded bot.
4//
5// The package is pure and stateless: Position is a value type, and every
6// method returns a new Position instead of mutating the receiver.
7package minichess
8
9import (
10 "errors"
11 "strconv"
12 "strings"
13)
14
15const (
16 Size = 8
17 NumSquares = Size * Size
18)
19
20// Piece is a signed piece code: positive for white, negative for black.
21type Piece int8
22
23const (
24 Empty Piece = 0
25 Pawn Piece = 1
26 Knight Piece = 2
27 Bishop Piece = 3
28 Rook Piece = 4
29 Queen Piece = 5
30 King Piece = 6
31)
32
33// Color is the side to move.
34type Color int8
35
36const (
37 White Color = 0
38 Black Color = 1
39)
40
41func (c Color) Other() Color { return 1 - c }
42
43func (c Color) String() string {
44 if c == White {
45 return "white"
46 }
47 return "black"
48}
49
50// Type returns the unsigned piece type.
51func (p Piece) Type() Piece {
52 if p < 0 {
53 return -p
54 }
55 return p
56}
57
58// Color returns the colour of a non-empty piece.
59func (p Piece) Color() Color {
60 if p < 0 {
61 return Black
62 }
63 return White
64}
65
66func colored(t Piece, c Color) Piece {
67 if c == Black {
68 return -t
69 }
70 return t
71}
72
73// Castling rights bits.
74const (
75 CastleWK = 1 // white king side (e1g1)
76 CastleWQ = 2 // white queen side (e1c1)
77 CastleBK = 4 // black king side (e8g8)
78 CastleBQ = 8 // black queen side (e8c8)
79)
80
81// Position is a complete game state.
82type Position struct {
83 Board [NumSquares]Piece // index = rank*8 + file; a1 = 0, h8 = 63
84 Turn Color
85 Castling int // CastleWK|CastleWQ|CastleBK|CastleBQ bits
86 EP int // en passant target square, or -1
87 Halfmove int // plies since the last capture or pawn move
88 Fullmove int // starts at 1, incremented after black moves
89}
90
91// Move is a move from one square to another, with an optional promotion
92// piece type (Knight, Bishop, Rook or Queen). Castling is the king's move
93// (e1g1); en passant is the capturing pawn's move to the target square.
94type Move struct {
95 From, To int
96 Promo Piece
97}
98
99// StartFEN is the standard starting position.
100const StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
101
102// NewGame returns the starting position.
103func NewGame() Position {
104 p, err := ParseFEN(StartFEN)
105 if err != nil {
106 panic(err)
107 }
108 return p
109}
110
111var pieceChars = " pnbrqk"
112
113func pieceToChar(p Piece) string {
114 if p == Empty {
115 return "."
116 }
117 ch := string(pieceChars[p.Type()])
118 if p.Color() == White {
119 return strings.ToUpper(ch)
120 }
121 return ch
122}
123
124func charToPiece(ch byte) (Piece, bool) {
125 lower := ch
126 white := false
127 if ch >= 'A' && ch <= 'Z' {
128 lower = ch - 'A' + 'a'
129 white = true
130 }
131 idx := strings.IndexByte(pieceChars, lower)
132 if idx <= 0 {
133 return Empty, false
134 }
135 if white {
136 return Piece(idx), true
137 }
138 return -Piece(idx), true
139}
140
141// ParseFEN parses standard FEN. The halfmove and fullmove fields are
142// optional.
143func ParseFEN(s string) (Position, error) {
144 var p Position
145 p.EP = -1
146 fields := strings.Fields(s)
147 if len(fields) < 2 {
148 return p, errors.New("chess: FEN needs board and side to move")
149 }
150 ranks := strings.Split(fields[0], "/")
151 if len(ranks) != Size {
152 return p, errors.New("chess: FEN must have 8 ranks")
153 }
154 kings := [2]int{}
155 for i, row := range ranks {
156 r := Size - 1 - i
157 f := 0
158 for j := 0; j < len(row); j++ {
159 ch := row[j]
160 if ch >= '1' && ch <= '8' {
161 f += int(ch - '0')
162 continue
163 }
164 pc, ok := charToPiece(ch)
165 if !ok {
166 return p, errors.New("chess: bad FEN piece " + string(ch))
167 }
168 if f >= Size {
169 return p, errors.New("chess: FEN rank too long")
170 }
171 if pc == King {
172 kings[0]++
173 } else if pc == -King {
174 kings[1]++
175 }
176 p.Board[r*Size+f] = pc
177 f++
178 }
179 if f != Size {
180 return p, errors.New("chess: FEN rank has wrong length")
181 }
182 }
183 if kings[0] != 1 || kings[1] != 1 {
184 return p, errors.New("chess: FEN needs exactly one king per side")
185 }
186 switch fields[1] {
187 case "w":
188 p.Turn = White
189 case "b":
190 p.Turn = Black
191 default:
192 return p, errors.New("chess: FEN side must be w or b")
193 }
194 if len(fields) > 2 && fields[2] != "-" {
195 for _, c := range fields[2] {
196 switch c {
197 case 'K':
198 p.Castling |= CastleWK
199 case 'Q':
200 p.Castling |= CastleWQ
201 case 'k':
202 p.Castling |= CastleBK
203 case 'q':
204 p.Castling |= CastleBQ
205 default:
206 return p, errors.New("chess: bad FEN castling field")
207 }
208 }
209 }
210 if len(fields) > 3 && fields[3] != "-" {
211 sq, ok := ParseSquare(fields[3])
212 if !ok {
213 return p, errors.New("chess: bad FEN en passant square")
214 }
215 p.EP = sq
216 }
217 p.Fullmove = 1
218 if len(fields) > 4 {
219 n, err := strconv.Atoi(fields[4])
220 if err != nil || n < 0 {
221 return p, errors.New("chess: bad halfmove clock")
222 }
223 p.Halfmove = n
224 }
225 if len(fields) > 5 {
226 n, err := strconv.Atoi(fields[5])
227 if err != nil || n < 1 {
228 return p, errors.New("chess: bad fullmove number")
229 }
230 p.Fullmove = n
231 }
232 return p, nil
233}
234
235func (p Position) placement() string {
236 var sb strings.Builder
237 for r := Size - 1; r >= 0; r-- {
238 empty := 0
239 for f := 0; f < Size; f++ {
240 pc := p.Board[r*Size+f]
241 if pc == Empty {
242 empty++
243 continue
244 }
245 if empty > 0 {
246 sb.WriteString(strconv.Itoa(empty))
247 empty = 0
248 }
249 sb.WriteString(pieceToChar(pc))
250 }
251 if empty > 0 {
252 sb.WriteString(strconv.Itoa(empty))
253 }
254 if r > 0 {
255 sb.WriteString("/")
256 }
257 }
258 return sb.String()
259}
260
261func (p Position) castlingString() string {
262 s := ""
263 if p.Castling&CastleWK != 0 {
264 s += "K"
265 }
266 if p.Castling&CastleWQ != 0 {
267 s += "Q"
268 }
269 if p.Castling&CastleBK != 0 {
270 s += "k"
271 }
272 if p.Castling&CastleBQ != 0 {
273 s += "q"
274 }
275 if s == "" {
276 return "-"
277 }
278 return s
279}
280
281func (p Position) side() string {
282 if p.Turn == White {
283 return "w"
284 }
285 return "b"
286}
287
288// FEN returns the full FEN string of the position.
289func (p Position) FEN() string {
290 ep := "-"
291 if p.EP >= 0 {
292 ep = SquareName(p.EP)
293 }
294 return p.placement() + " " + p.side() + " " + p.castlingString() + " " + ep +
295 " " + strconv.Itoa(p.Halfmove) + " " + strconv.Itoa(p.Fullmove)
296}
297
298// BoardKey identifies the position for threefold repetition: placement,
299// side to move, castling rights and an en passant square only when an en
300// passant capture is actually available.
301func (p Position) BoardKey() string {
302 ep := "-"
303 if p.EP >= 0 && p.epCapturePossible() {
304 ep = SquareName(p.EP)
305 }
306 return p.placement() + " " + p.side() + " " + p.castlingString() + " " + ep
307}
308
309func (p Position) epCapturePossible() bool {
310 e := newEngine(&p)
311 for _, m := range e.legal() {
312 if (m>>6)&63 == p.EP && e.b[m&63].Type() == Pawn {
313 return true
314 }
315 }
316 return false
317}
318
319// PieceAt returns the piece on a square given by name, e.g. "e4".
320func (p Position) PieceAt(square string) Piece {
321 sq, ok := ParseSquare(square)
322 if !ok {
323 return Empty
324 }
325 return p.Board[sq]
326}
327
328// SquareName converts an index to algebraic notation ("a1".."h8").
329func SquareName(sq int) string {
330 return string(rune('a'+sq%Size)) + string(rune('1'+sq/Size))
331}
332
333// ParseSquare converts algebraic notation to an index.
334func ParseSquare(s string) (int, bool) {
335 if len(s) != 2 {
336 return 0, false
337 }
338 f := int(s[0]) - 'a'
339 r := int(s[1]) - '1'
340 if f < 0 || f >= Size || r < 0 || r >= Size {
341 return 0, false
342 }
343 return r*Size + f, true
344}
345
346// String returns the move in coordinate notation, e.g. "e2e4" or "e7e8q".
347func (m Move) String() string {
348 s := SquareName(m.From) + SquareName(m.To)
349 if m.Promo != Empty {
350 s += string(pieceChars[m.Promo.Type()])
351 }
352 return s
353}
354
355// ParseMove parses coordinate notation: "e2e4", "e1g1" or "e7e8n".
356// It does not check legality; use Position.FindLegal for that.
357func ParseMove(s string) (Move, error) {
358 s = strings.ToLower(strings.TrimSpace(s))
359 var m Move
360 if len(s) != 4 && len(s) != 5 {
361 return m, errors.New("chess: move must look like e2e4 or e7e8q")
362 }
363 from, ok1 := ParseSquare(s[0:2])
364 to, ok2 := ParseSquare(s[2:4])
365 if !ok1 || !ok2 {
366 return m, errors.New("chess: bad square in move " + s)
367 }
368 m.From, m.To = from, to
369 if len(s) == 5 {
370 switch s[4] {
371 case 'n':
372 m.Promo = Knight
373 case 'b':
374 m.Promo = Bishop
375 case 'r':
376 m.Promo = Rook
377 case 'q':
378 m.Promo = Queen
379 default:
380 return m, errors.New("chess: promotion must be q, r, b or n")
381 }
382 }
383 return m, nil
384}