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 }