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

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
}