You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
10 KiB
403 lines
10 KiB
4 years ago
|
package chess
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
startFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||
|
)
|
||
|
|
||
|
// A Outcome is the result of a game.
|
||
|
type Outcome string
|
||
|
|
||
|
const (
|
||
|
// NoOutcome indicates that a game is in progress or ended without a result.
|
||
|
NoOutcome Outcome = "*"
|
||
|
// WhiteWon indicates that white won the game.
|
||
|
WhiteWon Outcome = "1-0"
|
||
|
// BlackWon indicates that black won the game.
|
||
|
BlackWon Outcome = "0-1"
|
||
|
// Draw indicates that game was a draw.
|
||
|
Draw Outcome = "1/2-1/2"
|
||
|
)
|
||
|
|
||
|
// String implements the fmt.Stringer interface
|
||
|
func (o Outcome) String() string {
|
||
|
return string(o)
|
||
|
}
|
||
|
|
||
|
// A Method is the method that generated the outcome.
|
||
|
type Method uint8
|
||
|
|
||
|
const (
|
||
|
// NoMethod indicates that an outcome hasn't occurred or that the method can't be determined.
|
||
|
NoMethod Method = iota
|
||
|
// Checkmate indicates that the game was won checkmate.
|
||
|
Checkmate
|
||
|
// Resignation indicates that the game was won by resignation.
|
||
|
Resignation
|
||
|
// DrawOffer indicates that the game was drawn by a draw offer.
|
||
|
DrawOffer
|
||
|
// Stalemate indicates that the game was drawn by stalemate.
|
||
|
Stalemate
|
||
|
// ThreefoldRepetition indicates that the game was drawn when the game
|
||
|
// state was repeated three times and a player requested a draw.
|
||
|
ThreefoldRepetition
|
||
|
// FivefoldRepetition indicates that the game was automatically drawn
|
||
|
// by the game state being repeated five times.
|
||
|
FivefoldRepetition
|
||
|
// FiftyMoveRule indicates that the game was drawn by the half
|
||
|
// move clock being one hundred or greater when a player requested a draw.
|
||
|
FiftyMoveRule
|
||
|
// SeventyFiveMoveRule indicates that the game was automatically drawn
|
||
|
// when the half move clock was one hundred and fifty or greater.
|
||
|
SeventyFiveMoveRule
|
||
|
// InsufficientMaterial indicates that the game was automatically drawn
|
||
|
// because there was insufficient material for checkmate.
|
||
|
InsufficientMaterial
|
||
|
)
|
||
|
|
||
|
// TagPair represents metadata in a key value pairing used in the PGN format.
|
||
|
type TagPair struct {
|
||
|
Key string
|
||
|
Value string
|
||
|
}
|
||
|
|
||
|
// A Game represents a single chess game.
|
||
|
type Game struct {
|
||
|
Notation Notation
|
||
|
TagPairs []*TagPair
|
||
|
Moves []*Move
|
||
|
Positions []*Position
|
||
|
Pos *Position
|
||
|
Outcome Outcome
|
||
|
Method Method
|
||
|
IgnoreAutomaticDraws bool
|
||
|
}
|
||
|
|
||
|
// PGN takes a reader and returns a function that updates
|
||
|
// the game to reflect the PGN data. The PGN can use any
|
||
|
// move notation supported by this package. The returned
|
||
|
// function is designed to be used in the NewGame constructor.
|
||
|
// An error is returned if there is a problem parsing the PGN data.
|
||
|
func PGN(r io.Reader) (func(*Game), error) {
|
||
|
b, err := ioutil.ReadAll(r)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
game, err := decodePGN(string(b))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return func(g *Game) {
|
||
|
g.copy(game)
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// FEN takes a string and returns a function that updates
|
||
|
// the game to reflect the FEN data. Since FEN doesn't encode
|
||
|
// prior moves, the move list will be empty. The returned
|
||
|
// function is designed to be used in the NewGame constructor.
|
||
|
// An error is returned if there is a problem parsing the FEN data.
|
||
|
func FEN(fen string) (func(*Game), error) {
|
||
|
pos, err := decodeFEN(fen)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return func(g *Game) {
|
||
|
pos.inCheck = isInCheck(pos)
|
||
|
g.Pos = pos
|
||
|
g.Positions = []*Position{pos}
|
||
|
g.updatePosition()
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// TagPairs returns a function that sets the tag pairs
|
||
|
// to the given value. The returned function is designed
|
||
|
// to be used in the NewGame constructor.
|
||
|
func TagPairs(tagPairs []*TagPair) func(*Game) {
|
||
|
return func(g *Game) {
|
||
|
g.TagPairs = append([]*TagPair(nil), tagPairs...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// UseNotation returns a function that sets the game's notation
|
||
|
// to the given value. The notation is used to parse the
|
||
|
// string supplied to the MoveStr() method as well as the
|
||
|
// any PGN output. The returned function is designed
|
||
|
// to be used in the NewGame constructor.
|
||
|
func UseNotation(n Notation) func(*Game) {
|
||
|
return func(g *Game) {
|
||
|
g.Notation = n
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewGame defaults to returning a game in the standard
|
||
|
// opening position. Options can be given to configure
|
||
|
// the game's initial state.
|
||
|
func NewGame(options ...func(*Game)) *Game {
|
||
|
pos, _ := decodeFEN(startFEN)
|
||
|
game := &Game{
|
||
|
Notation: AlgebraicNotation{},
|
||
|
Moves: []*Move{},
|
||
|
Pos: pos,
|
||
|
Positions: []*Position{pos},
|
||
|
Outcome: NoOutcome,
|
||
|
Method: NoMethod,
|
||
|
}
|
||
|
for _, f := range options {
|
||
|
if f != nil {
|
||
|
f(game)
|
||
|
}
|
||
|
}
|
||
|
return game
|
||
|
}
|
||
|
|
||
|
// Move updates the game with the given move. An error is returned
|
||
|
// if the move is invalid or the game has already been completed.
|
||
|
func (g *Game) Move(m *Move) error {
|
||
|
valid := moveSlice(g.ValidMoves()).find(m)
|
||
|
if valid == nil {
|
||
|
return fmt.Errorf("chess: invalid move %s", m)
|
||
|
}
|
||
|
g.Moves = append(g.Moves, valid)
|
||
|
g.Pos = g.Pos.Update(valid)
|
||
|
g.Positions = append(g.Positions, g.Pos)
|
||
|
g.updatePosition()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// MoveStr decodes the given string in game's notation
|
||
|
// and calls the Move function. An error is returned if
|
||
|
// the move can't be decoded or the move is invalid.
|
||
|
func (g *Game) MoveStr(s string) error {
|
||
|
m, err := g.Notation.Decode(g.Pos, s)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return g.Move(m)
|
||
|
}
|
||
|
|
||
|
// ValidMoves returns a list of valid moves in the
|
||
|
// current position.
|
||
|
func (g *Game) ValidMoves() []*Move {
|
||
|
return g.Pos.ValidMoves()
|
||
|
}
|
||
|
|
||
|
// PositionsHistory returns the position history of the game.
|
||
|
func (g *Game) PositionsHistory() []*Position {
|
||
|
return append([]*Position(nil), g.Positions...)
|
||
|
}
|
||
|
|
||
|
// MovesHistory returns the move history of the game.
|
||
|
func (g *Game) MovesHistory() []*Move {
|
||
|
return append([]*Move(nil), g.Moves...)
|
||
|
}
|
||
|
|
||
|
// GetTagPairs returns the game's tag pairs.
|
||
|
func (g *Game) GetTagPairs() []*TagPair {
|
||
|
return append([]*TagPair(nil), g.TagPairs...)
|
||
|
}
|
||
|
|
||
|
// Position returns the game's current position.
|
||
|
func (g *Game) Position() *Position {
|
||
|
return g.Pos
|
||
|
}
|
||
|
|
||
|
// GameOutcome returns the game outcome.
|
||
|
func (g *Game) GameOutcome() Outcome {
|
||
|
return g.Outcome
|
||
|
}
|
||
|
|
||
|
// OutcomeMethod returns the method in which the outcome occurred.
|
||
|
func (g *Game) OutcomeMethod() Method {
|
||
|
return g.Method
|
||
|
}
|
||
|
|
||
|
// FEN returns the FEN notation of the current position.
|
||
|
func (g *Game) FEN() string {
|
||
|
return g.Pos.String()
|
||
|
}
|
||
|
|
||
|
// String implements the fmt.Stringer interface and returns
|
||
|
// the game's PGN.
|
||
|
func (g *Game) String() string {
|
||
|
return encodePGN(g)
|
||
|
}
|
||
|
|
||
|
// MarshalText implements the encoding.TextMarshaler interface and
|
||
|
// encodes the game's PGN.
|
||
|
func (g *Game) MarshalText() (text []byte, err error) {
|
||
|
return []byte(encodePGN(g)), nil
|
||
|
}
|
||
|
|
||
|
// UnmarshalText implements the encoding.TextUnarshaler interface and
|
||
|
// assumes the data is in the PGN format.
|
||
|
func (g *Game) UnmarshalText(text []byte) error {
|
||
|
game, err := decodePGN(string(text))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
g.copy(game)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Draw attempts to draw the game by the given method. If the
|
||
|
// method is valid, then the game is updated to a draw by that
|
||
|
// method. If the method isn't valid then an error is returned.
|
||
|
func (g *Game) Draw(method Method) error {
|
||
|
switch method {
|
||
|
case ThreefoldRepetition:
|
||
|
if g.numOfRepitions() < 3 {
|
||
|
return errors.New("chess: draw by ThreefoldRepetition requires at least three repetitions of the current board state")
|
||
|
}
|
||
|
case FiftyMoveRule:
|
||
|
if g.Pos.halfMoveClock < 100 {
|
||
|
return fmt.Errorf("chess: draw by FiftyMoveRule requires the half move clock to be at 100 or greater but is %d", g.Pos.halfMoveClock)
|
||
|
}
|
||
|
case DrawOffer:
|
||
|
default:
|
||
|
return fmt.Errorf("chess: unsupported draw method %s", method.String())
|
||
|
}
|
||
|
g.Outcome = Draw
|
||
|
g.Method = method
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Resign resigns the game for the given color. If the game has
|
||
|
// already been completed then the game is not updated.
|
||
|
func (g *Game) Resign(color Color) {
|
||
|
if g.Outcome != NoOutcome || color == NoColor {
|
||
|
return
|
||
|
}
|
||
|
if color == White {
|
||
|
g.Outcome = BlackWon
|
||
|
} else {
|
||
|
g.Outcome = WhiteWon
|
||
|
}
|
||
|
g.Method = Resignation
|
||
|
}
|
||
|
|
||
|
// EligibleDraws returns valid inputs for the Draw() method.
|
||
|
func (g *Game) EligibleDraws() []Method {
|
||
|
draws := []Method{DrawOffer}
|
||
|
if g.numOfRepitions() >= 3 {
|
||
|
draws = append(draws, ThreefoldRepetition)
|
||
|
}
|
||
|
if g.Pos.halfMoveClock >= 100 {
|
||
|
draws = append(draws, FiftyMoveRule)
|
||
|
}
|
||
|
return draws
|
||
|
}
|
||
|
|
||
|
// AddTagPair adds or updates a tag pair with the given key and
|
||
|
// value and returns true if the value is overwritten.
|
||
|
func (g *Game) AddTagPair(k, v string) bool {
|
||
|
for i, tag := range g.TagPairs {
|
||
|
if tag.Key == k {
|
||
|
g.TagPairs[i].Value = v
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
g.TagPairs = append(g.TagPairs, &TagPair{Key: k, Value: v})
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// GetTagPair returns the tag pair for the given key or nil
|
||
|
// if it is not present.
|
||
|
func (g *Game) GetTagPair(k string) *TagPair {
|
||
|
for _, tag := range g.TagPairs {
|
||
|
if tag.Key == k {
|
||
|
return tag
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RemoveTagPair removes the tag pair for the given key and
|
||
|
// returns true if a tag pair was removed.
|
||
|
func (g *Game) RemoveTagPair(k string) bool {
|
||
|
cp := []*TagPair{}
|
||
|
found := false
|
||
|
for _, tag := range g.TagPairs {
|
||
|
if tag.Key == k {
|
||
|
found = true
|
||
|
} else {
|
||
|
cp = append(cp, tag)
|
||
|
}
|
||
|
}
|
||
|
g.TagPairs = cp
|
||
|
return found
|
||
|
}
|
||
|
|
||
|
func (g *Game) updatePosition() {
|
||
|
method := g.Pos.Status()
|
||
|
if method == Stalemate {
|
||
|
g.Method = Stalemate
|
||
|
g.Outcome = Draw
|
||
|
} else if method == Checkmate {
|
||
|
g.Method = Checkmate
|
||
|
g.Outcome = WhiteWon
|
||
|
if g.Pos.Turn() == White {
|
||
|
g.Outcome = BlackWon
|
||
|
}
|
||
|
}
|
||
|
if g.Outcome != NoOutcome {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// five fold rep creates automatic draw
|
||
|
if !g.IgnoreAutomaticDraws && g.numOfRepitions() >= 5 {
|
||
|
g.Outcome = Draw
|
||
|
g.Method = FivefoldRepetition
|
||
|
}
|
||
|
|
||
|
// 75 move rule creates automatic draw
|
||
|
if !g.IgnoreAutomaticDraws && g.Pos.halfMoveClock >= 150 && g.Method != Checkmate {
|
||
|
g.Outcome = Draw
|
||
|
g.Method = SeventyFiveMoveRule
|
||
|
}
|
||
|
|
||
|
// insufficient material creates automatic draw
|
||
|
if !g.IgnoreAutomaticDraws && !g.Pos.board.hasSufficientMaterial() {
|
||
|
g.Outcome = Draw
|
||
|
g.Method = InsufficientMaterial
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (g *Game) copy(game *Game) {
|
||
|
g.TagPairs = game.GetTagPairs()
|
||
|
g.Moves = game.MovesHistory()
|
||
|
g.Positions = game.PositionsHistory()
|
||
|
g.Pos = game.Pos
|
||
|
g.Outcome = game.Outcome
|
||
|
g.Method = game.Method
|
||
|
}
|
||
|
|
||
|
// Clone clones a game
|
||
|
func (g *Game) Clone() *Game {
|
||
|
return &Game{
|
||
|
TagPairs: g.GetTagPairs(),
|
||
|
Notation: g.Notation,
|
||
|
Moves: g.MovesHistory(),
|
||
|
Positions: g.PositionsHistory(),
|
||
|
Pos: g.Pos,
|
||
|
Outcome: g.Outcome,
|
||
|
Method: g.Method,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (g *Game) numOfRepitions() int {
|
||
|
count := 0
|
||
|
for _, pos := range g.PositionsHistory() {
|
||
|
if g.Pos.samePosition(pos) {
|
||
|
count++
|
||
|
}
|
||
|
}
|
||
|
return count
|
||
|
}
|