Search Apps Documentation Source Content File Folder Download Copy Actions Download

engine.gno

28.65 Kb ยท 1197 lines
   1// Package chess implements a simple chess engine, designed to implement all
   2// of the official FIDE rules with the intention of validating moves for a
   3// "chess server" realm (gno.land/r/demo/chess).
   4//
   5// To implement the rules, the FIDE "Laws of Chess" are used as a reference:
   6// https://www.fide.com/FIDE/handbook/LawsOfChess.pdf
   7//
   8// This package was designed with a focus on clarity and on using this code as
   9// a didactic tool. Any contributions to the code should respect this.
  10package chess
  11
  12import (
  13	"errors"
  14	"sort"
  15	"strconv"
  16	"strings"
  17
  18	"gno.land/p/morgan/chess/zobrist"
  19)
  20
  21// PositionFlags. The lower 4 bits indicate an en passant column; the upper
  22// 4 indicate castling rights.
  23type PositionFlags byte
  24
  25const (
  26	EnPassant PositionFlags = 1 << (iota + 3)
  27	NoCastleWQ
  28	NoCastleWK
  29	NoCastleBQ
  30	NoCastleBK
  31
  32	MaskEnPassant = 7 // low 4 bits
  33)
  34
  35// CastlingRights returns FEN castling rights.
  36// https://www.chessprogramming.org/Forsyth-Edwards_Notation#Castling_ability
  37func (p PositionFlags) CastlingRights() string {
  38	s := ""
  39	if p&NoCastleWK == 0 {
  40		s += "K"
  41	}
  42	if p&NoCastleWQ == 0 {
  43		s += "Q"
  44	}
  45	if p&NoCastleBK == 0 {
  46		s += "k"
  47	}
  48	if p&NoCastleBQ == 0 {
  49		s += "q"
  50	}
  51	if s == "" {
  52		return "-"
  53	}
  54	return s
  55}
  56
  57// Position contains the information about a chessboard, and surrounding
  58// context: the previous moves, the castling rights and "en passant" column.
  59//
  60// NOTE: the position of a piece is encoded in a [Square].
  61type Position struct {
  62	B     Board
  63	Moves []Move
  64	Flags PositionFlags
  65
  66	// Halfmoves since the last pawn move or capture.
  67	// https://www.chessprogramming.org/Halfmove_Clock
  68	HalfMoveClock uint16
  69	// Used to calculate repeating positions (3- 5-fold repetition).
  70	// Zobrist hashing: https://www.chessprogramming.org/Zobrist_Hashing
  71	// Reset together with halfmove clock.
  72	Hashes []uint64
  73}
  74
  75// NewPosition returns a new Position, set up with the initial board position.
  76func NewPosition() Position {
  77	return Position{
  78		B:      NewBoard(),
  79		Moves:  make([]Move, 0, 80), // typical chess game is ~40 moves, 80 half-moves
  80		Hashes: []uint64{zobrist.InitialPosition},
  81	}
  82}
  83
  84// Color of the "next" move after p.Moves. (White for even len(p.Moves),
  85// Black otherwise)
  86func (p Position) Color() Color { return Color(len(p.Moves)&1 == 1) }
  87
  88// uintSlice attaches the methods of sort.Interface to []uint64, sorting in increasing order.
  89type uintSlice []uint64
  90
  91func (x uintSlice) Len() int           { return len(x) }
  92func (x uintSlice) Less(i, j int) bool { return x[i] < x[j] }
  93func (x uintSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
  94
  95// maxHashCount counts the maximum number of repeating hashes in p.Hashes.
  96func (p Position) maxHashCount() int {
  97	if len(p.Hashes) == 0 {
  98		return 0
  99	}
 100	sort.Sort(uintSlice(p.Hashes))
 101	var last uint64
 102	var cur, max int
 103	for _, v := range p.Hashes {
 104		if last != v {
 105			last = v
 106			if cur >= max {
 107				max = cur
 108			}
 109			cur = 0
 110		}
 111		cur++
 112	}
 113	if cur >= max {
 114		max = cur
 115	}
 116	return max
 117}
 118
 119func sign(n int8) int8 {
 120	switch {
 121	case n > 0:
 122		return 1
 123	case n < 0:
 124		return -1
 125	default:
 126		return 0
 127	}
 128}
 129
 130func abs(n int8) int8 {
 131	return n * sign(n)
 132}
 133
 134// EncodeFEN encodes p into FEN.
 135// https://www.chessprogramming.org/Forsyth-Edwards_Notation
 136func (p Position) EncodeFEN() string {
 137	var s string
 138	emptyCount := 0
 139	// FEN has different ordering from us, as [0] is a black rook while for us
 140	// is a white rook. So we need to invert the order of rows.
 141	for i := 56; i >= 0; i++ {
 142		v := p.B[i]
 143		if v == PieceEmpty {
 144			emptyCount++
 145			if i%8 == 7 {
 146				s += strconv.Itoa(emptyCount) + "/"
 147				emptyCount = 0
 148				i -= 16
 149			}
 150			continue
 151		}
 152		if emptyCount > 0 {
 153			s += strconv.Itoa(emptyCount)
 154			emptyCount = 0
 155		}
 156		s += v.String()
 157		if i%8 == 7 {
 158			s += "/"
 159			i -= 16
 160		}
 161	}
 162	// remove trailing slash
 163	s = s[:len(s)-1]
 164
 165	strs := []string{
 166		s,                        // 0: piece placement
 167		"w",                      // 1: side to move
 168		p.Flags.CastlingRights(), // 2: castling ability
 169		"-",                      // 3: e.p. target square
 170
 171		strconv.Itoa(int(p.HalfMoveClock)), // 4: halfmove clock
 172		strconv.Itoa(len(p.Moves)/2 + 1),   // 5: fullmove counter
 173	}
 174
 175	var epFile byte
 176	if p.Flags&EnPassant > 0 {
 177		epFile = 'a' + byte(p.Flags&MaskEnPassant)
 178	}
 179
 180	if p.Color() == Black {
 181		strs[1] = "b"
 182		if epFile != 0 {
 183			strs[3] = string(epFile) + "3"
 184		}
 185	} else if epFile != 0 {
 186		strs[3] = string(epFile) + "6"
 187	}
 188
 189	return strings.Join(strs, " ")
 190}
 191
 192// ValidateMove checks whether the given move is legal in Chess.
 193//
 194// Caller must guarantee m.To and m.From to be valid (<64).
 195func (p Position) ValidateMove(m Move) (newP Position, valid bool) {
 196	if p.B[m.To].StripColor() == PieceKing {
 197		return
 198	}
 199
 200	return p.validateMove(m)
 201}
 202
 203// validateMove allows for m to be a "king-capture" move, which is illegal in
 204// chess, but it is useful for InCheck.
 205// This is commonly known in chess programming as a "pseudo-legal" move.
 206func (oldp Position) validateMove(m Move) (newPos Position, ok bool) {
 207	p := oldp
 208
 209	piece := p.B[m.From]
 210
 211	// piece moved must be of player's color
 212	color := p.Color()
 213	if piece == PieceEmpty || piece.Color() != color ||
 214		// additionally, check piece has actually moved
 215		m.From == m.To {
 216		return
 217	}
 218	// destination must not be occupied by piece of same color
 219	if to := p.B[m.To]; to != PieceEmpty && to.Color() == color {
 220		return
 221	}
 222
 223	// one of the two necessarily != 0 (consequence of m.From != m.To).
 224	delta := m.From.Sub(m.To)
 225	dr, dc := delta[0], delta[1]
 226
 227	// Keep old castling rights; remove en passant info.
 228	newFlags := p.Flags &^ (MaskEnPassant | EnPassant)
 229	// Marked as true for succesful promotions.
 230	var promoted bool
 231
 232	isDiag := func() bool {
 233		// move diagonally (|dr| == |dc|)
 234		if abs(dr) != abs(dc) {
 235			return false
 236		}
 237		signr, signc := sign(dr), sign(dc)
 238		// squares crossed must be empty
 239		for i := int8(1); i < abs(dr); i++ {
 240			if p.B[m.From.Move(i*signr, i*signc)] != PieceEmpty {
 241				return false
 242			}
 243		}
 244		return true
 245	}
 246	isHorizVert := func() bool {
 247		// only one of dr, dc must be 0 (horiz/vert movement)
 248		if dr != 0 && dc != 0 {
 249			return false
 250		}
 251		// squares crossed must be empty
 252		for i := int8(1); i < abs(dr); i++ {
 253			if p.B[m.From.Move(i*sign(dr), 0)] != PieceEmpty {
 254				return false
 255			}
 256		}
 257		for i := int8(1); i < abs(dc); i++ {
 258			if p.B[m.From.Move(0, i*sign(dc))] != PieceEmpty {
 259				return false
 260			}
 261		}
 262		return true
 263	}
 264
 265	switch piece.StripColor() {
 266	case PieceRook:
 267		if !isHorizVert() {
 268			return
 269		}
 270		// if rook has moved from a starting position, this disables castling
 271		// on the side of the rook. flag accordingly in the move.
 272		var fg Square
 273		if color == Black {
 274			fg = 7 << 3
 275		}
 276		switch m.From {
 277		case fg: // a-col rook (either side)
 278			if color == White {
 279				newFlags |= NoCastleWQ
 280			} else {
 281				newFlags |= NoCastleBQ
 282			}
 283		case fg | 7: // h-col rook (either side)
 284			if color == White {
 285				newFlags |= NoCastleWK
 286			} else {
 287				newFlags |= NoCastleBK
 288			}
 289		}
 290
 291	case PieceKnight:
 292		// move L-shaped
 293		// rationale: if you only have positive integers, the only way you can
 294		// obtain x * y == 2 is if x,y are either 1,2 or 2,1.
 295		if abs(dc*dr) != 2 {
 296			return
 297		}
 298
 299	case PieceBishop:
 300		if !isDiag() {
 301			return
 302		}
 303
 304	case PieceQueen:
 305		if !isHorizVert() && !isDiag() {
 306			return
 307		}
 308
 309	case PieceKing:
 310		// castling
 311		if abs(dc) == 2 && dr == 0 {
 312			// determine if castle is a valid form of castling for the given color
 313			ctype := m.isCastle(color)
 314			if ctype == 0 {
 315				return
 316			}
 317
 318			if false ||
 319				// check that there are no previous moves which disable castling
 320				p.castlingDisabled(color, ctype) ||
 321				// check that we have the exact board set ups we need
 322				// + make sure that the original and crossed squares are not in check
 323				!p.checkCastlingSetup(ctype) {
 324				return
 325			}
 326
 327			// perform rook move here
 328			p.B = p.B.castleRookMove(color, ctype)
 329			// add NoCastle flags to prevent any further castling
 330			if color == White {
 331				newFlags |= NoCastleWQ | NoCastleWK
 332			} else {
 333				newFlags |= NoCastleBQ | NoCastleBK
 334			}
 335			break
 336		}
 337		// move 1sq in all directions
 338		if dc < -1 || dc > 1 || dr < -1 || dr > 1 {
 339			return
 340		}
 341		// king has moved: disable castling.
 342		if color == White {
 343			newFlags |= NoCastleWQ | NoCastleWK
 344		} else {
 345			newFlags |= NoCastleBQ | NoCastleBK
 346		}
 347
 348	case PiecePawn:
 349		// determine direction depending on color
 350		dir := int8(1)
 351		if color == Black {
 352			dir = -1
 353		}
 354
 355		switch {
 356		case dc == 0 && dr == dir: // 1sq up
 357			// destination must be empty (no captures allowed)
 358			if p.B[m.To] != PieceEmpty {
 359				return
 360			}
 361		case dc == 0 && dr == dir*2: // 2sq up (only from starting row)
 362			wantRow := Square(1)
 363			if color == Black {
 364				wantRow = 6
 365			}
 366			// check starting row, and that two squares are empty
 367			if (m.From>>3) != wantRow ||
 368				p.B[m.From.Move(int8(dir), 0)] != PieceEmpty ||
 369				p.B[m.To] != PieceEmpty {
 370				return
 371			}
 372			_, col := m.To.Split()
 373			newFlags |= EnPassant | PositionFlags(col)
 374		case abs(dc) == 1 && dr == dir: // capture on diag
 375			// must be a capture
 376			if p.B[m.To] == PieceEmpty {
 377				if sq := p.checkEnPassant(color, m.To); sq != SquareInvalid {
 378					// remove other pawn
 379					p.B[sq] = PieceEmpty
 380					break
 381				}
 382				return
 383			}
 384			// p.B[m.To] is necessarily an opponent piece; we check & return
 385			// p.B[m.To].Color == color at the beginning of the fn.
 386		default: // not a recognized move
 387			return
 388		}
 389
 390		row := m.To >> 3
 391		if (color == White && row == 7) ||
 392			(color == Black && row == 0) {
 393			switch m.Promotion {
 394			case 0:
 395				// m.To is a king? then this is a pseudo-move check.
 396				// assume queen in that case.
 397				if p.B[m.To].StripColor() != PieceKing {
 398					// no promotion given, invalid
 399					return
 400				}
 401				m.Promotion = PieceQueen
 402			case PieceQueen, PieceBishop, PieceKnight, PieceRook:
 403			default:
 404				return
 405			}
 406			promoted = true
 407			p.B[m.From] = m.Promotion | color.Piece()
 408		}
 409	}
 410
 411	// reject moves with promotion if there's nothing to promote
 412	if m.Promotion != 0 && !promoted {
 413		return
 414	}
 415
 416	if p.B[m.To].StripColor() == PieceKing {
 417		// King captures don't check for our own king in check;
 418		// these are only "theoretical" moves.
 419		return Position{}, true
 420	}
 421
 422	// perform board mutation
 423	capture := p.B[m.To] != PieceEmpty
 424	p.B[m.From], p.B[m.To] = PieceEmpty, p.B[m.From]
 425	p.Flags = newFlags
 426	p.Moves = append([]Move{}, p.Moves...)
 427	p.Moves = append(p.Moves, m)
 428
 429	// halfmove clock + hashes logic
 430	if piece.StripColor() == PiecePawn || capture {
 431		// reset both
 432		p.HalfMoveClock = 0
 433		p.Hashes = nil
 434	} else {
 435		p.HalfMoveClock++
 436	}
 437	ep := byte(255)
 438	if p.Flags&EnPassant != 0 {
 439		ep = byte(p.Flags & MaskEnPassant)
 440	}
 441	// Not ideal, but avoids GenMoves potentially polluting "real moves" hashes.
 442	p.Hashes = append([]uint64{}, p.Hashes...)
 443	p.Hashes = append(
 444		p.Hashes,
 445		// color is inverted, because we consider the present move as already
 446		// done (hence, it is the other player to move).
 447		zobrist.Hash(toZobristBoard(p.B), bool(!color), byte(p.Flags)>>4, ep),
 448	)
 449
 450	// is our king in check, as a result of the current move?
 451	if p.B.InCheck(color) {
 452		return
 453	}
 454	return p, true
 455}
 456
 457func toZobristBoard(src Board) zobrist.Board {
 458	var zb zobrist.Board
 459	for pos, piece := range src {
 460		zb[pos] = zobrist.Piece(piece)
 461	}
 462	return zb
 463}
 464
 465// used by InCheck to simulate a move by black player.
 466var blackPrevMoves = make([]Move, 1)
 467
 468// InCheck checks whether the king with the given color is in check.
 469// If such king does not exist on the board, InCheck returns false.
 470//
 471// A king is in check if the move from a piece of the other color
 472// towards the king is valid, ignoring any checks on the other color's king.
 473//
 474// NOTE: the last remark is important:
 475// https://lichess.org/analysis/4k3/8/4b3/8/8/8/K3R3/8_w_-_-_0_1?color=white
 476// -- this is still a check for white, even if _technically_ black couldn't
 477// move the bishop (as that would check its own king)
 478func (b Board) InCheck(color Color) bool {
 479	pWant := PieceKing | color.Piece()
 480	kingp := b.findPiece(pWant)
 481	if kingp == SquareInvalid {
 482		return false
 483	}
 484
 485	pos := Position{B: b}
 486	if color == White {
 487		// color == White -> simulate a move by black player -> pos.Moves odd
 488		pos.Moves = blackPrevMoves
 489	}
 490
 491	for sq, piece := range b {
 492		if piece == PieceEmpty || piece.Color() == color {
 493			continue
 494		}
 495		_, ok := pos.validateMove(Move{
 496			From: Square(sq),
 497			To:   kingp,
 498			// validateMove (unexp) understands that moves to capture a king are
 499			// pseudo moves, so it doesn't check for checking on its own king,
 500			// or promotion.
 501		})
 502		if ok {
 503			return true
 504		}
 505	}
 506
 507	return false
 508}
 509
 510// Board is a representation of a chess board.
 511// Details on how to transform a chess algebraic position into an index
 512// can be found at [Square].
 513type Board [64]Piece
 514
 515// NewBoard returns a Board normally set up at the initial position for standard
 516// chess.
 517func NewBoard() Board {
 518	return Board{
 519		// row 1
 520		p['R'], p['N'], p['B'], p['Q'],
 521		p['K'], p['B'], p['N'], p['R'],
 522		// row 2
 523		p['P'], p['P'], p['P'], p['P'],
 524		p['P'], p['P'], p['P'], p['P'],
 525
 526		// rows 3, 4, 5, 6
 527		0, 0, 0, 0, 0, 0, 0, 0,
 528		0, 0, 0, 0, 0, 0, 0, 0,
 529		0, 0, 0, 0, 0, 0, 0, 0,
 530		0, 0, 0, 0, 0, 0, 0, 0,
 531
 532		// row 7
 533		p['p'], p['p'], p['p'], p['p'],
 534		p['p'], p['p'], p['p'], p['p'],
 535		// row 8
 536		p['r'], p['n'], p['b'], p['q'],
 537		p['k'], p['b'], p['n'], p['r'],
 538	}
 539}
 540
 541func (b Board) findPiece(pWant Piece) Square {
 542	for sq, p := range b {
 543		if p == pWant {
 544			return Square(sq)
 545		}
 546	}
 547	return SquareInvalid
 548}
 549
 550func (p Position) checkCastlingSetup(typ byte) bool {
 551	// set up correct row and piece flags according to color
 552	c := p.Color()
 553	b := p.B
 554	var fg Square
 555	var pfg Piece
 556	if c == Black {
 557		fg, pfg = 7<<3, PieceBlack
 558	}
 559
 560	// _cross are the squares that the king starts from,
 561	// crosses and "lands". they are recorded as they must all be
 562	// not in check by any opponent piece.
 563	var _cross [3]Square
 564
 565	if typ == 'K' {
 566		if !(b[fg|4] == pfg|PieceKing &&
 567			b[fg|5] == PieceEmpty &&
 568			b[fg|6] == PieceEmpty &&
 569			b[fg|7] == pfg|PieceRook) {
 570			return false
 571		}
 572		_cross = [3]Square{fg | 4, fg | 5, fg | 6}
 573	} else {
 574		if !(b[fg|4] == pfg|PieceKing &&
 575			b[fg|3] == PieceEmpty &&
 576			b[fg|2] == PieceEmpty &&
 577			b[fg|1] == PieceEmpty &&
 578			b[fg|0] == pfg|PieceRook) {
 579			return false
 580		}
 581		_cross = [3]Square{fg | 4, fg | 3, fg | 2}
 582	}
 583
 584	testb := p.B
 585	for _, sq := range _cross {
 586		testb[sq] = pfg | PieceKing
 587		if testb.InCheck(c) {
 588			return false
 589		}
 590		testb[sq] = PieceEmpty
 591	}
 592
 593	return true
 594}
 595
 596func (b Board) castleRookMove(c Color, typ byte) Board {
 597	var fg Square
 598	var pfg Piece
 599	if c == Black {
 600		fg, pfg = 7<<3, PieceBlack
 601	}
 602
 603	if typ == 'K' {
 604		b[fg|7], b[fg|5] = PieceEmpty, PieceRook|pfg
 605	} else {
 606		b[fg|0], b[fg|3] = PieceEmpty, PieceRook|pfg
 607	}
 608	return b
 609}
 610
 611func (p Position) castlingDisabled(color Color, kind byte) bool {
 612	if kind != 'K' && kind != 'Q' {
 613		return false
 614	}
 615
 616	// Determine what flag we're looking for.
 617	var want PositionFlags
 618	switch {
 619	case color == White && kind == 'K':
 620		want = NoCastleWK
 621	case color == White && kind == 'Q':
 622		want = NoCastleWQ
 623	case color == Black && kind == 'K':
 624		want = NoCastleBK
 625	case color == Black && kind == 'Q':
 626		want = NoCastleBQ
 627	}
 628
 629	return p.Flags&want != 0
 630}
 631
 632func (p Position) checkEnPassant(c Color, sq Square) Square {
 633	row, col := sq.Split()
 634	if p.Flags&EnPassant == 0 ||
 635		(c == White && row != 5) ||
 636		(c == Black && row != 2) {
 637		return SquareInvalid
 638	}
 639	wantCol := byte(p.Flags & MaskEnPassant)
 640
 641	if col != wantCol {
 642		return SquareInvalid
 643	}
 644
 645	if c == White {
 646		return Square(4<<3 | col)
 647	}
 648	return Square(3<<3 | col)
 649}
 650
 651// InsufficientMaterial tests for insufficient material on the board, in which
 652// case it returns true.
 653//
 654// See this reference for the rules used:
 655// https://www.chess.com/article/view/how-chess-games-can-end-8-ways-explained#insufficient-material
 656//
 657//   - king vs king
 658//   - king+N/B vs king
 659//   - king+NN vs king
 660//   - king+N/B vs king+N/B
 661func (bd Board) InsufficientMaterial() bool {
 662	// strategy:
 663	// store the pieces which could count for an insufficient material
 664	// scenario in w, b.
 665	// if we encounter any pawn, queen, or rook, material is sufficient.
 666	// if we encounter 2 bishops, material is sufficient.
 667	// afterwards, we verify that w and b are one of the possible insuf material
 668	// scenarios.
 669
 670	const (
 671		imN  byte = 1 << iota // knight
 672		imN2                  // second knight
 673		imB                   // bishop
 674	)
 675	var w, b byte
 676	for _, p := range bd {
 677		strip := p.StripColor()
 678		switch strip {
 679		case PieceQueen, PiecePawn, PieceRook:
 680			return false
 681		case PieceKing, PieceEmpty:
 682			continue
 683		}
 684		// strip is one of PieceBishop PieceKnight
 685		t := &w
 686		if p.Color() == Black {
 687			t = &b
 688		}
 689		if strip == PieceBishop {
 690			if *t&imB != 0 {
 691				return false
 692			}
 693			*t |= imB
 694		} else {
 695			if *t&imN2 != 0 {
 696				// third knight (ie. from pawn promotion)
 697				return false
 698			}
 699			if *t&imN != 0 {
 700				*t |= imN2
 701			} else {
 702				*t |= imN
 703			}
 704		}
 705	}
 706	// make it so that w is bigger than b
 707	if b > w {
 708		w, b = b, w
 709	}
 710	switch [2]byte{w, b} {
 711	case [2]byte{0, 0}, // king vs king
 712		[2]byte{imN, 0}, // king+N/B vs king
 713		[2]byte{imB, 0},
 714		[2]byte{imN | imN2, 0}, // king+NN vs king
 715		[2]byte{imN, imN},      // king+N/B vs king+N/B
 716		[2]byte{imB, imB},
 717		[2]byte{imB, imN}: // imB > imN, so [2]byte{imN, imB} cannot happen
 718		return true
 719	}
 720	return false
 721}
 722
 723// GenMoves implements a rudimentary move generator.
 724// This is not used beyond aiding in determing stalemate and doing perft tests.
 725// Each generated move is passed to cb.
 726// If cb returns an error, it is returned without processing further moves.
 727func (p Position) GenMoves(cb func(Position, Move) error) error {
 728	color := p.Color()
 729	for sq, piece := range p.B {
 730		if piece == PieceEmpty || piece.Color() != color {
 731			continue
 732		}
 733
 734		from := Square(sq)
 735
 736		pstrip := piece.StripColor()
 737		// If the piece is a pawn, and they are on the second last row, we know
 738		// that whatever move they do (advance, or take diagonally) they're going
 739		// to promote.
 740		prom := pstrip == PiecePawn &&
 741			((color == White && from>>3 == 6) ||
 742				(color == Black && from>>3 == 1))
 743
 744		// delta generator needs to know if p is black
 745		if pstrip == PiecePawn && color == Black {
 746			pstrip |= Black.Piece()
 747		}
 748
 749		var err error
 750		deltaGenerator(pstrip, func(delta Delta) byte {
 751			// create move; if the resulting square is oob, continue
 752			m := Move{
 753				From: from,
 754				To:   from.Apply(delta),
 755			}
 756			if m.To == SquareInvalid ||
 757				(p.B[m.To] != PieceEmpty && p.B[m.To].Color() == color) {
 758				return deltaGenStopLinear
 759			}
 760
 761			// handle promotion case
 762			if prom {
 763				m.Promotion = PieceQueen
 764			}
 765
 766			// if it's a valid move, call cb on it
 767			newp, ok := p.ValidateMove(m)
 768			if !ok {
 769				return deltaGenOK
 770			}
 771			if err = cb(newp, m); err != nil {
 772				return deltaGenStop
 773			}
 774
 775			// if we've promoted, handle the cases where we've promoted to a non-queen.
 776			if !prom {
 777				return deltaGenOK
 778			}
 779
 780			for _, promPiece := range [...]Piece{PieceRook, PieceKnight, PieceBishop} {
 781				newp.B[m.To] = promPiece | color.Piece()
 782				m.Promotion = promPiece
 783				if err = cb(newp, m); err != nil {
 784					return deltaGenStop
 785				}
 786			}
 787			return deltaGenOK
 788		})
 789		if err != nil {
 790			return err
 791		}
 792	}
 793	return nil
 794}
 795
 796const (
 797	// carry on normally
 798	deltaGenOK = iota
 799	// if the generator is doing a linear attack (ie. rook, bishop, queen),
 800	// then stop that (there is a piece of same colour in the way.)
 801	deltaGenStopLinear
 802	// abort generation asap.
 803	deltaGenStop
 804)
 805
 806/*func init() {
 807	for i := PiecePawn; i <= PieceKing; i++ {
 808		println("generator ", i.String())
 809		deltaGenerator(i, func(d Delta) byte {
 810			println("  ", d[0], d[1])
 811			return deltaGenOK
 812		})
 813	}
 814}*/
 815
 816// deltaGenerator generates the possible ways in which p can move.
 817// the callback may return one of the three deltaGen* values.
 818func deltaGenerator(p Piece, cb func(d Delta) byte) {
 819	doLinear := func(d Delta) bool {
 820		for i := int8(1); i <= 7; i++ {
 821			switch cb(d.Mul(i)) {
 822			case deltaGenStopLinear:
 823				return false
 824			case deltaGenStop:
 825				return true
 826			}
 827		}
 828		return false
 829	}
 830	rotate := func(d Delta, lin bool) bool {
 831		for i := 0; i < 4; i++ {
 832			if lin {
 833				if doLinear(d) {
 834					return true
 835				}
 836			} else {
 837				if cb(d) == deltaGenStop {
 838					return true
 839				}
 840			}
 841
 842			d = d.Rot()
 843		}
 844		return false
 845	}
 846
 847	// In the following, we use logical OR's to do conditional evaluation
 848	// (if the first item returns true, the second won't be evaluated)
 849	switch p {
 850	case PiecePawn, PiecePawn | PieceBlack:
 851		dir := int8(1)
 852		if p.Color() == Black {
 853			dir = -1
 854		}
 855		// try moving 1sq forward; if we get StopLinear, don't try to do 2sq.
 856		fw := cb(Delta{dir, 0})
 857		if fw == deltaGenStop {
 858			return
 859		}
 860		if fw != deltaGenStopLinear {
 861			if cb(Delta{dir * 2, 0}) == deltaGenStop {
 862				return
 863			}
 864		}
 865
 866		_ = cb(Delta{dir, 1}) == deltaGenStop ||
 867			cb(Delta{dir, -1}) == deltaGenStop
 868
 869	case PieceRook:
 870		rotate(Delta{0, 1}, true)
 871	case PieceBishop:
 872		rotate(Delta{1, 1}, true)
 873	case PieceKnight:
 874		_ = rotate(Delta{1, 2}, false) ||
 875			rotate(Delta{2, 1}, false)
 876	case PieceQueen:
 877		_ = rotate(Delta{0, 1}, true) ||
 878			rotate(Delta{1, 1}, true)
 879	case PieceKing:
 880		_ = rotate(Delta{0, 1}, false) ||
 881			rotate(Delta{1, 1}, false) ||
 882			cb(Delta{0, 2}) == deltaGenStop ||
 883			cb(Delta{0, -2}) == deltaGenStop
 884	}
 885}
 886
 887// Outcome is returned by IsFinished, and indicates if the game has finished,
 888// and additionally if either of the player can claim draw by the 50-move rule
 889// or by 3-fold repetition.
 890//
 891// Either one of the sequential outcomes will be set (values 1-5), indicating
 892// that the game is terminated, or the low 3 bits will be 0, and
 893// the Can* flags may be set.
 894type Outcome byte
 895
 896const (
 897	NotFinished Outcome = iota
 898	Checkmate
 899	Stalemate
 900	Drawn75Move
 901	Drawn5Fold
 902
 903	Can50Move       Outcome = 8
 904	Can3Fold        Outcome = 16
 905	CanInsufficient Outcome = 32
 906)
 907
 908var errFound = errors.New("found")
 909
 910// IsFinished determines if the game at the given position can be considered
 911// "finished".
 912// See [Outcome] for the possible results.
 913func (p Position) IsFinished() Outcome {
 914	err := p.GenMoves(func(Position, Move) error {
 915		return errFound
 916	})
 917	// If there is any legal move, this is not any kind of mate.
 918	if err != nil {
 919		mhc := p.maxHashCount()
 920		switch {
 921		case mhc >= 5:
 922			return Drawn5Fold
 923		case p.HalfMoveClock >= 150:
 924			return Drawn75Move
 925		}
 926		var o Outcome
 927		if mhc >= 3 {
 928			o |= Can3Fold
 929		}
 930		if p.HalfMoveClock >= 100 {
 931			o |= Can50Move
 932		}
 933		if p.B.InsufficientMaterial() {
 934			o |= CanInsufficient
 935		}
 936		return o
 937	}
 938
 939	// No legal moves. Is the king in check?
 940	if p.B.InCheck(p.Color()) {
 941		return Checkmate
 942	}
 943	return Stalemate
 944}
 945
 946// Color determines a player's color -- either white or black.
 947type Color bool
 948
 949const (
 950	White Color = false
 951	Black Color = true
 952)
 953
 954// Piece returns the color as a piece to be OR'd into a Piece;
 955// ie. 0 on White, and [PieceBlack] on black.
 956func (c Color) Piece() Piece {
 957	if c == White {
 958		return 0
 959	}
 960	return PieceBlack
 961}
 962
 963// Piece represents a piece on the board.
 964type Piece byte
 965
 966// PieceFromChar returns the piece corresponding to the given character.
 967// White pieces are uppercase, and black pieces are lowercase.
 968// If a piece is invalid, PieceInvalid is returned.
 969func PieceFromChar(b byte) Piece {
 970	return p[b]
 971}
 972
 973// piece character to internal piece
 974var p = [256]Piece{
 975	'P': PiecePawn,
 976	'R': PieceRook,
 977	'N': PieceKnight,
 978	'B': PieceBishop,
 979	'Q': PieceQueen,
 980	'K': PieceKing,
 981
 982	'p': PieceBlack | PiecePawn,
 983	'r': PieceBlack | PieceRook,
 984	'n': PieceBlack | PieceKnight,
 985	'b': PieceBlack | PieceBishop,
 986	'q': PieceBlack | PieceQueen,
 987	'k': PieceBlack | PieceKing,
 988}
 989
 990var pstring = [PieceBlack | PieceKing + 1]byte{
 991	PiecePawn:                'P',
 992	PieceRook:                'R',
 993	PieceKnight:              'N',
 994	PieceBishop:              'B',
 995	PieceQueen:               'Q',
 996	PieceKing:                'K',
 997	PieceBlack | PiecePawn:   'p',
 998	PieceBlack | PieceRook:   'r',
 999	PieceBlack | PieceKnight: 'n',
1000	PieceBlack | PieceBishop: 'b',
1001	PieceBlack | PieceQueen:  'q',
1002	PieceBlack | PieceKing:   'k',
1003}
1004
1005func (p Piece) String() string {
1006	if int(p) >= len(pstring) {
1007		return ""
1008	}
1009	v := pstring[p]
1010	if v == 0 {
1011		return ""
1012	}
1013	return string(v)
1014}
1015
1016// Possible values of Piece. Within the context of Board, Piece is assumed to
1017// be white, unless p&PieceBlack != 0. Note PieceBlack is not a valid piece; it
1018// must be bitwise OR'd to a non-empty piece.
1019const (
1020	PieceEmpty Piece = iota
1021
1022	PiecePawn
1023	PieceRook
1024	PieceKnight
1025	PieceBishop
1026	PieceQueen
1027	PieceKing
1028
1029	PieceBlack Piece = 8 // bit-flag
1030)
1031
1032// Color returns the color of the piece.
1033func (p Piece) Color() Color { return Color(p&PieceBlack != 0) }
1034
1035// Piece returns the given Piece without color information.
1036func (p Piece) StripColor() Piece { return p &^ PieceBlack }
1037
1038// Switch switches the color of the given piece.
1039func (p Piece) Switch() Piece {
1040	if p.Color() == Black {
1041		return p &^ PieceBlack
1042	}
1043	return p | PieceBlack
1044}
1045
1046// Delta represents a 2d vector for indicating a movement from one square
1047// to another. The first value indicates the change in column, the second the
1048// change in rows.
1049type Delta [2]int8
1050
1051// Valid ensures the two values of delta are valid.
1052func (d Delta) Valid() bool {
1053	return d[0] >= -7 && d[0] <= 7 &&
1054		d[1] >= -7 && d[1] <= 7 &&
1055		!(d[0] == 0 && d[1] == 0)
1056}
1057
1058// Rot applies a 90 degree anti-clockwise rotation to d.
1059func (d Delta) Rot() Delta {
1060	// Rationale: this is just matrix-vector multiplication.
1061	// 90 deg rotation is just the matrix {0, -1; 1, 0}.
1062	return Delta{d[1], -d[0]}
1063}
1064
1065// Mul multiplies both values by n, otherwise known as scalar product.
1066func (d Delta) Mul(n int8) Delta {
1067	return Delta{d[0] * n, d[1] * n}
1068}
1069
1070// Square encodes piece position information, in chess the "square" the piece is on.
1071// Indexing 0 as the LSB, bits 0-3 indicate the column and bits 4-6 indicate
1072// the row. For instance, square 44 (decimal) is:
1073//
1074//	44 = 0b00 101  100  = d5
1075//	          ^row ^col
1076//
1077// (note: in algebraic notation, this is swapped: the letter represents the
1078// column, and the number represents the row).
1079type Square byte
1080
1081// SquareInvalid is returned by some Square-related methods to indicate
1082// invalid parameters.
1083const SquareInvalid Square = 255
1084
1085// String returns p in algebraic notation.
1086func (q Square) String() string {
1087	if q >= 64 {
1088		return "<invalid>"
1089	}
1090	return string(q&7+'a') + string(q>>3+'1')
1091}
1092
1093// SquareFromString returns Square, reading the human-readable algebraic
1094// notation in s. s must be 2 bytes long, with the first byte a letter included
1095// between ['a'; 'h'], and the second a number included between ['1';'8'].
1096// If s is invalid, SquareInvalid is returned.
1097func SquareFromString(s string) Square {
1098	if len(s) != 2 {
1099		return SquareInvalid
1100	}
1101	col, row := s[0]-'a', s[1]-'1'
1102	// because s[0] is a byte, if s[0] < 'a' then the above will underflow and
1103	// row will be >= 8 (same for col).
1104	if row >= 8 || col >= 8 {
1105		return SquareInvalid
1106	}
1107	return Square(row<<3 | col)
1108}
1109
1110// Move changes the square of q, moving it vertically according to dr
1111// (delta row) and horizontally according to dc (delta column).
1112// If the resulting square is not on the board, then SquareInvalid is returned.
1113func (q Square) Move(dr, dc int8) Square {
1114	if q == SquareInvalid || !(Delta{dr, dc}).Valid() {
1115		return SquareInvalid
1116	}
1117
1118	row, col := int8(q>>3), int8(q&7)
1119	row += dr
1120	col += dc
1121
1122	nr, nc := Square(row), Square(col)
1123	if nr >= 8 || nc >= 8 {
1124		return SquareInvalid
1125	}
1126	return nr<<3 | nc
1127}
1128
1129// Apply applies the given delta to the square.
1130// It is shorthand for q.Move(d[0], d[1]).
1131func (q Square) Apply(d Delta) Square { return q.Move(d[0], d[1]) }
1132
1133// Split splits Square into its components.
1134// This function does not check if p is invalid.
1135func (q Square) Split() (row, col byte) {
1136	return byte(q >> 3), byte(q & 7)
1137}
1138
1139// SplitI works like [Square.Split], but returns int8's instead
1140// of bytes.
1141func (q Square) SplitI() (row, col int8) {
1142	return int8(q >> 3), int8(q & 7)
1143}
1144
1145// Sub calculates the difference between the two squares.
1146// q is the originating square, s is the ending square. The difference in
1147// rows and columns from q to s is returned; for instance, d1.Sub(a4) yields
1148// Delta{3, -3}.
1149func (q Square) Sub(s Square) Delta {
1150	fr, fc := q.SplitI()
1151	tr, tc := s.SplitI()
1152	return Delta{tr - fr, tc - fc}
1153}
1154
1155// Move represents a chess game move.
1156type Move struct {
1157	From, To  Square
1158	Promotion Piece
1159}
1160
1161// String returns a string representation of Move.
1162// It is in the form of "Long Algebraic Notation".
1163// https://backscattering.de/chess/uci/#move-lan
1164func (m Move) String() string {
1165	p := ""
1166	if m.Promotion != 0 {
1167		p = string(m.Promotion.String()[0] + ('a' - 'A'))
1168	}
1169	return m.From.String() + m.To.String() + p
1170}
1171
1172var (
1173	castleWhiteQ = Move{From: SquareFromString("e1"), To: SquareFromString("c1")}
1174	castleWhiteK = Move{From: SquareFromString("e1"), To: SquareFromString("g1")}
1175	castleBlackQ = Move{From: SquareFromString("e8"), To: SquareFromString("c8")}
1176	castleBlackK = Move{From: SquareFromString("e8"), To: SquareFromString("g8")}
1177)
1178
1179// returns 0, 'K' or 'Q'.
1180func (m Move) isCastle(c Color) (kind byte) {
1181	if c == White {
1182		switch m {
1183		case castleWhiteQ:
1184			return 'Q'
1185		case castleWhiteK:
1186			return 'K'
1187		}
1188	} else {
1189		switch m {
1190		case castleBlackQ:
1191			return 'Q'
1192		case castleBlackK:
1193			return 'K'
1194		}
1195	}
1196	return 0
1197}