1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 """A Python engine for Risk-like games
22 from .log import Logger
25 class PlayerError (Exception):
28 class NameMixin (object):
29 """Simple mixin for pretty-printing named objects.
31 def __init__(self, name):
39 class ID_CmpMixin (object):
40 """Simple mixin to ensure the fancier comparisons are all based on
43 def __cmp__(self, other):
44 return cmp(id(self), id(other))
45 def __eq__(self, other):
46 return self.__cmp__(other) == 0
47 def __ne__(self, other):
48 return self.__cmp__(other) != 0
50 class Territory (NameMixin, ID_CmpMixin, list):
51 """An occupiable territory.
53 Contains a list of neighboring territories.
55 def __init__(self, name, short_name=None, type=-1,
56 link_names=[], continent=None, player=None):
57 NameMixin.__init__(self, name)
58 ID_CmpMixin.__init__(self)
60 self.short_name = short_name
61 if short_name == None:
62 self.short_name = name
63 self._card_type = type # for Deck construction
64 self._link_names = list(link_names) # used by World._resolve_link_names
65 self.continent = continent # used by World.production
66 self.player = player # who owns this territory
67 self.armies = 0 # number of occupying armies
69 if self.short_name == self.name:
71 return '%s (%s)' % (self.name, self.short_name)
72 def borders(self, other):
74 if id(t) == id(other):
78 class Continent (NameMixin, ID_CmpMixin, list):
79 """A group of Territories.
81 Stores the army-production bonus if it's owned by a single player.
83 def __init__(self, name, production, territories=[]):
84 NameMixin.__init__(self, name)
85 ID_CmpMixin.__init__(self)
86 list.__init__(self, territories)
87 self.production = production
88 def append(self, territory):
89 """Add a new territory (setting the territory's .continent
92 list.append(self, territory)
93 territory.continent = self
94 def territory_by_name(self, name):
95 """Find a Territory instance by name (long or short, case
99 if name.lower() in [t.short_name.lower(), t.name.lower()]:
102 def single_player(self):
103 """Is the continent owned by a single player?
106 for territory in self:
107 if territory.player != p:
111 class World (NameMixin, ID_CmpMixin, list):
112 """Store the world map and current world state.
114 Holds list of Continents. Also controls territory-based army
115 production (via production).
117 def __init__(self, name, continents=[]):
118 NameMixin.__init__(self, name)
119 ID_CmpMixin.__init__(self)
120 list.__init__(self, continents)
121 self.initial_armies = { # num_players:num_armies
122 2: 40, 3:35, 4:30, 5:25, 6:20
124 def territories(self):
125 """Iterate through all the territories in the world.
127 for continent in self:
128 for territory in continent:
130 def territory_by_name(self, name):
131 """Find a Territory instance by name (long or short, case
134 for continent in self:
136 return continent.territory_by_name(name)
140 def continent_by_name(self, name):
141 """Find a Continent instance by name (case insensitive).
143 for continent in self:
144 if continent.name.lower() == name.lower():
147 def _resolve_link_names(self):
148 """Initialize Territory links.
150 The Territory class doesn't actually link to neighbors after
151 initialization, but one of each linked pair has the others
152 name in _link_names. This method goes through the territories,
153 looks up the referenced link target, and joins the pair.
155 self._check_short_names()
156 for territory in self.territories():
157 for name in territory._link_names:
158 other = self.territory_by_name(name)
159 if not territory.borders(other):
160 territory.append(other)
161 if not other.borders(territory):
162 other.append(territory)
163 def _check_short_names(self):
164 """Ensure there are no short_name collisions.
167 for t in self.territories():
168 if t.short_name.lower() not in ts:
169 ts[t.short_name.lower()] = t
171 raise ValueError('%s shared by %s and %s'
172 % (t.short_name.lower(), ts[t.short_name.lower()], t))
173 def production(self, player):
174 """Calculate the number of armies a player should earn based
175 on territory occupation.
177 ts = list(player.territories(self))
178 production = max(3, len(ts) / 3)
179 continents = set([t.continent.name for t in ts])
180 for c_name in continents:
181 c = self.continent_by_name(c_name)
182 if c.single_player() == True:
183 production += c.production
184 return (production, {})
185 def place_territory_production(self, territory_production):
186 """Place armies based on {territory_name: num_armies, ...}.
188 for territory_name,production in territory_production.items():
189 t = self.territory_by_name(territory_name)
190 t.armies += production
192 class Card (ID_CmpMixin):
193 """Represent a territory card (or wild)
195 Nothing exciting going on here, just a class for pretty-printing
198 def __init__(self, deck, type_, territory=None):
199 ID_CmpMixin.__init__(self)
201 self.territory = territory
204 if self.territory == None:
205 return '<Card %s>' % (self.deck.type_names[self.type])
207 return '<Card %s %s>' % (self.territory,
208 self.deck.type_names[self.type])
210 return self.__str__()
213 """All the cards yet to be handed out in a given game.
215 Controls the type branding (via type_names) and army production
216 values for scoring sets (via production_value).
218 def __init__(self, territories=[], num_wilds=2,
219 type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
220 list.__init__(self, [Card(self, t._card_type, t) for t in territories])
221 self.type_names = type_names
222 for i in range(num_wilds):
223 self.append(Card(self, 0))
224 self._production_sequence = [4, 6, 8, 10, 12, 15]
225 self._production_index = 0
227 """Shuffle the remaining cards in the deck.
230 def production_value(self, index):
233 >>> [d.production_value(i) for i in range(8)]
234 [4, 6, 8, 10, 12, 15, 20, 25]
236 if index < len(self._production_sequence):
237 return self._production_sequence[index]
238 extra = index - len(self._production_sequence) + 1
239 return self._production_sequence[-1] + 5 * extra
240 def production(self, player, cards=None):
243 >>> a = Player('Alice')
244 >>> b = Player('Bob')
245 >>> d.production(a, None)
247 >>> d.production(a, [Card(d, 1, Territory('a')),
248 ... Card(d, 1, Territory('b'))])
249 Traceback (most recent call last):
251 PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
252 >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
253 ... Card(d, 1, Territory('b', player=b)),
254 ... Card(d, 1, Territory('c'))])
256 >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
257 ... Card(d, 2, Territory('b', player=a)),
258 ... Card(d, 0, Territory('c', player=a))])
261 >>> sorted(tp.items())
262 [('a', 1), ('b', 1), ('c', 1)]
268 p = self.production_value(self._production_index)
269 self._production_index += 1
270 territory_production = {}
272 if c.territory != None and c.territory.player == player:
273 territory_production[c.territory.name] = 1
274 return (p, territory_production)
275 raise PlayerError('%s is not a scoring set' % h)
278 """Represent a hand of cards.
280 This is the place to override the set of allowed scoring
281 combinations. You should override one of
287 Adding additional scoring methods as needed (e.g. flush).
289 def __init__(self, cards=[]):
290 list.__init__(self, cards)
294 s = sorted(set([card.type for card in self]))
296 or (len(s) == 2 and s[0] == 0):
302 if len(set([card.type for card in self])) == 3:
306 """The hand is any valid scoring combination.
308 return self.set() or self.run()
309 def subhands(self, lengths=None):
310 """Return all possible subhands.
312 Lengths can either be a list of allowed subhand lengths or
313 None. If None, all possible subhand lengths are allowed.
316 >>> h = Hand([Card(d, 1, Territory('a')),
317 ... Card(d, 1, Territory('b')),
318 ... Card(d, 1, Territory('c')),
319 ... Card(d, 1, Territory('d'))])
320 >>> for hand in h.subhands():
326 [<Card a Infantry>, <Card b Infantry>]
327 [<Card a Infantry>, <Card c Infantry>]
328 [<Card a Infantry>, <Card d Infantry>]
329 [<Card b Infantry>, <Card c Infantry>]
330 [<Card b Infantry>, <Card d Infantry>]
331 [<Card c Infantry>, <Card d Infantry>]
332 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
333 [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
334 [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
335 [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
336 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
338 for i in range(len(self)):
339 i += 1 # check all sub-hands of length i
340 if lengths != None and i not in lengths:
341 continue # don't check this length
343 stop = range(len(self)-i, len(self))
344 while indices != stop:
345 yield Hand([self[i] for i in indices])
346 indices = self._increment(indices, stop)
347 yield Hand([self[i] for i in indices])
348 def _increment(self, indices, stop):
351 >>> h = Hand([Card(d, 1, Territory('a'))])
352 >>> h._increment([0, 1, 2], [2, 3, 4])
354 >>> h._increment([0, 1, 3], [2, 3, 4])
356 >>> h._increment([0, 1, 4], [2, 3, 4])
359 moveable = [i for i,m in zip(indices, stop) if i < m]
360 assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
361 key = indices.index(moveable[-1])
362 new = indices[key] + 1
363 for i in range(key, len(indices)):
364 indices[i] = new + i-key
367 """Return a list of all possible scoring subhands.
369 for h in self.subhands():
373 class Player (NameMixin, ID_CmpMixin):
374 """Represent a risk player.
376 This class implements a very basic AI player. Subclasses should
377 consider overriding the "action-required" methods:
385 And the "report" methods:
390 def __init__(self, name):
391 NameMixin.__init__(self, name)
392 ID_CmpMixin.__init__(self)
395 self._message_index = 0
396 def territories(self, world):
397 """Iterate through all territories owned by this player.
399 for t in world.territories():
402 def border_territories(self, world):
403 """Iterate through all territories owned by this player which
404 border another player's territories.
406 for t in self.territories(world):
408 if neighbor.player != self:
411 def report(self, world, log):
412 """Send reports about death and game endings.
414 These events mark the end of contact and require no change in
415 player status or response, so they get a special command
416 seperate from the usual action family. The action commands in
417 Player subclasses can notify the player (possibly by calling
418 report internally) if they feel so inclined.
422 draw - another notification-only method
424 print 'Reporting for %s:\n %s' \
425 % (self, '\n '.join(log[self._message_index:]))
426 self._message_index = len(log)
427 def draw(self, world, log, cards=[]):
428 """Only called if you earned a new card (or cards).
432 report - another notification-only method
435 def select_territory(self, world, log, error=None):
436 """Return the selected territory's name.
438 free_territories = [t for t in world.territories() if t.player == None]
439 return random.sample(free_territories, 1)[0].name
440 def play_cards(self, world, log, error=None,
442 """Decide whether or not to turn in a set of cards.
444 Return a list of cards to turn in or None. If play_required
445 is True, you *must* play.
447 if play_required == True:
448 return random.sample(list(self.hand.possible()), 1)[0]
449 def place_armies(self, world, log, error=None,
450 remaining=1, this_round=1):
451 """Both during setup and before each turn.
453 Return {territory_name: num_armies, ...}
455 t = random.sample(list(self.border_territories(world)), 1)[0]
456 return {t.name: this_round}
457 def attack_and_fortify(self, world, log, error=None,
459 """Return list of (source, target, armies) tuples. Place None
460 in the list to end this phase.
462 assert mode != 'fortify', mode
463 possible_attacks = []
464 for t in self.border_territories(world):
465 if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
467 targets = [border_t for border_t in t if border_t.player != self]
469 possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
470 if len(possible_attacks) == 0:
471 return [None, None] # stop attack phase, then stop fortification phase
472 return random.sample(possible_attacks, 1) # + [None]
473 def support_attack(self, world, log, error,
475 """Follow up on a conquest by moving additional armies.
477 return source.armies-1
479 class Engine (ID_CmpMixin):
482 Basic usage will be along the lines of
484 >>> world = generate_earth()
485 >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
486 >>> e = Engine(world, players)
487 >>> e.run() # doctest: +ELLIPSIS
490 def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
491 ID_CmpMixin.__init__(self)
493 self.deck = deck_class(world.territories())
494 self.log = logger_class()
495 self.players = players
497 return '<engine %s %s>' % (self.world, self.players)
499 return self.__str__()
501 """The main entry point.
507 """Setup phase. Pick territories, place initial armies, and
510 for p in self.players:
512 random.shuffle(self.players)
513 self.log('Game started with %s' % self.players)
515 self.select_territories()
516 self.place_initial_armies()
517 for p in self.players:
520 """Main gameplay phase. Take turns until only one Player survives.
524 living = len(self.living_players())
526 self.play_turn(self.players[active_player])
527 living = len(self.living_players())
528 active_player = (active_player + 1) % len(self.players)
530 while self.players[active_player].alive == False:
531 active_player = (active_player + 1) % len(self.players)
534 """The end of the game.
536 Currently just a notification hook.
538 self.log('Game over.')
539 for p in self.players:
540 p.report(self.world, self.log)
541 def play_turn(self, player):
542 """Work through the phases of player's turn.
544 self.log("%s's turn (territory score: %s)"
545 % (player, [(p,len(list(p.territories(self.world))))
546 for p in self.players]))
547 self.play_cards_and_place_armies(player)
548 captures = self.attack_and_fortify(player)
549 self.end_of_turn_cards(player, captures)
550 def select_territories(self):
551 for t in self.world.territories():
553 num_terrs = len(list(self.world.territories()))
554 for i in range(num_terrs-1):
555 p = self.players[i % len(self.players)]
559 t_name = p.select_territory(self.world, self.log, error)
561 t = self.world.territory_by_name(t_name)
563 raise PlayerError('Invalid territory "%s"' % t_name)
565 raise PlayerError('Cannot select %s owned by %s'
568 except PlayerError, error:
570 self.log('%s selects %s' % (p, t))
573 # last player has no choice.
574 p = self.players[(num_terrs-1) % len(self.players)]
575 t = [t for t in self.world.territories() if t.player == None][0]
578 def place_initial_armies(self):
579 already_placed = [len(list(p.territories(self.world))) for p in self.players]
580 s = list(set(already_placed))
581 assert len(s) in [1,2], already_placed
582 if len(s) == 2: # catch up the players who are one territory short
583 assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
584 for p,placed in zip(self.players, already_placed):
586 self.player_place_armies(p, remaining, 1)
587 remaining = self.world.initial_armies[len(self.players)] - max(s)
589 for p in self.players:
590 self.player_place_armies(p, remaining, 1)
592 def player_place_armies(self, player, remaining=1, this_round=1):
596 placements = player.place_armies(self.world, self.log, error,
597 remaining, this_round)
598 if sum(placements.values()) != this_round:
599 raise PlayerError('Placing more than %d armies' % this_round)
600 for ter_name,armies in placements.items():
602 t = self.world.territory_by_name(ter_name)
604 raise PlayerError('Invalid territory "%s"' % t_name)
605 if t.player != player:
606 raise PlayerError('Placing armies in %s owned by %s'
609 raise PlayerError('Placing a negative number of armies (%d) in %s'
612 except PlayerError, error:
614 self.log('%s places %s' % (player, placements))
615 for terr_name,armies in placements.items():
616 t = self.world.territory_by_name(terr_name)
618 def deal(self, player, number):
620 for i in range(number):
621 cards.append(self.deck.pop())
622 player.hand.extend(cards)
623 player.draw(self.world, self.log, cards)
624 self.log('%s dealt %d cards' % (player, number))
625 def play_cards_and_place_armies(self, player, additional_armies=0):
626 cards_required = len(player.hand) >= 5
630 cards = player.play_cards(
631 self.world, self.log, error, play_required=cards_required)
632 if cards_required == True and cards == None:
633 raise PlayerError('You have %d >= 5 cards in your hand, you must play'
635 c_prod,c_terr_prod = self.deck.production(player, cards)
637 except PlayerError, error:
639 w_prod,w_terr_prod = self.world.production(player)
640 self.log('%s earned %d armies from territories' % (player, w_prod))
642 self.log('%s played %s, earning %d armies'
643 % (player, cards, c_prod+sum(c_terr_prod.values())))
646 player.hand.remove(c)
647 for terr,prod in c_terr_prod.items():
648 if terr in w_terr_prod:
649 w_terr_prod[terr] += prod
651 w_terr_prod[terr] = prod
652 self.world.place_territory_production(w_terr_prod)
653 if len(w_terr_prod) > 0:
654 self.log('%s was required to place %s' % (player, w_terr_prod))
655 armies = w_prod + c_prod
656 self.player_place_armies(player, armies, armies)
657 def attack_and_fortify(self, player):
663 actions = player.attack_and_fortify(self.world, self.log, error, mode)
664 for action in actions:
670 assert mode == 'fortify', mode
672 source_name,target_name,armies = action
674 source = self.world.territory_by_name(source_name)
676 raise PlayerError('Invalid territory "%s"' % source_name)
678 target = self.world.territory_by_name(target_name)
680 raise PlayerError('Invalid territory "%s"' % targer_name)
681 if not source.borders(target):
682 raise PlayerError('Cannot reach %s from %s to %s'
683 % (target, source, mode))
685 tplayer = target.player
686 capture = self.attack(source, target, armies)
689 if len(list(tplayer.territories(self.world))) == 0:
690 self.player_killed(tplayer, killer=player)
692 assert mode == 'fortify', mode
693 self.fortify(source, target, armies)
694 except PlayerError, error:
696 def attack(self, source, target, armies):
697 if source.player == target.player:
698 raise PlayerError('%s attacking %s, but you own both.'
701 raise PlayerError('%s attacking %s with 0 armies.'
703 if armies >= source.armies:
704 raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
705 % (source, target, armies, source.armies-1))
706 a_dice = sorted([random.randint(1, 6) for i in range(armies)],
708 t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
712 for a,d in zip(a_dice, t_dice):
717 source.armies -= a_dead
718 target.armies -= t_dead
719 if target.armies == 0:
720 self.takeover(source, target, remaining_attackers=armies-a_dead)
721 self.log('%s conquered %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d'
722 % (source.player, target, source, armies, len(t_dice),
723 a_dead, t_dead, source.armies, target.armies))
724 assert target.armies > 0, target
726 self.log('%s attacked %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d' \
727 % (source.player, target, source, armies, len(t_dice),
728 a_dead, t_dead, source.armies, target.armies))
729 assert target.armies > 0, target
731 def takeover(self, source, target, remaining_attackers):
732 source.armies -= remaining_attackers
733 target.armies += remaining_attackers
734 target.player = source.player
735 if source.armies > 1:
739 support = source.player.support_attack(
740 self.world, self.log, error, source, target)
741 if support < 0 or support >= source.armies:
743 'Cannot support from %s to %s with %d armies, only %d available'
744 % (source, target, support, source.armies-1))
746 except PlayerError, error:
748 source.armies -= support
749 target.armies += support
750 def player_killed(self, player, killer):
752 killer.hand.extend(player.hand)
753 if len(self.living_players()) > 1:
754 while len(killer.hand) > 5:
755 self.play_cards_and_place_armies(killer)
756 self.log('%s killed by %s' % (player, killer))
757 if len(self.living_players()) > 1:
758 player.report(self.world, self.log)
759 # else the game is over, and killed will hear about this then.
760 def end_of_turn_cards(self, player, captures):
761 """Deal end-of-turn reward for any territory captures.
763 if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
765 def living_players(self):
766 return [p for p in self.players if p.alive == True]
769 def generate_earth():
771 c = Continent('North America', 5)
772 c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))
773 c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
774 c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
775 c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
776 c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
777 c.append(Territory('Quebec', 'que', 3, ['eus']))
778 c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
779 c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
780 c.append(Territory('Central America', 'cam', 3, ['ven']))
783 c = Continent('Europe', 5)
784 c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
785 c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
786 c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
787 c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
788 c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
789 c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
790 c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
793 c = Continent('Asia', 7)
794 c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
795 c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
796 c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
797 c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
798 c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
799 c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
800 c.append(Territory('Japan', 'jap', 2))
801 c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
802 c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
803 c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
804 c.append(Territory('India', 'indi', 3, ['sia']))
805 c.append(Territory('Siam', 'sia', 1, ['indo']))
809 c = Continent('South America', 2)
810 c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
811 c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
812 c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
813 c.append(Territory('Argentina', 'arg', 2))
816 c = Continent('Africa', 3)
817 c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
818 c.append(Territory('Egypt', 'egy', 1, ['eaf']))
819 c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
820 c.append(Territory('Congo', 'con', 3, ['saf']))
821 c.append(Territory('South Africa', 'saf', 1, ['mad']))
822 c.append(Territory('Madagascar', 'mad', 2))
825 c = Continent('Australia', 2)
826 c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
827 c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
828 c.append(Territory('Western Australia', 'wau', 2, ['eau']))
829 c.append(Territory('Eastern Australia', 'eau', 3))
832 w._resolve_link_names()
837 failures,tests = doctest.testmod(sys.modules[__name__])
841 from player.email import IncomingEmailDispatcher, OutgoingEmailDispatcher, EmailPlayer
842 world = generate_earth()
843 ied = IncomingEmailDispatcher(fifo_path='/tmp/pyrisk.in')
844 oed = OutgoingEmailDispatcher(return_address='server@example.com')
845 players = [EmailPlayer('Alice', 'alice@big.edu', ied, oed),
846 Player('Bob'), Player('Charlie')]
847 e = Engine(world, players)
851 if __name__ == '__main__':
853 failures = self.test()