forked from notnil/chess
-
Notifications
You must be signed in to change notification settings - Fork 2
/
game.go
401 lines (361 loc) · 10.3 KB
/
game.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
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()
}
// Positions returns the position history of the game.
func (g *Game) Positions() []*Position {
return append([]*Position(nil), g.positions...)
}
// Moves returns the move history of the game.
func (g *Game) Moves() []*Move {
return append([]*Move(nil), g.moves...)
}
// TagPairs returns the game's tag pairs.
func (g *Game) TagPairs() []*TagPair {
return append([]*TagPair(nil), g.tagPairs...)
}
// Position returns the game's current position.
func (g *Game) Position() *Position {
return g.pos
}
// Outcome returns the game outcome.
func (g *Game) Outcome() Outcome {
return g.outcome
}
// Method returns the method in which the outcome occurred.
func (g *Game) Method() 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.TagPairs()
g.moves = game.Moves()
g.positions = game.Positions()
g.pos = game.pos
g.outcome = game.outcome
g.method = game.method
}
func (g *Game) Clone() *Game {
return &Game{
tagPairs: g.TagPairs(),
notation: g.notation,
moves: g.Moves(),
positions: g.Positions(),
pos: g.pos,
outcome: g.outcome,
method: g.method,
}
}
func (g *Game) numOfRepitions() int {
count := 0
for _, pos := range g.Positions() {
if g.pos.samePosition(pos) {
count++
}
}
return count
}