From: <umg...@us...> - 2007-03-27 15:40:52
|
Revision: 370 http://svn.sourceforge.net/pybridge/?rev=370&view=rev Author: umgangee Date: 2007-03-27 08:39:04 -0700 (Tue, 27 Mar 2007) Log Message: ----------- Refactor BridgeGame class to support new LocalTable and RemoteTable. Modified Paths: -------------- trunk/pybridge/pybridge/bridge/game.py trunk/pybridge/pybridge/network/error.py Modified: trunk/pybridge/pybridge/bridge/game.py =================================================================== --- trunk/pybridge/pybridge/bridge/game.py 2007-03-27 15:33:14 UTC (rev 369) +++ trunk/pybridge/pybridge/bridge/game.py 2007-03-27 15:39:04 UTC (rev 370) @@ -16,107 +16,301 @@ # Foundation Inc. 51 Franklin Street Fifth Floor Boston MA 02110-1301 USA. +from twisted.spread import pb +from zope.interface import implements + +from pybridge.interfaces.game import ICardGame +from pybridge.interfaces.observer import ISubject +from pybridge.network.error import GameError + from bidding import Bidding +from board import Board from playing import Playing -from symbols import Player, Suit +from symbols import Direction, Suit, Strain -class GameError(Exception): - pass +class BridgeGame(object): + """A bridge game models the bidding and play sequence. + + The methods of this class comprise the interface of a state machine. + Clients should only use the class methods to interact with the game state. + Modifications to the state are typically made through BridgePlayer objects. + + Methods which change the game state (makeCall, playCard) require a player + argument as "authentication". + """ -class Game: - """A bridge game models the bidding, playing, scoring sequence.""" + implements(ICardGame, ISubject) - def __init__(self, dealer, deal, scoring, vulnNS, vulnEW): - """Initialises game. - - scoring: instance of scoring class. - """ - self.vulnNS, self.vulnEW = vulnNS, vulnEW - self.deal = deal - self.playing = None - self.scoring = scoring - - # Start bidding. - self.bidding = Bidding(dealer) + # Valid positions. + positions = Direction + # Mapping from Strain symbols (in bidding) to Suit symbols (in play). + trumpMap = {Strain.Club: Suit.Club, Strain.Diamond: Suit.Diamond, + Strain.Heart: Suit.Heart, Strain.Spade: Suit.Spade, + Strain.NoTrump: None} - def isComplete(self): - """Returns True if game is complete, False otherwise.""" - if self.playing: - return self.playing.isComplete() + + def __init__(self): + self.listeners = [] + + self.board = None + self.bidding = None + self.contract = None + self.play = None + + self.boardQueue = [] # Boards for successive games. + self.players = {} # One-to-one mapping from Direction to BridgePlayer. + + +# Implementation of ICardGame. + + + def start(self, board=None): + if board: # Use specified board. + self.board = board + elif self.board: # Advance to next deal. + self.board.nextDeal(result=999) + else: # Create a board. + self.board = Board() + self.board.nextDeal() + self.bidding = Bidding(self.board['dealer']) # Start bidding. + self.contract = None + self.play = None + + # Remove deal from board, so it does not appear to players. + visibleBoard = self.board.copy() + del visibleBoard['deal'] + self.notify('start', board=visibleBoard) + + + def inProgress(self): + if self.play: + return not self.play.isComplete() + elif self.bidding: + return not self.bidding.isPassedOut() else: - return self.bidding.isPassedOut() + return False - def isHandVisible(self, player, viewer): - """A hand is visible if one of the following conditions is met: + def getState(self): + # TODO: all flag? + state = {} + + if self.inProgress(): + # Remove deal from board, so it does not appear to players. + visibleBoard = self.board.copy() + del visibleBoard['deal'] + state['board'] = visibleBoard + + if self.bidding: + state['calls'] = self.bidding.calls + if self.play: + state['played'] = [] + trickcount = max([len(s) for s in self.play.played.values()]) + for index in range(trickcount): + leader, cards = self.play.getTrick(index) + for pos in Direction[leader.index:] + Direction[:leader.index]: + if pos in cards: + state['played'].append(cards[pos]) + + return state + + + def setState(self, state): + if state.get('board'): + self.start(state['board']) + + # Comprehensive error checking is suppressed. + for call in state.get('calls', []): + turn = self.getTurn() + self.makeCall(call, self.players[turn]) +# self.bidding.makeCall(call, player=turn) + + # If a contract has been reached, start the play. +# contract = self.bidding.getContract() +# if contract: +# self.contract = contract +# trumpSuit = self.trumpMap[self.contract['bid'].strain] +# self.play = Playing(contract['declarer'], trumpSuit) + + # Comprehensive error checking is suppressed. + for card in state.get('played', []): + turn = self.getTurn() + self.playCard(card, self.players[turn]) + #self.play.playCard(card, player=turn) + + + def updateState(self, event, *args, **kwargs): + allowed = ['start', 'makeCall', 'playCard'] + if event in allowed: + handler = getattr(self, event) + handler(*args, **kwargs) +# else: +# print "updateState unknown attempted", event + + + def addPlayer(self, position): + if position not in Direction: + raise TypeError, "Expected Direction, got %s" % type(position) + if position in self.players: + raise GameError, "Position %s is taken" % position + + player = BridgePlayer(self) + self.players[position] = player + self.notify('addPlayer', position=position) + + return player + + + def removePlayer(self, position): + if position not in Direction: + raise TypeError, "Expected Direction, got %s" % type(position) + if position not in self.players: + raise GameError, "Position %s is vacant" % position + + player = self.players[position] + del self.players[position] + self.notify('removePlayer', position=position) + + +# Implementation of ISubject. + + + def attach(self, listener): + self.listeners.append(listener) + + + def detach(self, listener): + self.listeners.remove(listener) + + + def notify(self, event, *args, **kwargs): + for listener in self.listeners: + listener.update(event, *args, **kwargs) + + +# Bridge-specific methods. + + + def makeCall(self, call, player): + """Make a call in the current bidding session. - 1. The hand is the viewer's own hand. - 2. Game is complete. - 3. Bidding complete and hand is dummy's, and first card of - first trick has been played. + @param call: the call. + @type call: Bid or Pass or Double or Redouble + @param player: a player identifier. + @type player: BridgePlayer """ - return player == viewer \ - or self.isComplete() \ - or (self.bidding.isComplete() and player == self.playing.dummy and \ - len(self.playing.getTrick(0)[1]) >= 1) + position = self.getPositionOfPlayer(player) + if position is None: + raise GameError, "Invalid player reference" + # Validate call according to game state. + if not self.bidding or self.bidding.isComplete(): + raise GameError, "Game not running or bidding complete" + if self.getTurn() is not position: + raise GameError, "Call made out of turn" + if not self.bidding.isValidCall(call, position): + raise GameError, "Call cannot be made" - def makeCall(self, player, call): - """Makes call from player.""" + self.bidding.makeCall(call, position) + self.notify('makeCall', call=call, position=position) + if self.bidding.isComplete(): - raise GameError('not in bidding') - - if self.bidding.whoseTurn() is not player: - raise GameError('out of turn') - elif not self.bidding.isValidCall(call, player): - raise GameError('invalid call') - - self.bidding.makeCall(call, player) - - # If bidding is complete, start playing. - if self.bidding.isComplete() and not self.bidding.isPassedOut(): + # If a contract has been reached, start the play. contract = self.bidding.getContract() - # Convert from bidding's Strain type to playing's Suit type. - # Note that No Trumps implies a None trump suit. - trumpSuit = getattr(Suit, str(contract['bid'].strain), None) - self.playing = Playing(contract['declarer'], trumpSuit) + if contract: + self.contract = contract + trumpSuit = self.trumpMap[self.contract['bid'].strain] + self.play = Playing(contract['declarer'], trumpSuit) + elif self.bidding.isPassedOut(): + self.notify('gameOver') - def playCard(self, player, card): - """Plays card from player.""" - if not self.bidding.isComplete() or self.bidding.isPassedOut(): - raise GameError('not in play') - elif self.playing.isComplete(): - raise GameError('not in play') + def signalAlert(self, alert, player): + pass # TODO + + + def playCard(self, card, player): + """Play a card in the current play session. - hand = self.deal[player] - if self.playing.whoseTurn() is not player: - raise GameError('out of turn') - elif not self.playing.isValidPlay(card, player, hand): - raise GameError('invalid card') - - self.playing.playCard(card) + @param card: a Card object. + @param player: a BridgePlayer object. + """ + position = self.getPositionOfPlayer(player) + if position is None: + raise GameError, "Invalid player reference" + if not self.play or self.play.isComplete(): + raise GameError, "Game not running or play complete" + if self.getTurn() is self.play.dummy: # Dummy's turn. + if position is self.play.declarer: + position = self.play.dummy # Declarer can play dummy's cards. + elif position is self.play.dummy: + raise GameError, "Dummy cannot play cards" + elif self.getTurn() is not position: + raise GameError, "Card played out of turn" - def getHand(self, player, viewer=None): - """Returns the hand of specified player. + hand = self.board['deal'][position] # May be empty, if hand unknown. + if not self.play.isValidPlay(card, position, hand): + raise GameError, "Card cannot be played from hand" + + self.play.playCard(card) + self.notify('playCard', card=card, position=position) + + + def getHand(self, position, player): + """If specified hand is visible, returns the list of cards in hand. - If viewer player is specified, then the ability of viewer - to "see" the hand will be examined. + A hand is visible if one of the following conditions is met: + + 1. The hand is the player's own hand. + 2. The game is finished. + 3. The bidding is complete and the hand is dummy's, and first card of + first trick has been played. + + @param position: the hand identifier. + @type position: Direction + @param player: a player identifier. + @type player: BridgePlayer """ - if viewer and not self.isHandVisible(player, viewer): - raise GameError('hand not visible') - return self.deal[player] + viewer = self.getPositionOfPlayer(player) + if viewer is None: + raise GameError, "Invalid player reference" + if not self.inProgress() and self.bidding is None: + raise GameError, "No game in progress" + if player == viewer or not self.inProgress(): + return self.board.deal[position] + if self.bidding.isComplete() and position == self.play.dummy: + leader, cards = self.play.getTrick(0) + if len(cards) >= 1: + return self.board.deal[position] + # At this point, checks have been exhausted. + raise GameError, "Hand is not visible" + + + def getTurn(self): + if self.inProgress(): + if self.play: # Currently in the play. + return self.play.whoseTurn() + else: # Currently in the bidding. + return self.bidding.whoseTurn() + else: # Not in game. + raise GameError, "No game in progress" + + def getTrickCount(self): - """ + """Returns various - @return['declarerWon']: number of tricks won by declarer/dummy. + @return: a dictionary of result information. + @rtype: dict + + + ['declarerWon']: number of tricks won by declarer/dummy. @return['defenceWon']: number of tricks won by defenders. @return['declarerNeeds']: number of extra tricks required by declarer to make contract. @@ -125,16 +319,16 @@ @return['required']: number of tricks required from contract level. """ - if self.playing is None: - raise GameError('not in play') + if self.play is None: + raise GameError, "Not in play" count = dict.fromkeys(('declarerWon', 'declarerNeeds', 'defenceWon', 'defenceNeeds'), 0) - for index in range(len(self.playing.winners)): - trick = self.playing.getTrick(index) - winner = self.playing.whoPlayed(self.playing.winningCard(trick)) - if winner in (self.playing.declarer, self.playing.dummy): + for index in range(len(self.play.winners)): + trick = self.play.getTrick(index) + winner = self.play.whoPlayed(self.play.winningCard(trick)) + if winner in (self.play.declarer, self.play.dummy): count['declarerWon'] += 1 else: # Trick won by defenders. count ['defenceWon'] += 1 @@ -148,43 +342,78 @@ return count - def score(self): + def getScore(self): """Returns the integer score value for declarer/dummy if: - bidding stage has been passed out, with no bids made. - play stage is complete. """ - if not self.isComplete(): - raise GameError('game not complete') - elif self.bidding.isPassedOut(): + if self.inProgress() or self.bidding is None: + raise GameError, "Game not complete" + if self.bidding.isPassedOut(): return 0 # A passed out deal does not score. - else: - contract = self.bidding.getContract() - declarer = contract['declarer'] - dummy = Player[(declarer.index + 2) % 4] - vulnerable = (self.vulnNS and declarer in (Player.North, Player.South)) + \ - (self.vulnEW and declarer in (Player.West, Player.East)) - - tricksMade = 0 # Count of tricks won by declarer or dummy. - for index in range(len(self.playing.winners)): - trick = self.playing.getTrick(index) - winningCard = self.playing.winningCard(trick) - winner = self.playing.whoPlayed(winningCard) - tricksMade += winner in (declarer, dummy) - - result = {'contract' : contract, - 'tricksMade' : tricksMade, - 'vulnerable' : vulnerable, } - return self.scoring(result) + contract = self.bidding.getContract() + declarer = contract['declarer'] + dummy = Seat[(declarer.index + 2) % 4] + vulnerable = (self.vulnNS and declarer in (Seat.North, Seat.South)) + \ + (self.vulnEW and declarer in (Seat.West, Seat.East)) - def whoseTurn(self): - """Returns the player that is next to call or play card.""" - if not self.isComplete(): - if self.bidding.isComplete(): - return self.playing.whoseTurn() - else: - return self.bidding.whoseTurn() - else: - raise GameError('game complete') + tricksMade = 0 # Count of tricks won by declarer or dummy. + for index in range(len(self.play.winners)): + trick = self.play.getTrick(index) + winningCard = self.play.winningCard(trick) + winner = self.play.whoPlayed(winningCard) + tricksMade += winner in (declarer, dummy) + result = {'contract' : contract, + 'tricksMade' : tricksMade, + 'vulnerable' : vulnerable, } + return self.scoring(result) + + def getPositionOfPlayer(self, player): + """If player is playing, returns position of player, otherwise None. + + @param player: a BridgePlayer object. + @type player: BridgePlayer + @return: the position of player. + @rtype: Direction or None + """ + for position, p in self.players.items(): + if p == player: + return position + return None + + + + +class BridgePlayer(pb.Referenceable): + """Actor representing a player's view of a BridgeGame object.""" + + + def __init__(self, game): + self.__game = game # Provide access to game only through this object. + + + def makeCall(self, call): + self.__game.makeCall(call, player=self) + + + def playCard(self, card): + self.__game.playCard(card, player=self) + + + def nextGame(self): + pass + + +# Aliases for remote-callable methods. + + + def remote_makeCall(self, call): + self.makeCall(call) + + + def remote_playCard(self, card): + self.playCard(card) + Modified: trunk/pybridge/pybridge/network/error.py =================================================================== --- trunk/pybridge/pybridge/network/error.py 2007-03-27 15:33:14 UTC (rev 369) +++ trunk/pybridge/pybridge/network/error.py 2007-03-27 15:39:04 UTC (rev 370) @@ -33,3 +33,7 @@ Please report any bugs which you discover in PyBridge! """ + +class GameError(pb.Error): + """Raised by game in response to an unsatisfiable or erroneous request.""" + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |