1
# -*- coding: utf-8 -*-
5
__author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
6
__license__ = 'GNU General Public License Version 2'
7
__copyright__ = 'Copyright 2005-2006 Robert Ancell'
13
RESULT_IN_PROGRESS = '*'
14
RESULT_WHITE_WINS = '1-0'
15
RESULT_BLACK_WINS = '0-1'
16
RESULT_DRAW = '1/2-1/2'
18
# Reasons for the result
19
RULE_CHECKMATE = 'CHECKMATE'
20
RULE_STALEMATE = 'STALEMATE'
21
RULE_TIMEOUT = 'TIMEOUT'
22
RULE_FIFTY_MOVES = 'FIFTY_MOVES'
23
RULE_THREE_FOLD_REPETITION = 'THREE_FOLD_REPETITION'
24
RULE_INSUFFICIENT_MATERIAL = 'INSUFFICIENT_MATERIAL'
25
RULE_RESIGN = 'RESIGN'
27
RULE_AGREEMENT = 'AGREEMENT'
28
RULE_ABANDONMENT = 'ABANDONMENT'
34
# The move number (game starts at 0)
37
# The player and piece that moved
41
# The piece that was promoted to (or None)
44
# The victim piece (or None)
47
# The start and end position of the move
51
# The move in CAN and SAN format
55
# The game result after this move
56
opponentInCheck = False
57
opponentCanMove = False
59
# If this move can be used as a resignation
61
threeFoldRepetition = False
63
# A comment about this move
66
# Numeric annotation glyph for move
73
def __init__(self, name):
74
"""Constructor for a chess player.
76
'name' is the name of the player.
78
self.__name = str(name)
80
self.__readyToMove = False
85
def onPieceMoved(self, piece, start, end, delete):
86
"""Called when a chess piece is moved.
88
'piece' is the piece that has been moved (chess.board.ChessPiece).
89
'start' is the location the piece in LAN format (string) or None if the piece has been created.
90
'end' is the location the piece has moved to in LAN format (string).
91
'delete' is a flag to show if the piece should be deleted when it arrives there (boolean).
95
def onPlayerMoved(self, player, move):
96
"""Called when a player has moved.
98
'player' is the player that has moved (ChessPlayer).
99
'move' is the record for this move (ChessMove).
103
def onUndoMove(self):
106
def onPlayerStartTurn(self, player):
109
def onGameEnded(self, game):
110
"""Called when a chess game has ended.
112
'game' is the game that has ended (Game).
116
def readyToMove(self):
124
"""Get the name of this player.
126
Returns the player name (string).
131
"""Get the game this player is in.
133
Returns the game (Game) or None if not in a game.
137
def getRemainingTime(self):
138
"""Get the amount of time this player has remaining.
140
Returns the amount of time in milliseconds.
142
if self is self.__game.getWhite():
143
timer = self.__game.whiteTimer
144
elif self is self.__game.getBlack():
145
timer = self.__game.blackTimer
152
return timer.controller.getRemaining()
154
def isReadyToMove(self):
157
return self.__readyToMove
159
def canMove(self, start, end, promotionType = chess.board.QUEEN):
162
return self.__game.canMove(self, start, end, promotionType)
164
def move(self, move):
167
'move' is the move to make in Normal/Long/Standard Algebraic format (string).
169
self.__game.move(self, move)
172
"""Undo moves until it is this players turn"""
173
self.__game.undo(self)
176
"""Complete this players turn"""
177
self.__game.endMove(self)
180
"""Resign from the game"""
181
self.__game.resign(self)
185
return self.__game.claimDraw()
188
"""Report this players timer has expired"""
189
self.__game.outOfTime(self)
192
"""Report this player has died"""
194
if self.__game is not None:
195
self.__game.killPlayer(self)
199
def _setGame(self, game):
204
def _setReadyToMove(self, readyToMove):
205
if self.__readyToMove == readyToMove:
207
self.__readyToMove = readyToMove
208
if readyToMove is True:
211
class ChessGameBoard(chess.board.ChessBoard):
215
def __init__(self, game):
219
chess.board.ChessBoard.__init__(self)
221
def onPieceMoved(self, piece, start, end, delete):
222
"""Called by chess.board.ChessBoard"""
223
self.__game._onPieceMoved(piece, start, end, delete)
225
class ChessGameSANConverter(chess.san.SANConverter):
229
__colourToSAN = {chess.board.WHITE: chess.san.SANConverter.WHITE,
230
chess.board.BLACK: chess.san.SANConverter.BLACK}
232
for (a, b) in __colourToSAN.iteritems():
235
__typeToSAN = {chess.board.PAWN: chess.san.SANConverter.PAWN,
236
chess.board.KNIGHT: chess.san.SANConverter.KNIGHT,
237
chess.board.BISHOP: chess.san.SANConverter.BISHOP,
238
chess.board.ROOK: chess.san.SANConverter.ROOK,
239
chess.board.QUEEN: chess.san.SANConverter.QUEEN,
240
chess.board.KING: chess.san.SANConverter.KING}
242
for (a, b) in __typeToSAN.iteritems():
245
def __init__(self, board, moveNumber):
247
self.moveNumber = moveNumber
248
chess.san.SANConverter.__init__(self)
250
def decode(self, colour, move):
251
(start, end, result, promotionType) = chess.san.SANConverter.decode(self, self.__colourToSAN[colour], move)
252
return (start, end, self.__sanToType[promotionType])
254
def encode(self, start, end, isTake, promotionType):
255
if promotionType is None:
256
promotion = self.QUEEN
258
promotion = self.__typeToSAN[promotionType]
259
return chess.san.SANConverter.encode(self, start, end, isTake, promotion)
261
def getPiece(self, location):
262
"""Called by chess.san.SANConverter"""
263
piece = self.board.getPiece(location, self.moveNumber)
266
return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()])
268
def testMove(self, colour, start, end, promotionType, allowSuicide = False):
269
"""Called by chess.san.SANConverter"""
270
move = self.board.testMove(self.__sanToColour[colour], start, end,
271
self.__sanToType[promotionType], allowSuicide, self.moveNumber)
275
if move.opponentInCheck:
276
if not move.opponentCanMove:
277
return chess.san.SANConverter.CHECKMATE
278
return chess.san.SANConverter.CHECK
286
"""Game constructor"""
288
self.__spectators = []
289
self.__whitePlayer = None
290
self.__blackPlayer = None
291
self.__currentPlayer = None
293
self.__inCallback = False
294
self.__queuedCalls = []
295
self.board = ChessGameBoard(self)
297
self.__started = False
298
self.result = RESULT_IN_PROGRESS
300
self.whiteTimer = None
301
self.blackTimer = None
303
def getAlivePieces(self, moveNumber = -1):
304
"""Get the alive pieces on the board.
306
'moveNumber' is the move to get the pieces from (integer).
308
Returns a dictionary of the alive pieces (board.ChessPiece) keyed by location.
309
Raises an IndexError exception if moveNumber is invalid.
311
return self.board.getAlivePieces(moveNumber)
313
def getDeadPieces(self, moveNumber = -1):
314
"""Get the dead pieces from the game.
316
'moveNumber' is the move to get the pieces from (integer).
318
Returns a list of the pieces (board.ChessPiece) in the order they were killed.
319
Raises an IndexError exception if moveNumber is invalid.
321
return self.board.getDeadPieces(moveNumber)
323
def setTimers(self, whiteTimer, blackTimer):
326
self.whiteTimer = whiteTimer
327
self.blackTimer = blackTimer
329
def setWhite(self, player):
330
"""Set the white player in the game.
332
'player' is the player to use as white.
334
If the game has started or there is a white player an exception is thrown.
336
assert(self.__started is False)
337
assert(self.__whitePlayer is None)
338
self.__whitePlayer = player
339
self.__connectPlayer(player)
342
"""Returns the current white player (player.Player)"""
343
return self.__whitePlayer
345
def setBlack(self, player):
346
"""Set the black player in the game.
348
'player' is the player to use as black.
350
If the game has started or there is a black player an exception is thrown.
352
assert(self.__started is False)
353
assert(self.__blackPlayer is None)
354
self.__blackPlayer = player
355
self.__connectPlayer(player)
358
"""Returns the current white player (player.Player)"""
359
return self.__blackPlayer
361
def getCurrentPlayer(self):
362
"""Get the player to move"""
363
return self.__currentPlayer
365
def addSpectator(self, player):
366
"""Add a spectator to the game.
368
'player' is the player spectating.
370
This can be called after the game has started.
372
self.__spectators.append(player)
373
self.__connectPlayer(player)
376
"""Returns True if the game has been started"""
377
return self.__started
379
def start(self, moves = []):
382
'moves' is a list of moves to start with.
384
If there is no white or black player then an exception is raised.
386
assert(self.__whitePlayer is not None and self.__blackPlayer is not None)
390
#self.x = network.GameReporter('Test Game', 12345)
393
# Load starting moves
394
self.__currentPlayer = self.__whitePlayer
396
self.move(self.__currentPlayer, move)
397
if self.__currentPlayer is self.__whitePlayer:
398
self.__currentPlayer = self.__blackPlayer
400
self.__currentPlayer = self.__whitePlayer
402
self.__started = True
404
# Stop if both players aren't alive
405
if not self.__whitePlayer.isAlive:
406
self.killPlayer(self.__whitePlayer)
408
if not self.__blackPlayer.isAlive:
409
self.killPlayer(self.__blackPlayer)
412
# Stop if game ended on loaded moves
413
if self.result != RESULT_IN_PROGRESS:
414
self._notifyEndGame()
419
# Inform other players of the result
420
for player in self.__players:
421
player.onPlayerStartTurn(self.__currentPlayer)
423
# Get the next player to move
424
self.__currentPlayer._setReadyToMove(True)
428
def getSquareOwner(self, coord):
431
piece = self.board.getPiece(coord)
435
colour = piece.getColour()
436
if colour is chess.board.WHITE:
437
return self.__whitePlayer
438
elif colour is chess.board.BLACK:
439
return self.__blackPlayer
443
def canMove(self, player, start, end, promotionType):
444
"""Test if a player can move.
446
'player' is the player making the move.
447
'start' is the location to move from in LAN format (string).
448
'end' is the location to move from in LAN format (string).
449
'promotionType' is the piece type to promote pawns to. FIXME: Make this a property of the player
451
Return True if can move, otherwise False.
453
if player is not self.__currentPlayer:
456
if player is self.__whitePlayer:
457
colour = chess.board.WHITE
458
elif player is self.__blackPlayer:
459
colour = chess.board.BLACK
463
move = self.board.testMove(colour, start, end, promotionType = promotionType)
465
return move is not None
467
def move(self, player, move):
468
"""Get a player to make a move.
470
'player' is the player making the move.
471
'move' is the move to make in SAN or LAN format (string).
473
if self.__inCallback:
474
self.__queuedCalls.append((self.move, (player, move)))
479
if player is not self.__currentPlayer:
480
print 'Player attempted to move out of turn'
482
self._move(player, move)
486
def undo(self, player):
487
if self.__inCallback:
488
self.__queuedCalls.append((self.undo, (player,)))
493
self.__whitePlayer._setReadyToMove(False)
494
self.__blackPlayer._setReadyToMove(False)
496
# Pretend the current player is the oponent so when endMove() is called
497
# this player will become the active player.
498
# Yes, this IS a big hack...
499
if player is self.__whitePlayer:
500
self.__currentPlayer = self.__blackPlayer
502
self.__currentPlayer = self.__whitePlayer
504
# If this player hasn't moved then undo oponents move before their one
505
if len(self.__moves) > 0 and self.__moves[-1].player is not player:
510
for i in xrange(count):
511
if len(self.__moves) != 0:
513
self.__moves = self.__moves[:-1]
514
for p in self.__players:
520
assert(self.__inCallback is False)
521
self.__inCallback = True
524
self.__inCallback = False
525
while len(self.__queuedCalls) > 0:
526
(call, args) = self.__queuedCalls[0]
527
self.__queuedCalls = self.__queuedCalls[1:]
530
def _move(self, player, move):
533
if self.result != RESULT_IN_PROGRESS:
534
print 'Game completed'
537
if self.__currentPlayer is self.__whitePlayer:
538
colour = chess.board.WHITE
540
colour = chess.board.BLACK
542
# If move is SAN process it as such
544
(start, end, _, _, promotionType, _) = chess.lan.decode(colour, move)
545
except chess.lan.DecodeError, e:
546
converter = ChessGameSANConverter(self.board, len(self.__moves))
548
(start, end, promotionType) = converter.decode(colour, move)
549
except chess.san.Error, e:
550
print 'Invalid move: ' + move
553
# Only use promotion type if a pawn move to far file
554
piece = self.board.getPiece(start)
556
if piece is not None and piece.getType() is chess.board.PAWN:
557
if colour is chess.board.WHITE:
559
promotion = promotionType
562
promotion = promotionType
564
moveResult = self.board.movePiece(colour, start, end, promotionType)
565
if moveResult is None:
566
print 'Illegal move: ' + str(move)
569
# Re-encode for storing and reporting
570
canMove = chess.lan.encode(colour, start, end, promotionType = promotion)
571
converter = ChessGameSANConverter(self.board, len(self.__moves))
573
sanMove = converter.encode(start, end, moveResult.victim != None, promotionType)
574
except chess.san.Error:
575
# If for some reason we couldn't make the SAN move the use the CAN move instead
579
if len(self.__moves) == 0:
582
m.number = self.__moves[-1].number + 1
583
m.player = self.__currentPlayer
585
m.victim = moveResult.victim
590
m.opponentInCheck = moveResult.opponentInCheck
591
m.opponentCanMove = moveResult.opponentCanMove
592
m.fiftyMoveRule = moveResult.fiftyMoveRule
593
m.threeFoldRepetition = moveResult.threeFoldRepetition
594
#FIXME: m.comment = move.comment
595
#FIXME: m.nag = move.nag
597
self.__moves.append(m)
599
# This player has now moved
600
self.__currentPlayer._setReadyToMove(False)
602
# Inform other players of the result
603
for player in self.__players:
604
player.onPlayerMoved(self.__currentPlayer, m)
606
# Check if the game has ended
607
result = RESULT_IN_PROGRESS
608
if not m.opponentCanMove:
609
if self.__currentPlayer is self.__whitePlayer:
610
result = RESULT_WHITE_WINS
612
result = RESULT_BLACK_WINS
613
if m.opponentInCheck:
614
rule = RULE_CHECKMATE
617
rule = RULE_STALEMATE
619
# Check able to complete
620
if not self.board.sufficientMaterial():
622
rule = RULE_INSUFFICIENT_MATERIAL
624
if result is not RESULT_IN_PROGRESS:
625
self.endGame(result, rule)
627
def endMove(self, player):
630
if self.__inCallback:
631
self.__queuedCalls.append((self.endMove, (player,)))
634
if player is not self.__currentPlayer:
635
print 'Player attempted to move out of turn'
637
if player.move is None:
638
print "Ending move when haven't made one"
641
if self.__currentPlayer is self.__whitePlayer:
642
self.__currentPlayer = self.__blackPlayer
644
self.__currentPlayer = self.__whitePlayer
648
# Inform other players of the result
649
for player in self.__players:
650
player.onPlayerStartTurn(self.__currentPlayer)
652
# Notify the next player they can move
653
if self.__started is True and self.result == RESULT_IN_PROGRESS:
654
self.__currentPlayer._setReadyToMove(True)
658
def resign(self, player):
659
"""Get a player to resign.
661
'player' is the player resigning.
664
if player is self.__whitePlayer:
665
self.endGame(RESULT_BLACK_WINS, rule)
667
self.endGame(RESULT_WHITE_WINS, rule)
672
# TODO: Penalise if make an incorrect attempt
674
move = self.__moves[-1]
678
if move.fiftyMoveRule:
679
rule = RULE_FIFTY_MOVES
680
elif move.threeFoldRepetition:
681
rule = RULE_THREE_FOLD_REPETITION
685
self.endGame(RESULT_DRAW, rule)
688
def killPlayer(self, player):
689
"""Report a player has died
691
'player' is the player that has died.
693
if player is self.__whitePlayer:
694
result = RESULT_BLACK_WINS
695
elif player is self.__blackPlayer:
696
result = RESULT_WHITE_WINS
697
self.endGame(result, RULE_DEATH)
699
def outOfTime(self, player):
700
"""Report a player's timer has expired"""
701
if player is self.__whitePlayer:
702
result = RESULT_BLACK_WINS
703
elif player is self.__blackPlayer:
704
result = RESULT_WHITE_WINS
707
self.endGame(result, RULE_TIMEOUT)
710
self.endGame(RESULT_DRAW, RULE_ABANDONMENT)
712
def endGame(self, result, rule):
713
if self.result != RESULT_IN_PROGRESS:
718
self._notifyEndGame()
720
def _notifyEndGame(self):
721
self.__currentPlayer._setReadyToMove(False)
722
for player in self.__players:
723
player.onGameEnded(self)
733
for player in self.__players:
734
player.onGameEnded(self)
738
def __connectPlayer(self, player):
739
"""Add a player into the game.
741
'player' is the player to add.
743
The player will be notified of the current state of the board.
745
self.__players.append(player)
746
player._setGame(self)
748
# Notify the player of the current state
749
# FIXME: Make the board iteratable...
750
for file in '12345678':
751
for rank in 'abcdefgh':
753
piece = self.board.getPiece(coord)
757
# These are moves from nowhere to their current location
758
player.onPieceMoved(piece, None, coord, False)
760
def _onPieceMoved(self, piece, start, end, delete):
761
"""Called by the chess board"""
763
# Notify all players of creations and deletions
764
# NOTE: Normal moves are done above since the SAN moves are calculated before the move...
765
# FIXME: Change this so the SAN moves are done afterwards...
766
for player in self.__players:
767
player.onPieceMoved(piece, start, end, delete)
769
class NetworkChessGame(ChessGame):
773
def move(self, player, move):
774
"""Get a player to make a move.
776
'player' is the player making the move.
777
'move' is the move to make. It can be of the form:
778
A coordinate move in the form ((file0, rank0), (file1, rank1), promotionType) ((int, int), (int, int), chess.board.PIECE_TYPE) or
784
if __name__ == '__main__':
789
p = pgn.PGN('black.pgn')
792
class PGNPlayer(ChessPlayer):
794
def __init__(self, isWhite):
795
self.__isWhite = isWhite
796
self.__moveNumber = 1
798
def readyToMove(self):
800
move = g.getWhiteMove(self.__moveNumber)
802
move = g.getBlackMove(self.__moveNumber)
803
self.__moveNumber += 1
806
white = PGNPlayer(True)
807
black = PGNPlayer(False)