Files
chess/internal/server/chess/san.go
T

180 lines
4.2 KiB
Go

package chess
import (
"fmt"
"strings"
)
// maxSANLength bounds parser input; the longest SAN is "Qa1xb2+" plus
// annotation glyphs.
const maxSANLength = 16
// SAN returns Standard Algebraic Notation for a legal move, with "+" or "#".
func (p *Position) SAN(m Move) string {
return p.san(m, p.LegalMoves())
}
func (p *Position) san(m Move, legal []Move) string {
var b strings.Builder
piece := p.board[m.From]
switch {
case piece.Kind() == King && m.To-m.From == 2:
b.WriteString("O-O")
case piece.Kind() == King && m.From-m.To == 2:
b.WriteString("O-O-O")
case piece.Kind() == Pawn:
if m.From.File() != m.To.File() {
b.WriteByte(byte('a' + m.From.File()))
b.WriteByte('x')
}
b.WriteString(m.To.String())
if m.Promotion != NoKind {
b.WriteByte('=')
b.WriteByte(kindLetters[m.Promotion])
}
default:
b.WriteByte(kindLetters[piece.Kind()])
b.WriteString(p.disambiguation(m, legal))
if p.board[m.To] != NoPiece {
b.WriteByte('x')
}
b.WriteString(m.To.String())
}
next := p.play(m)
if next.InCheck() {
if next.HasLegalMoves() {
b.WriteByte('+')
} else {
b.WriteByte('#')
}
}
return b.String()
}
// disambiguation returns the file, rank, or square needed to tell m apart
// from other legal moves of the same piece kind to the same square.
func (p *Position) disambiguation(m Move, legal []Move) string {
kind := p.board[m.From].Kind()
ambiguous, sameFile, sameRank := false, false, false
for _, other := range legal {
if other.To != m.To || other.From == m.From || p.board[other.From].Kind() != kind {
continue
}
ambiguous = true
if other.From.File() == m.From.File() {
sameFile = true
}
if other.From.Rank() == m.From.Rank() {
sameRank = true
}
}
switch {
case !ambiguous:
return ""
case !sameFile:
return string(byte('a' + m.From.File()))
case !sameRank:
return string(byte('1' + m.From.Rank()))
default:
return m.From.String()
}
}
// ParseSAN resolves a SAN move against the legal moves. It accepts check and
// annotation suffixes, "0-0" castling, promotion with or without "=", and
// redundant disambiguation.
func (p *Position) ParseSAN(s string) (Move, error) {
if len(s) == 0 || len(s) > maxSANLength {
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
}
text := strings.TrimRight(s, "+#!?")
legal := p.LegalMoves()
switch text {
case "O-O", "0-0", "O-O-O", "0-0-0":
kingFrom, delta := sqE1, Square(2)
if p.turn == Black {
kingFrom = sqE8
}
if len(text) == 5 {
delta = -2
}
for _, m := range legal {
if m.From == kingFrom && m.To == kingFrom+delta && p.board[m.From].Kind() == King {
return m, nil
}
}
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
}
kind := Pawn
if len(text) > 0 && strings.IndexByte("NBRQK", text[0]) >= 0 {
kind = kindOfLetter(text[0])
text = text[1:]
}
promotion := NoKind
if n := len(text); n >= 2 && strings.IndexByte("NBRQ", text[n-1]) >= 0 {
promotion = kindOfLetter(text[n-1])
text = strings.TrimSuffix(text[:n-1], "=")
}
if len(text) < 2 {
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
}
to, err := ParseSquare(text[len(text)-2:])
if err != nil {
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
}
text = strings.TrimSuffix(text[:len(text)-2], "x")
fromFile, fromRank := -1, -1
for i := 0; i < len(text); i++ {
switch c := text[i]; {
case c >= 'a' && c <= 'h' && fromFile < 0 && fromRank < 0:
fromFile = int(c - 'a')
case c >= '1' && c <= '8' && fromRank < 0:
fromRank = int(c - '1')
default:
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
}
}
var found Move
matches := 0
for _, m := range legal {
if m.To != to || m.Promotion != promotion || p.board[m.From].Kind() != kind {
continue
}
if fromFile >= 0 && m.From.File() != fromFile || fromRank >= 0 && m.From.Rank() != fromRank {
continue
}
found = m
matches++
}
switch matches {
case 1:
return found, nil
case 0:
return Move{}, fmt.Errorf("%w: %q", ErrIllegalMove, s)
default:
return Move{}, fmt.Errorf("%w: ambiguous %q", ErrIllegalMove, s)
}
}
func kindOfLetter(c byte) Kind {
switch c {
case 'N':
return Knight
case 'B':
return Bishop
case 'R':
return Rook
case 'Q':
return Queen
case 'K':
return King
}
return NoKind
}