Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}