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):
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.
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.
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).
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
"""
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
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()
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))
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
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.'
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):
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.
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