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.
402 lines
10 KiB
402 lines
10 KiB
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 |
|
}
|
|
|