X-Git-Url: http://git.tremily.us/?p=pyrisk.git;a=blobdiff_plain;f=pyrisk%2Fbase.py;h=8229f6080e0e9f40519cc93d7d048f7e54972464;hp=295b27b1e3c69830313d8122c2f31ba5d2d9c823;hb=a0da46e0d7f14b46f94559992d4989dfddfe5702;hpb=1de24848bf983bcc6bea47bf40f86d52a162cac7 diff --git a/pyrisk/base.py b/pyrisk/base.py index 295b27b..8229f60 100644 --- a/pyrisk/base.py +++ b/pyrisk/base.py @@ -19,7 +19,9 @@ import random -from .log import Logger +from .log import Logger, BeginGame, EndGame, Killed, StartTurn, DealtCards, \ + EarnsArmies, SelectTerritory, PlaceArmies, PlayCards, Attack, Conquer, \ + Fortify class PlayerError (Exception): @@ -74,6 +76,11 @@ class Territory (NameMixin, ID_CmpMixin, list): if id(t) == id(other): return True return False + def border(self): + for t in self: + if t.player != self.player: + return True + return False class Continent (NameMixin, ID_CmpMixin, list): """A group of Territories. @@ -399,15 +406,6 @@ class Player (NameMixin, ID_CmpMixin): for t in world.territories(): if t.player == self: yield t - def border_territories(self, world): - """Iterate through all territories owned by this player which - border another player's territories. - """ - for t in self.territories(world): - for neighbor in t: - if neighbor.player != self: - yield t - break def report(self, world, log): """Send reports about death and game endings. @@ -422,7 +420,7 @@ class Player (NameMixin, ID_CmpMixin): draw - another notification-only method """ print 'Reporting for %s:\n %s' \ - % (self, '\n '.join(log[self._message_index:])) + % (self, '\n '.join([str(e) for e in log[self._message_index:]])) self._message_index = len(log) def draw(self, world, log, cards=[]): """Only called if you earned a new card (or cards). @@ -432,12 +430,13 @@ class Player (NameMixin, ID_CmpMixin): report - another notification-only method """ pass - def select_territory(self, world, log): + def select_territory(self, world, log, error=None): """Return the selected territory's name. """ free_territories = [t for t in world.territories() if t.player == None] return random.sample(free_territories, 1)[0].name - def play_cards(self, world, log, play_required=True): + def play_cards(self, world, log, error=None, + play_required=True): """Decide whether or not to turn in a set of cards. Return a list of cards to turn in or None. If play_required @@ -445,29 +444,47 @@ class Player (NameMixin, ID_CmpMixin): """ if play_required == True: return random.sample(list(self.hand.possible()), 1)[0] - def place_armies(self, world, log, remaining=1, this_round=1): + def place_armies(self, world, log, error=None, + remaining=1, this_round=1): """Both during setup and before each turn. Return {territory_name: num_armies, ...} """ - t = random.sample(list(self.border_territories(world)), 1)[0] - return {t.name: this_round} - def attack_and_fortify(self, world, log, mode='attack'): + terr = random.sample([t for t in self.territories(world) + if t.border()], 1)[0] + return {terr.name: this_round} + def attack_and_fortify(self, world, log, error=None, + mode='attack'): """Return list of (source, target, armies) tuples. Place None in the list to end this phase. """ assert mode != 'fortify', mode possible_attacks = [] - for t in self.border_territories(world): + for t in self.territories(world): + if not t.border(): + continue if t.armies <= 3: #1: # be more conservative, only attack with 3 dice continue targets = [border_t for border_t in t if border_t.player != self] for tg in targets: - possible_attacks.append((t.name, tg.name, min(3, t.armies-1))) + possible_attacks.append( + (t.name, tg.name, min(3, t.armies-1))) if len(possible_attacks) == 0: - return [None, None] # stop attack phase, then stop fortification phase - return random.sample(possible_attacks, 1) # + [None] - def support_attack(self, world, log, source, target): + fortifications = [] + for t in self.territories(world): + if t.border() or t.armies == 1: + continue + targets = list(t) + for tg in targets: + fortifications.append( + (t.name, tg.name, t.armies-1)) + if len(fortifications) > 1: + fortifications = random.sample(fortifications, 1) + # stop attack phase, fortify, stop fortification phase + return [None] + fortifications + [None] + return random.sample(possible_attacks, 1) + def support_attack(self, world, log, error, + source, target): """Follow up on a conquest by moving additional armies. """ return source.armies-1 @@ -506,7 +523,7 @@ class Engine (ID_CmpMixin): for p in self.players: p.alive = True random.shuffle(self.players) - self.log('Game started with %s' % self.players) + self.log(BeginGame(self.players)) self.deck.shuffle() self.select_territories() self.place_initial_armies() @@ -531,31 +548,44 @@ class Engine (ID_CmpMixin): Currently just a notification hook. """ - self.log('Game over.') + self.log(EndGame(self.players)) for p in self.players: p.report(self.world, self.log) def play_turn(self, player): """Work through the phases of player's turn. """ - self.log("%s's turn (territory score: %s)" - % (player, [(p,len(list(p.territories(self.world)))) - for p in self.players])) + self.log(StartTurn(player, self.players, self.world)) self.play_cards_and_place_armies(player) captures = self.attack_and_fortify(player) self.end_of_turn_cards(player, captures) def select_territories(self): for t in self.world.territories(): t.player = None - for i in range(len(list(self.world.territories()))): + num_terrs = len(list(self.world.territories())) + for i in range(num_terrs-1): p = self.players[i % len(self.players)] - t_name = p.select_territory(self.world, self.log) - t = self.world.territory_by_name(t_name) - if t.player != None: - raise PlayerError('Cannot select %s owned by %s' - % (t, t.player)) - self.log('%s selects %s' % (p, t)) + error = None + while True: + try: + t_name = p.select_territory(self.world, self.log, error) + try: + t = self.world.territory_by_name(t_name) + except KeyError: + raise PlayerError('Invalid territory "%s"' % t_name) + if t.player != None: + raise PlayerError('Cannot select %s owned by %s' + % (t, t.player)) + break + except PlayerError, error: + continue + self.log(SelectTerritory(p, t)) t.player = p t.armies = 1 + # last player has no choice. + p = self.players[(num_terrs-1) % len(self.players)] + t = [t for t in self.world.territories() if t.player == None][0] + t.player = p + t.armies = 1 def place_initial_armies(self): already_placed = [len(list(p.territories(self.world))) for p in self.players] s = list(set(already_placed)) @@ -571,18 +601,28 @@ class Engine (ID_CmpMixin): self.player_place_armies(p, remaining, 1) remaining -= 1 def player_place_armies(self, player, remaining=1, this_round=1): - placements = player.place_armies(self.world, self.log, remaining, this_round) - if sum(placements.values()) != this_round: - raise PlayerError('Placing more than %d armies' % this_round) - for ter_name,armies in placements.items(): - t = self.world.territory_by_name(ter_name) - if t.player != player: - raise PlayerError('Placing armies in %s owned by %s' - % (t, t.player)) - if armies < 0: - raise PlayerError('Placing a negative number of armies (%d) in %s' - % (armies, t)) - self.log('%s places %s' % (player, placements)) + error = None + while True: + try: + placements = player.place_armies(self.world, self.log, error, + remaining, this_round) + if sum(placements.values()) != this_round: + raise PlayerError('Placing more than %d armies' % this_round) + for ter_name,armies in placements.items(): + try: + t = self.world.territory_by_name(ter_name) + except KeyError: + raise PlayerError('Invalid territory "%s"' % t_name) + if t.player != player: + raise PlayerError('Placing armies in %s owned by %s' + % (t, t.player)) + if armies < 0: + raise PlayerError('Placing a negative number of armies (%d) in %s' + % (armies, t)) + break + except PlayerError, error: + continue + self.log(PlaceArmies(player, placements)) for terr_name,armies in placements.items(): t = self.world.territory_by_name(terr_name) t.armies += armies @@ -592,62 +632,72 @@ class Engine (ID_CmpMixin): cards.append(self.deck.pop()) player.hand.extend(cards) player.draw(self.world, self.log, cards) - self.log('%s dealt %d cards' % (player, number)) + self.log(DealtCards(player, number, len(self.deck))) def play_cards_and_place_armies(self, player, additional_armies=0): cards_required = len(player.hand) >= 5 - cards = player.play_cards( - self.world, self.log, play_required=cards_required) - if cards_required == True and cards == None: - raise PlayerError('You have %d >= 5 cards in your hand, you must play' - % len(player.hand)) + error = None + while True: + try: + cards = player.play_cards( + self.world, self.log, error, play_required=cards_required) + if cards_required == True and cards == None: + raise PlayerError('You have %d >= 5 cards in your hand, you must play' + % len(player.hand)) + c_prod,c_terr_prod = self.deck.production(player, cards) + break + except PlayerError, error: + continue w_prod,w_terr_prod = self.world.production(player) - self.log('%s earned %d armies from territories' % (player, w_prod)) - c_prod,c_terr_prod = self.deck.production(player, cards) - if c_prod > 0: - self.log('%s played %s, earning %d armies' - % (player, cards, c_prod+sum(c_terr_prod.values()))) + self.log(EarnsArmies(player, w_prod, w_terr_prod)) if cards != None: for c in cards: player.hand.remove(c) + self.log(PlayCards(player, cards, c_prod, c_terr_prod)) for terr,prod in c_terr_prod.items(): if terr in w_terr_prod: w_terr_prod[terr] += prod else: w_terr_prod[terr] = prod self.world.place_territory_production(w_terr_prod) - if len(w_terr_prod) > 0: - self.log('%s was required to place %s' % (player, w_terr_prod)) armies = w_prod + c_prod self.player_place_armies(player, armies, armies) def attack_and_fortify(self, player): captures = 0 mode = 'attack' + error = None while True: - actions = player.attack_and_fortify(self.world, self.log, mode) - for action in actions: - if action == None: + try: + actions = player.attack_and_fortify(self.world, self.log, error, mode) + for action in actions: + if action == None: + if mode == 'attack': + mode = 'fortify' + continue + else: + assert mode == 'fortify', mode + return captures + source_name,target_name,armies = action + try: + source = self.world.territory_by_name(source_name) + except KeyError: + raise PlayerError('Invalid territory "%s"' % source_name) + try: + target = self.world.territory_by_name(target_name) + except KeyError: + raise PlayerError('Invalid territory "%s"' % targer_name) if mode == 'attack': - mode = 'fortify' - continue + tplayer = target.player + capture = self.attack(source, target, armies) + if capture == True: + captures += 1 + if len(list(tplayer.territories(self.world))) == 0: + self.player_killed(tplayer, killer=player) else: assert mode == 'fortify', mode - return captures - source_name,target_name,armies = action - source = self.world.territory_by_name(source_name) - target = self.world.territory_by_name(target_name) - if not source.borders(target): - raise PlayerError('Cannot reach %s from %s to %s' - % (target, source, mode)) - if mode == 'attack': - tplayer = target.player - capture = self.attack(source, target, armies) - if capture == True: - captures += 1 - if len(list(tplayer.territories(self.world))) == 0: - self.player_killed(tplayer, killer=player) - else: - assert mode == 'fortify', mode - self.fortify(source, target, armies) + self.fortify(source, target, armies) + return captures # only allow one fortification + except PlayerError, error: + continue def attack(self, source, target, armies): if source.player == target.player: raise PlayerError('%s attacking %s, but you own both.' @@ -658,29 +708,28 @@ class Engine (ID_CmpMixin): if armies >= source.armies: raise PlayerError('%s attacking %s with %d armies, but only %d are available.' % (source, target, armies, source.armies-1)) - a_dice = sorted([random.randint(1, 6) for i in range(armies)], + if not source.borders(target): + raise PlayerError('Cannot reach %s from %s to attack' + % (target, source)) + s_dice = sorted([random.randint(1, 6) for i in range(armies)], reverse=True) t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))], reverse=True) - a_dead = 0 + s_dead = 0 t_dead = 0 - for a,d in zip(a_dice, t_dice): + for a,d in zip(s_dice, t_dice): if d >= a: - a_dead += 1 + s_dead += 1 else: t_dead += 1 - source.armies -= a_dead + source.armies -= s_dead target.armies -= t_dead if target.armies == 0: - self.takeover(source, target, remaining_attackers=armies-a_dead) - self.log('%s conquered %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d' - % (source.player, target, source, armies, len(t_dice), - a_dead, t_dead, source.armies, target.armies)) + self.takeover(source, target, remaining_attackers=armies-s_dead) + self.log(Conquer(source, target, s_dice, t_dice, s_dead, t_dead)) assert target.armies > 0, target return True - self.log('%s attacked %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d' \ - % (source.player, target, source, armies, len(t_dice), - a_dead, t_dead, source.armies, target.armies)) + self.log(Attack(source, target, s_dice, t_dice, s_dead, t_dead)) assert target.armies > 0, target return False def takeover(self, source, target, remaining_attackers): @@ -688,21 +737,42 @@ class Engine (ID_CmpMixin): target.armies += remaining_attackers target.player = source.player if source.armies > 1: - support = source.player.support_attack( - self.world, self.log, source, target) - if support < 0 or support >= source.armies: - raise PlayerError( - 'Cannot support from %s to %s with %d armies, only %d available' - % (source, target, support, source.armies-1)) + error = None + while True: + try: + support = source.player.support_attack( + self.world, self.log, error, source, target) + if support < 0 or support >= source.armies: + raise PlayerError( + 'Cannot support from %s to %s with %d armies, only %d available' + % (source, target, support, source.armies-1)) + break + except PlayerError, error: + continue source.armies -= support target.armies += support + def fortify(self, source, target, armies): + if source.player != target.player: + raise PlayerError('%s (%s) cannot fortifiy %s (%s).' + % (source, source.player, target, target.player)) + if armies == 0: + return + if armies >= source.armies: + raise PlayerError('%s fortifying %s with %d armies, but only %d are available.' + % (source, target, armies, source.armies-1)) + if not source.borders(target): + raise PlayerError('Cannot reach %s from %s to fortify' + % (target, source)) + source.armies -= armies + target.armies += armies + self.log(Fortify(source, target, armies)) def player_killed(self, player, killer): player.alive = False killer.hand.extend(player.hand) if len(self.living_players()) > 1: while len(killer.hand) > 5: self.play_cards_and_place_armies(killer) - self.log('%s killed by %s' % (player, killer)) + self.log(Killed(player, killer)) if len(self.living_players()) > 1: player.report(self.world, self.log) # else the game is over, and killed will hear about this then. @@ -787,15 +857,10 @@ def test(): return failures def random_game(): - from player.email import IncomingEmailDispatcher, OutgoingEmailDispatcher, EmailPlayer world = generate_earth() - ied = IncomingEmailDispatcher(fifo_path='/tmp/pyrisk.in') - oed = OutgoingEmailDispatcher(return_address='server@example.com') - players = [EmailPlayer('Alice', 'alice@big.edu', ied, oed), - Player('Bob'), Player('Charlie')] + players = [Player('Alice'), Player('Bob'), Player('Charlie')] e = Engine(world, players) e.run() - ied.close() if __name__ == '__main__': import sys