import random, logging class GameException(Exception): pass class PlayersNotListException(GameException): def __str__(self): return "A list of players is required" class InsufficientPlayersException(GameException): def __str__(self): return "There must be at least three players" class EmailAddressInvalidException(GameException): def __init__(self, bademail): self.bademail = bademail def __str__(self): return "Email '%s' is invalid" % self.bademail class EmailAddressRepeatedException(GameException): def __init__(self, bademail): self.bademail = bademail def __str__(self): return "Email '%s' is given for more than one player" % self.bademail class DeckInvalidException(GameException): def __init__(self, err): self.err = err def __str__(self): return "The supplied deck was invalid (%s)" % self.err class WrongPlayerException(GameException): def __init__(self, right, wrong): self.right = right self.wrong = wrong def __str__(self): return "Expecting a move from '%s' but got one from '%s'" % ( self.right, self.wrong) class NoSuchMoveException(GameException): def __init__(self, badmove): self.badmove = badmove def __str__(self): return "No such move '%s'" % self.badmove class InappropriateMoveException(GameException): def __init__(self, badmove, goodmoves): self.badmove = badmove self.goodmoves = goodmoves def __str__(self): return "Move %s is wrong: it must be one of %s" % (self.badmove, ','.join(self.goodmoves)) class InvalidMoveException(GameException): def __init__(self, move, badparam): self.move = move self.badparam = badparam def __str__(self): return "Can't %s '%s'" % (self.move, self.badparam) DUTY = { "red": { "l": 0, "r": 1000, "d": 0, "n": 700, "m": 500, "a": 350, "p": 250, "b": 150, "c": 100, "w": 50}, "green": { "l": 0, "r": 500, "d": 0, "n": 350, "m": 250, "a": 175, "p": 125, "b": 75, "c": 50, "w": 25} } GENERIC_DOMAINS = "aero", "asia", "biz", "cat", "com", "coop", \ "edu", "gov", "info", "int", "jobs", "mil", "mobi", "museum", \ "name", "net", "org", "pro", "tel", "travel" def email_invalid(emailaddress, domains = GENERIC_DOMAINS): """Checks for a syntactically invalid email address.""" # Email address must be 7 characters in total. if len(emailaddress) < 7: return True # Address too short. # Split up email address into parts. try: localpart, domainname = emailaddress.rsplit('@', 1) host, toplevel = domainname.rsplit('.', 1) except ValueError: return True # Address does not have enough parts. # Check for Country code or Generic Domain. if len(toplevel) != 2 and toplevel not in domains: return True # Not a domain name. for i in '-_.%+.': localpart = localpart.replace(i, "") for i in '-_.': host = host.replace(i, "") if localpart.isalnum() and host.isalnum(): return False # Email address is fine. else: return True # Email address has funny characters. class Player(object): def __init__(self, email): self.email = email self._money = 500 def _get_money(self): return self._money def _set_money(self, v): logging.debug("Setting %s money to %s" % (self, v)) self._money = v money = property(_get_money, _set_money) def __str__(self): return self.email def __repr__(self): return self.__str__() class State(object): def __init__(self, player, move, hand): self.player = player self.move = move self.hand = ''.join(sorted(hand)) class Game(object): def __init__(self, players=None, deck=None): if not isinstance(players, list): raise PlayersNotListException if len(players) < 3: raise InsufficientPlayersException for e in players: if email_invalid(e): raise EmailAddressInvalidException(e) for e in players: c = 0 for e2 in players: if e == e2: c+=1 if c > 1: raise EmailAddressRepeatedException(e) if deck is None: self.deck = list(("l" * 24) + "rdnnmmmaaaappppbbbbcccccwwwwww") random.shuffle(self.deck) self.deck = ''.join(self.deck) else: self.deck = deck # confirm that passed deck is valid if self.deck.count("l") != 24: raise DeckInvalidException("Not 24 luggage cards") if self.deck.count("r") != 1: raise DeckInvalidException("Not 1 crown card") if self.deck.count("d") != 1: raise DeckInvalidException("Not 1 diplomatic bag card") if self.deck.count("n") != 2: raise DeckInvalidException("Not 2 necklace cards") if self.deck.count("m") != 3: raise DeckInvalidException("Not 3 camera cards") if self.deck.count("a") != 4: raise DeckInvalidException("Not 4 watch cards") if self.deck.count("p") != 4: raise DeckInvalidException("Not 4 perfume cards") if self.deck.count("b") != 4: raise DeckInvalidException("Not 4 brandy cards") if self.deck.count("c") != 5: raise DeckInvalidException("Not 5 cigars cards") if self.deck.count("w") != 6: raise DeckInvalidException("Not 6 wine cards") if len(self.deck) != 54: raise DeckInvalidException("A valid deck has 54 cards") self.original_deck = self.deck self.__players = [Player(x) for x in players] self.rounds = [] for co in range(len(self.players)): self.rounds += [(self.players[co], self.players[x]) for x in range(len(self.players)) if x != co] logging.debug("rounds: %s" % self.rounds) self.customs_officer, self.current_player = self.rounds.pop(0) # next round hand, self.deck = self.deck[:4], self.deck[4:] self.state = State(self.current_player, "declare", hand) @property def players(self): return self.__players def move(self, player_to_move, move_to_make, move_parameter=None): # validity checking if player_to_move != str(self.state.player) and player_to_move != self.state.player: raise WrongPlayerException(self.state.player, player_to_move) if move_to_make not in ["declare", "challenge", "accept", "discard"]: raise NoSuchMoveException(move_to_make) # state machine if self.state.move == "declare": if move_to_make != "declare": raise InappropriateMoveException(move_to_make, ["declare"]) if len(str(move_parameter)) != 4: if str(move_parameter) != "d": raise InvalidMoveException("declare", str(move_parameter)) if len(str(move_parameter)) == 4 and "d" in str(move_parameter): raise InvalidMoveException("declare", "the Diplomatic Bag and other stuff") for c in str(move_parameter): if c not in self.original_deck: raise InvalidMoveException("declare", c) # player has declared # customs officer needs to decide whether they accept or not logging.debug("MOVE DATA: player %s declares %s, now is for %s to challenge or no" % (player_to_move, move_parameter, self.customs_officer)) self.state = State(self.customs_officer, "challenge", self.state.hand) # save details of declaration self.__declaration = ''.join(sorted(move_parameter)) self.__declaring_player = self.current_player self.current_player = self.customs_officer elif self.state.move == "challenge": if move_to_make not in ["challenge", "accept"]: raise InappropriateMoveException(move_to_make, ["challenge", "accept"]) if move_to_make == "challenge": if self.__declaration == self.state.hand: # fail! challenged declaration was correct if self.current_player == self.customs_officer: logging.debug("MOVE DATA: customs officer %s unsuccessfully challenges" % (self.customs_officer)) # this challenge was done by the CO self.customs_officer.money -= 200 self.__declaring_player.money += 200 # already challenged, so informer does not get to challenge # 4 new cards to next player if not self.rounds: # game is over! self.state.player = None self.state.move = "end" return self.customs_officer, self.current_player = self.rounds.pop(0) # next round hand, self.deck = self.deck[:4], self.deck[4:] self.state = State(self.current_player, "declare", hand) else: # this challenge was done by an informer logging.debug("MOVE DATA: informer %s unsuccessfully challenges %s declaration" % (self.current_player, self.__declaring_player)) self.customs_officer.money -= 100 # reward to informer self.current_player.money -= 100 # defamation to declarer self.__declaring_player.money += 200 # 4 new cards to informer, who is already current_player hand, self.deck = self.deck[:4], self.deck[4:] self.state = State(self.current_player, "declare", hand) else: # win! challenged and declaration was lies. pay red duty on everything in the hand for c in self.state.hand: duty = DUTY["red"][c] self.customs_officer.money += duty self.__declaring_player.money -= duty logging.debug("MOVE DATA: pay red duty %s from %s to %s" % (duty, self.__declaring_player, self.customs_officer)) if "d" in self.__declaration: # player impersonated a diplomat. Pay an extra 200 self.customs_officer.money += 200 self.__declaring_player.money -= 200 if self.current_player == self.customs_officer: logging.debug("MOVE DATA: customs officer %s successfully challenges" % (self.customs_officer)) # already challenged, so informer does not get to challenge # 4 new cards to next player if not self.rounds: # game is over! self.state.player = None self.state.move = "end" return self.customs_officer, self.current_player = self.rounds.pop(0) # next round hand, self.deck = self.deck[:4], self.deck[4:] self.state = State(self.current_player, "declare", hand) else: logging.debug("MOVE DATA: informer %s successfully challenges" % (self.current_player)) # this challenge was done by an informer self.customs_officer.money -= 100 # reward to informer self.current_player.money += 100 if not self.rounds: # game is over! self.state.player = None self.state.move = "end" return # 4 new cards to informer, who is already current_player hand, self.deck = self.deck[:4], self.deck[4:] self.state = State(self.current_player, "declare", hand) else: # move_to_make = accept # pay green duty on everything declared if self.current_player == self.customs_officer: for c in self.__declaration: duty = DUTY["green"][c] self.customs_officer.money += duty self.__declaring_player.money -= duty logging.debug("MOVE DATA: pay green duty %s from %s to %s" % (duty, self.__declaring_player, self.customs_officer)) logging.debug("MOVE DATA: customs officer %s accepts" % (self.customs_officer)) # informer still gets a chance to challenge if not self.rounds: # this was the last round, but the informer has not challenged yet. # informer *must* be players[0] (because the CO is players[-1] # and current_player is players[-2]) # so add one last go to self.rounds self.rounds = [(self.customs_officer, self.players[0])] self.customs_officer, self.current_player = self.rounds.pop(0) # next round self.state = State(self.current_player, "challenge", self.state.hand) else: # the informer also accepts (and is current_player), so take 4 and discard logging.debug("MOVE DATA: informer %s accepts" % (self.current_player)) # if the hand contains the Bag, autodiscard it and draw one more card if "d" in self.state.hand: newhand = self.state.hand.replace("d", "") top, self.deck = self.deck[0], self.deck[1:] self.state = State(self.current_player, "declare", newhand + top) else: self.state = State(self.current_player, "discard", self.state.hand) elif self.state.move == "discard": if move_to_make != "discard": raise InappropriateMoveException(move_to_make, ["discard"]) if len(str(move_parameter)) != 1: raise InvalidMoveException("discard", str(move_parameter)) if str(move_parameter) not in self.state.hand: raise InvalidMoveException("discard", str(move_parameter)) l = list(self.state.hand) l.pop(l.index(str(move_parameter))) next, self.deck = self.deck[0], self.deck[1:] self.state = State(self.state.player, "declare", ''.join(l) + next)