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, BeginGame, EndGame, Killed, StartTurn, DealtCards, \
23 EarnsArmies, SelectTerritory, PlaceArmies, PlayCards, Attack, Conquer
26 class PlayerError (Exception):
29 class NameMixin (object):
30 """Simple mixin for pretty-printing named objects.
32 def __init__(self, name):
40 class ID_CmpMixin (object):
41 """Simple mixin to ensure the fancier comparisons are all based on
44 def __cmp__(self, other):
45 return cmp(id(self), id(other))
46 def __eq__(self, other):
47 return self.__cmp__(other) == 0
48 def __ne__(self, other):
49 return self.__cmp__(other) != 0
51 class Territory (NameMixin, ID_CmpMixin, list):
52 """An occupiable territory.
54 Contains a list of neighboring territories.
56 def __init__(self, name, short_name=None, type=-1,
57 link_names=[], continent=None, player=None):
58 NameMixin.__init__(self, name)
59 ID_CmpMixin.__init__(self)
61 self.short_name = short_name
62 if short_name == None:
63 self.short_name = name
64 self._card_type = type # for Deck construction
65 self._link_names = list(link_names) # used by World._resolve_link_names
66 self.continent = continent # used by World.production
67 self.player = player # who owns this territory
68 self.armies = 0 # number of occupying armies
70 if self.short_name == self.name:
72 return '%s (%s)' % (self.name, self.short_name)
73 def borders(self, other):
75 if id(t) == id(other):
79 class Continent (NameMixin, ID_CmpMixin, list):
80 """A group of Territories.
82 Stores the army-production bonus if it's owned by a single player.
84 def __init__(self, name, production, territories=[]):
85 NameMixin.__init__(self, name)
86 ID_CmpMixin.__init__(self)
87 list.__init__(self, territories)
88 self.production = production
89 def append(self, territory):
90 """Add a new territory (setting the territory's .continent
93 list.append(self, territory)
94 territory.continent = self
95 def territory_by_name(self, name):
96 """Find a Territory instance by name (long or short, case
100 if name.lower() in [t.short_name.lower(), t.name.lower()]:
103 def single_player(self):
104 """Is the continent owned by a single player?
107 for territory in self:
108 if territory.player != p:
112 class World (NameMixin, ID_CmpMixin, list):
113 """Store the world map and current world state.
115 Holds list of Continents. Also controls territory-based army
116 production (via production).
118 def __init__(self, name, continents=[]):
119 NameMixin.__init__(self, name)
120 ID_CmpMixin.__init__(self)
121 list.__init__(self, continents)
122 self.initial_armies = { # num_players:num_armies
123 2: 40, 3:35, 4:30, 5:25, 6:20
125 def territories(self):
126 """Iterate through all the territories in the world.
128 for continent in self:
129 for territory in continent:
131 def territory_by_name(self, name):
132 """Find a Territory instance by name (long or short, case
135 for continent in self:
137 return continent.territory_by_name(name)
141 def continent_by_name(self, name):
142 """Find a Continent instance by name (case insensitive).
144 for continent in self:
145 if continent.name.lower() == name.lower():
148 def _resolve_link_names(self):
149 """Initialize Territory links.
151 The Territory class doesn't actually link to neighbors after
152 initialization, but one of each linked pair has the others
153 name in _link_names. This method goes through the territories,
154 looks up the referenced link target, and joins the pair.
156 self._check_short_names()
157 for territory in self.territories():
158 for name in territory._link_names:
159 other = self.territory_by_name(name)
160 if not territory.borders(other):
161 territory.append(other)
162 if not other.borders(territory):
163 other.append(territory)
164 def _check_short_names(self):
165 """Ensure there are no short_name collisions.
168 for t in self.territories():
169 if t.short_name.lower() not in ts:
170 ts[t.short_name.lower()] = t
172 raise ValueError('%s shared by %s and %s'
173 % (t.short_name.lower(), ts[t.short_name.lower()], t))
174 def production(self, player):
175 """Calculate the number of armies a player should earn based
176 on territory occupation.
178 ts = list(player.territories(self))
179 production = max(3, len(ts) / 3)
180 continents = set([t.continent.name for t in ts])
181 for c_name in continents:
182 c = self.continent_by_name(c_name)
183 if c.single_player() == True:
184 production += c.production
185 return (production, {})
186 def place_territory_production(self, territory_production):
187 """Place armies based on {territory_name: num_armies, ...}.
189 for territory_name,production in territory_production.items():
190 t = self.territory_by_name(territory_name)
191 t.armies += production
193 class Card (ID_CmpMixin):
194 """Represent a territory card (or wild)
196 Nothing exciting going on here, just a class for pretty-printing
199 def __init__(self, deck, type_, territory=None):
200 ID_CmpMixin.__init__(self)
202 self.territory = territory
205 if self.territory == None:
206 return '<Card %s>' % (self.deck.type_names[self.type])
208 return '<Card %s %s>' % (self.territory,
209 self.deck.type_names[self.type])
211 return self.__str__()
214 """All the cards yet to be handed out in a given game.
216 Controls the type branding (via type_names) and army production
217 values for scoring sets (via production_value).
219 def __init__(self, territories=[], num_wilds=2,
220 type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
221 list.__init__(self, [Card(self, t._card_type, t) for t in territories])
222 self.type_names = type_names
223 for i in range(num_wilds):
224 self.append(Card(self, 0))
225 self._production_sequence = [4, 6, 8, 10, 12, 15]
226 self._production_index = 0
228 """Shuffle the remaining cards in the deck.
231 def production_value(self, index):
234 >>> [d.production_value(i) for i in range(8)]
235 [4, 6, 8, 10, 12, 15, 20, 25]
237 if index < len(self._production_sequence):
238 return self._production_sequence[index]
239 extra = index - len(self._production_sequence) + 1
240 return self._production_sequence[-1] + 5 * extra
241 def production(self, player, cards=None):
244 >>> a = Player('Alice')
245 >>> b = Player('Bob')
246 >>> d.production(a, None)
248 >>> d.production(a, [Card(d, 1, Territory('a')),
249 ... Card(d, 1, Territory('b'))])
250 Traceback (most recent call last):
252 PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
253 >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
254 ... Card(d, 1, Territory('b', player=b)),
255 ... Card(d, 1, Territory('c'))])
257 >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
258 ... Card(d, 2, Territory('b', player=a)),
259 ... Card(d, 0, Territory('c', player=a))])
262 >>> sorted(tp.items())
263 [('a', 1), ('b', 1), ('c', 1)]
269 p = self.production_value(self._production_index)
270 self._production_index += 1
271 territory_production = {}
273 if c.territory != None and c.territory.player == player:
274 territory_production[c.territory.name] = 1
275 return (p, territory_production)
276 raise PlayerError('%s is not a scoring set' % h)
279 """Represent a hand of cards.
281 This is the place to override the set of allowed scoring
282 combinations. You should override one of
288 Adding additional scoring methods as needed (e.g. flush).
290 def __init__(self, cards=[]):
291 list.__init__(self, cards)
295 s = sorted(set([card.type for card in self]))
297 or (len(s) == 2 and s[0] == 0):
303 if len(set([card.type for card in self])) == 3:
307 """The hand is any valid scoring combination.
309 return self.set() or self.run()
310 def subhands(self, lengths=None):
311 """Return all possible subhands.
313 Lengths can either be a list of allowed subhand lengths or
314 None. If None, all possible subhand lengths are allowed.
317 >>> h = Hand([Card(d, 1, Territory('a')),
318 ... Card(d, 1, Territory('b')),
319 ... Card(d, 1, Territory('c')),
320 ... Card(d, 1, Territory('d'))])
321 >>> for hand in h.subhands():
327 [<Card a Infantry>, <Card b Infantry>]
328 [<Card a Infantry>, <Card c Infantry>]
329 [<Card a Infantry>, <Card d Infantry>]
330 [<Card b Infantry>, <Card c Infantry>]
331 [<Card b Infantry>, <Card d Infantry>]
332 [<Card c Infantry>, <Card d Infantry>]
333 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
334 [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
335 [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
336 [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
337 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
339 for i in range(len(self)):
340 i += 1 # check all sub-hands of length i
341 if lengths != None and i not in lengths:
342 continue # don't check this length
344 stop = range(len(self)-i, len(self))
345 while indices != stop:
346 yield Hand([self[i] for i in indices])
347 indices = self._increment(indices, stop)
348 yield Hand([self[i] for i in indices])
349 def _increment(self, indices, stop):
352 >>> h = Hand([Card(d, 1, Territory('a'))])
353 >>> h._increment([0, 1, 2], [2, 3, 4])
355 >>> h._increment([0, 1, 3], [2, 3, 4])
357 >>> h._increment([0, 1, 4], [2, 3, 4])
360 moveable = [i for i,m in zip(indices, stop) if i < m]
361 assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
362 key = indices.index(moveable[-1])
363 new = indices[key] + 1
364 for i in range(key, len(indices)):
365 indices[i] = new + i-key
368 """Return a list of all possible scoring subhands.
370 for h in self.subhands():
374 class Player (NameMixin, ID_CmpMixin):
375 """Represent a risk player.
377 This class implements a very basic AI player. Subclasses should
378 consider overriding the "action-required" methods:
386 And the "report" methods:
391 def __init__(self, name):
392 NameMixin.__init__(self, name)
393 ID_CmpMixin.__init__(self)
396 self._message_index = 0
397 def territories(self, world):
398 """Iterate through all territories owned by this player.
400 for t in world.territories():
403 def border_territories(self, world):
404 """Iterate through all territories owned by this player which
405 border another player's territories.
407 for t in self.territories(world):
409 if neighbor.player != self:
412 def report(self, world, log):
413 """Send reports about death and game endings.
415 These events mark the end of contact and require no change in
416 player status or response, so they get a special command
417 seperate from the usual action family. The action commands in
418 Player subclasses can notify the player (possibly by calling
419 report internally) if they feel so inclined.
423 draw - another notification-only method
425 print 'Reporting for %s:\n %s' \
426 % (self, '\n '.join([str(e) for e in log[self._message_index:]]))
427 self._message_index = len(log)
428 def draw(self, world, log, cards=[]):
429 """Only called if you earned a new card (or cards).
433 report - another notification-only method
436 def select_territory(self, world, log, error=None):
437 """Return the selected territory's name.
439 free_territories = [t for t in world.territories() if t.player == None]
440 return random.sample(free_territories, 1)[0].name
441 def play_cards(self, world, log, error=None,
443 """Decide whether or not to turn in a set of cards.
445 Return a list of cards to turn in or None. If play_required
446 is True, you *must* play.
448 if play_required == True:
449 return random.sample(list(self.hand.possible()), 1)[0]
450 def place_armies(self, world, log, error=None,
451 remaining=1, this_round=1):
452 """Both during setup and before each turn.
454 Return {territory_name: num_armies, ...}
456 t = random.sample(list(self.border_territories(world)), 1)[0]
457 return {t.name: this_round}
458 def attack_and_fortify(self, world, log, error=None,
460 """Return list of (source, target, armies) tuples. Place None
461 in the list to end this phase.
463 assert mode != 'fortify', mode
464 possible_attacks = []
465 for t in self.border_territories(world):
466 if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
468 targets = [border_t for border_t in t if border_t.player != self]
470 possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
471 if len(possible_attacks) == 0:
472 return [None, None] # stop attack phase, then stop fortification phase
473 return random.sample(possible_attacks, 1) # + [None]
474 def support_attack(self, world, log, error,
476 """Follow up on a conquest by moving additional armies.
478 return source.armies-1
480 class Engine (ID_CmpMixin):
483 Basic usage will be along the lines of
485 >>> world = generate_earth()
486 >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
487 >>> e = Engine(world, players)
488 >>> e.run() # doctest: +ELLIPSIS
491 def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
492 ID_CmpMixin.__init__(self)
494 self.deck = deck_class(world.territories())
495 self.log = logger_class()
496 self.players = players
498 return '<engine %s %s>' % (self.world, self.players)
500 return self.__str__()
502 """The main entry point.
508 """Setup phase. Pick territories, place initial armies, and
511 for p in self.players:
513 random.shuffle(self.players)
514 self.log(BeginGame(self.players))
516 self.select_territories()
517 self.place_initial_armies()
518 for p in self.players:
521 """Main gameplay phase. Take turns until only one Player survives.
525 living = len(self.living_players())
527 self.play_turn(self.players[active_player])
528 living = len(self.living_players())
529 active_player = (active_player + 1) % len(self.players)
531 while self.players[active_player].alive == False:
532 active_player = (active_player + 1) % len(self.players)
535 """The end of the game.
537 Currently just a notification hook.
539 self.log(EndGame(self.players))
540 for p in self.players:
541 p.report(self.world, self.log)
542 def play_turn(self, player):
543 """Work through the phases of player's turn.
545 self.log(StartTurn(player, self.players, self.world))
546 self.play_cards_and_place_armies(player)
547 captures = self.attack_and_fortify(player)
548 self.end_of_turn_cards(player, captures)
549 def select_territories(self):
550 for t in self.world.territories():
552 num_terrs = len(list(self.world.territories()))
553 for i in range(num_terrs-1):
554 p = self.players[i % len(self.players)]
558 t_name = p.select_territory(self.world, self.log, error)
560 t = self.world.territory_by_name(t_name)
562 raise PlayerError('Invalid territory "%s"' % t_name)
564 raise PlayerError('Cannot select %s owned by %s'
567 except PlayerError, error:
569 self.log(SelectTerritory(p, t))
572 # last player has no choice.
573 p = self.players[(num_terrs-1) % len(self.players)]
574 t = [t for t in self.world.territories() if t.player == None][0]
577 def place_initial_armies(self):
578 already_placed = [len(list(p.territories(self.world))) for p in self.players]
579 s = list(set(already_placed))
580 assert len(s) in [1,2], already_placed
581 if len(s) == 2: # catch up the players who are one territory short
582 assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
583 for p,placed in zip(self.players, already_placed):
585 self.player_place_armies(p, remaining, 1)
586 remaining = self.world.initial_armies[len(self.players)] - max(s)
588 for p in self.players:
589 self.player_place_armies(p, remaining, 1)
591 def player_place_armies(self, player, remaining=1, this_round=1):
595 placements = player.place_armies(self.world, self.log, error,
596 remaining, this_round)
597 if sum(placements.values()) != this_round:
598 raise PlayerError('Placing more than %d armies' % this_round)
599 for ter_name,armies in placements.items():
601 t = self.world.territory_by_name(ter_name)
603 raise PlayerError('Invalid territory "%s"' % t_name)
604 if t.player != player:
605 raise PlayerError('Placing armies in %s owned by %s'
608 raise PlayerError('Placing a negative number of armies (%d) in %s'
611 except PlayerError, error:
613 self.log(PlaceArmies(player, placements))
614 for terr_name,armies in placements.items():
615 t = self.world.territory_by_name(terr_name)
617 def deal(self, player, number):
619 for i in range(number):
620 cards.append(self.deck.pop())
621 player.hand.extend(cards)
622 player.draw(self.world, self.log, cards)
623 self.log(DealtCards(player, number, len(self.deck)))
624 def play_cards_and_place_armies(self, player, additional_armies=0):
625 cards_required = len(player.hand) >= 5
629 cards = player.play_cards(
630 self.world, self.log, error, play_required=cards_required)
631 if cards_required == True and cards == None:
632 raise PlayerError('You have %d >= 5 cards in your hand, you must play'
634 c_prod,c_terr_prod = self.deck.production(player, cards)
636 except PlayerError, error:
638 w_prod,w_terr_prod = self.world.production(player)
639 self.log(EarnsArmies(player, w_prod, w_terr_prod))
642 player.hand.remove(c)
643 self.log(PlayCards(player, cards, c_prod, c_terr_prod))
644 for terr,prod in c_terr_prod.items():
645 if terr in w_terr_prod:
646 w_terr_prod[terr] += prod
648 w_terr_prod[terr] = prod
649 self.world.place_territory_production(w_terr_prod)
650 armies = w_prod + c_prod
651 self.player_place_armies(player, armies, armies)
652 def attack_and_fortify(self, player):
658 actions = player.attack_and_fortify(self.world, self.log, error, mode)
659 for action in actions:
665 assert mode == 'fortify', mode
667 source_name,target_name,armies = action
669 source = self.world.territory_by_name(source_name)
671 raise PlayerError('Invalid territory "%s"' % source_name)
673 target = self.world.territory_by_name(target_name)
675 raise PlayerError('Invalid territory "%s"' % targer_name)
677 tplayer = target.player
678 capture = self.attack(source, target, armies)
681 if len(list(tplayer.territories(self.world))) == 0:
682 self.player_killed(tplayer, killer=player)
684 assert mode == 'fortify', mode
685 self.fortify(source, target, armies)
686 except PlayerError, error:
688 def attack(self, source, target, armies):
689 if source.player == target.player:
690 raise PlayerError('%s attacking %s, but you own both.'
693 raise PlayerError('%s attacking %s with 0 armies.'
695 if armies >= source.armies:
696 raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
697 % (source, target, armies, source.armies-1))
698 if not source.borders(target):
699 raise PlayerError('Cannot reach %s from %s to attack'
701 s_dice = sorted([random.randint(1, 6) for i in range(armies)],
703 t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
707 for a,d in zip(s_dice, t_dice):
712 source.armies -= s_dead
713 target.armies -= t_dead
714 if target.armies == 0:
715 self.takeover(source, target, remaining_attackers=armies-s_dead)
716 self.log(Conquer(source, target, s_dice, t_dice, s_dead, t_dead))
717 assert target.armies > 0, target
719 self.log(Attack(source, target, s_dice, t_dice, s_dead, t_dead))
720 assert target.armies > 0, target
722 def takeover(self, source, target, remaining_attackers):
723 source.armies -= remaining_attackers
724 target.armies += remaining_attackers
725 target.player = source.player
726 if source.armies > 1:
730 support = source.player.support_attack(
731 self.world, self.log, error, source, target)
732 if support < 0 or support >= source.armies:
734 'Cannot support from %s to %s with %d armies, only %d available'
735 % (source, target, support, source.armies-1))
737 except PlayerError, error:
739 source.armies -= support
740 target.armies += support
741 def fortify(self, source, target, armies):
742 if source.player != target.player:
743 raise PlayerError('%s (%s) cannot fortifiy %s (%s).'
744 % (source, source.player, target, target.player))
747 if armies >= source.armies:
748 raise PlayerError('%s fortifying %s with %d armies, but only %d are available.'
749 % (source, target, armies, source.armies-1))
750 if not source.borders(target):
751 raise PlayerError('Cannot reach %s from %s to fortify'
753 source.armies -= armies
754 target.armies += armies
755 self.log(Fortify(source, target, armies))
756 def player_killed(self, player, killer):
758 killer.hand.extend(player.hand)
759 if len(self.living_players()) > 1:
760 while len(killer.hand) > 5:
761 self.play_cards_and_place_armies(killer)
762 self.log(Killed(player, killer))
763 if len(self.living_players()) > 1:
764 player.report(self.world, self.log)
765 # else the game is over, and killed will hear about this then.
766 def end_of_turn_cards(self, player, captures):
767 """Deal end-of-turn reward for any territory captures.
769 if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
771 def living_players(self):
772 return [p for p in self.players if p.alive == True]
775 def generate_earth():
777 c = Continent('North America', 5)
778 c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))
779 c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
780 c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
781 c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
782 c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
783 c.append(Territory('Quebec', 'que', 3, ['eus']))
784 c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
785 c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
786 c.append(Territory('Central America', 'cam', 3, ['ven']))
789 c = Continent('Europe', 5)
790 c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
791 c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
792 c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
793 c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
794 c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
795 c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
796 c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
799 c = Continent('Asia', 7)
800 c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
801 c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
802 c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
803 c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
804 c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
805 c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
806 c.append(Territory('Japan', 'jap', 2))
807 c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
808 c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
809 c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
810 c.append(Territory('India', 'indi', 3, ['sia']))
811 c.append(Territory('Siam', 'sia', 1, ['indo']))
815 c = Continent('South America', 2)
816 c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
817 c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
818 c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
819 c.append(Territory('Argentina', 'arg', 2))
822 c = Continent('Africa', 3)
823 c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
824 c.append(Territory('Egypt', 'egy', 1, ['eaf']))
825 c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
826 c.append(Territory('Congo', 'con', 3, ['saf']))
827 c.append(Territory('South Africa', 'saf', 1, ['mad']))
828 c.append(Territory('Madagascar', 'mad', 2))
831 c = Continent('Australia', 2)
832 c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
833 c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
834 c.append(Territory('Western Australia', 'wau', 2, ['eau']))
835 c.append(Territory('Eastern Australia', 'eau', 3))
838 w._resolve_link_names()
843 failures,tests = doctest.testmod(sys.modules[__name__])
847 world = generate_earth()
848 players = [Player('Alice'), Player('Bob'), Player('Charlie')]
849 e = Engine(world, players)
852 if __name__ == '__main__':
854 failures = self.test()