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, \
27 class PlayerError (Exception):
30 class NameMixin (object):
31 """Simple mixin for pretty-printing named objects.
33 def __init__(self, name):
41 class ID_CmpMixin (object):
42 """Simple mixin to ensure the fancier comparisons are all based on
45 def __cmp__(self, other):
46 return cmp(id(self), id(other))
47 def __eq__(self, other):
48 return self.__cmp__(other) == 0
49 def __ne__(self, other):
50 return self.__cmp__(other) != 0
52 class Territory (NameMixin, ID_CmpMixin, list):
53 """An occupiable territory.
55 Contains a list of neighboring territories.
57 def __init__(self, name, short_name=None, type=-1,
58 link_names=[], continent=None, player=None):
59 NameMixin.__init__(self, name)
60 ID_CmpMixin.__init__(self)
62 self.short_name = short_name
63 if short_name == None:
64 self.short_name = name
65 self._card_type = type # for Deck construction
66 self._link_names = list(link_names) # used by World._resolve_link_names
67 self.continent = continent # used by World.production
68 self.player = player # who owns this territory
69 self.armies = 0 # number of occupying armies
71 if self.short_name == self.name:
73 return '%s (%s)' % (self.name, self.short_name)
74 def borders(self, other):
76 if id(t) == id(other):
81 if t.player != self.player:
85 class Continent (NameMixin, ID_CmpMixin, list):
86 """A group of Territories.
88 Stores the army-production bonus if it's owned by a single player.
90 def __init__(self, name, production, territories=[]):
91 NameMixin.__init__(self, name)
92 ID_CmpMixin.__init__(self)
93 list.__init__(self, territories)
94 self.production = production
95 def append(self, territory):
96 """Add a new territory (setting the territory's .continent
99 list.append(self, territory)
100 territory.continent = self
101 def territory_by_name(self, name):
102 """Find a Territory instance by name (long or short, case
106 if name.lower() in [t.short_name.lower(), t.name.lower()]:
109 def single_player(self):
110 """Is the continent owned by a single player?
113 for territory in self:
114 if territory.player != p:
118 class World (NameMixin, ID_CmpMixin, list):
119 """Store the world map and current world state.
121 Holds list of Continents. Also controls territory-based army
122 production (via production).
124 def __init__(self, name, continents=[]):
125 NameMixin.__init__(self, name)
126 ID_CmpMixin.__init__(self)
127 list.__init__(self, continents)
128 self.initial_armies = { # num_players:num_armies
129 2: 40, 3:35, 4:30, 5:25, 6:20
131 def territories(self):
132 """Iterate through all the territories in the world.
134 for continent in self:
135 for territory in continent:
137 def territory_by_name(self, name):
138 """Find a Territory instance by name (long or short, case
141 for continent in self:
143 return continent.territory_by_name(name)
147 def continent_by_name(self, name):
148 """Find a Continent instance by name (case insensitive).
150 for continent in self:
151 if continent.name.lower() == name.lower():
154 def _resolve_link_names(self):
155 """Initialize Territory links.
157 The Territory class doesn't actually link to neighbors after
158 initialization, but one of each linked pair has the others
159 name in _link_names. This method goes through the territories,
160 looks up the referenced link target, and joins the pair.
162 self._check_short_names()
163 for territory in self.territories():
164 for name in territory._link_names:
165 other = self.territory_by_name(name)
166 if not territory.borders(other):
167 territory.append(other)
168 if not other.borders(territory):
169 other.append(territory)
170 def _check_short_names(self):
171 """Ensure there are no short_name collisions.
174 for t in self.territories():
175 if t.short_name.lower() not in ts:
176 ts[t.short_name.lower()] = t
178 raise ValueError('%s shared by %s and %s'
179 % (t.short_name.lower(), ts[t.short_name.lower()], t))
180 def production(self, player):
181 """Calculate the number of armies a player should earn based
182 on territory occupation.
184 ts = list(player.territories(self))
185 production = max(3, len(ts) / 3)
186 continents = set([t.continent.name for t in ts])
187 for c_name in continents:
188 c = self.continent_by_name(c_name)
189 if c.single_player() == True:
190 production += c.production
191 return (production, {})
192 def place_territory_production(self, territory_production):
193 """Place armies based on {territory_name: num_armies, ...}.
195 for territory_name,production in territory_production.items():
196 t = self.territory_by_name(territory_name)
197 t.armies += production
199 class Card (ID_CmpMixin):
200 """Represent a territory card (or wild)
202 Nothing exciting going on here, just a class for pretty-printing
205 def __init__(self, deck, type_, territory=None):
206 ID_CmpMixin.__init__(self)
208 self.territory = territory
211 if self.territory == None:
212 return '<Card %s>' % (self.deck.type_names[self.type])
214 return '<Card %s %s>' % (self.territory,
215 self.deck.type_names[self.type])
217 return self.__str__()
220 """All the cards yet to be handed out in a given game.
222 Controls the type branding (via type_names) and army production
223 values for scoring sets (via production_value).
225 def __init__(self, territories=[], num_wilds=2,
226 type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
227 list.__init__(self, [Card(self, t._card_type, t) for t in territories])
228 self.type_names = type_names
229 for i in range(num_wilds):
230 self.append(Card(self, 0))
231 self._production_sequence = [4, 6, 8, 10, 12, 15]
232 self._production_index = 0
234 """Shuffle the remaining cards in the deck.
237 def production_value(self, index):
240 >>> [d.production_value(i) for i in range(8)]
241 [4, 6, 8, 10, 12, 15, 20, 25]
243 if index < len(self._production_sequence):
244 return self._production_sequence[index]
245 extra = index - len(self._production_sequence) + 1
246 return self._production_sequence[-1] + 5 * extra
247 def production(self, player, cards=None):
250 >>> a = Player('Alice')
251 >>> b = Player('Bob')
252 >>> d.production(a, None)
254 >>> d.production(a, [Card(d, 1, Territory('a')),
255 ... Card(d, 1, Territory('b'))])
256 Traceback (most recent call last):
258 PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
259 >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
260 ... Card(d, 1, Territory('b', player=b)),
261 ... Card(d, 1, Territory('c'))])
263 >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
264 ... Card(d, 2, Territory('b', player=a)),
265 ... Card(d, 0, Territory('c', player=a))])
268 >>> sorted(tp.items())
269 [('a', 1), ('b', 1), ('c', 1)]
275 p = self.production_value(self._production_index)
276 self._production_index += 1
277 territory_production = {}
279 if c.territory != None and c.territory.player == player:
280 territory_production[c.territory.name] = 1
281 return (p, territory_production)
282 raise PlayerError('%s is not a scoring set' % h)
285 """Represent a hand of cards.
287 This is the place to override the set of allowed scoring
288 combinations. You should override one of
294 Adding additional scoring methods as needed (e.g. flush).
296 def __init__(self, cards=[]):
297 list.__init__(self, cards)
301 s = sorted(set([card.type for card in self]))
303 or (len(s) == 2 and s[0] == 0):
309 if len(set([card.type for card in self])) == 3:
313 """The hand is any valid scoring combination.
315 return self.set() or self.run()
316 def subhands(self, lengths=None):
317 """Return all possible subhands.
319 Lengths can either be a list of allowed subhand lengths or
320 None. If None, all possible subhand lengths are allowed.
323 >>> h = Hand([Card(d, 1, Territory('a')),
324 ... Card(d, 1, Territory('b')),
325 ... Card(d, 1, Territory('c')),
326 ... Card(d, 1, Territory('d'))])
327 >>> for hand in h.subhands():
333 [<Card a Infantry>, <Card b Infantry>]
334 [<Card a Infantry>, <Card c Infantry>]
335 [<Card a Infantry>, <Card d Infantry>]
336 [<Card b Infantry>, <Card c Infantry>]
337 [<Card b Infantry>, <Card d Infantry>]
338 [<Card c Infantry>, <Card d Infantry>]
339 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
340 [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
341 [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
342 [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
343 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
345 for i in range(len(self)):
346 i += 1 # check all sub-hands of length i
347 if lengths != None and i not in lengths:
348 continue # don't check this length
350 stop = range(len(self)-i, len(self))
351 while indices != stop:
352 yield Hand([self[i] for i in indices])
353 indices = self._increment(indices, stop)
354 yield Hand([self[i] for i in indices])
355 def _increment(self, indices, stop):
358 >>> h = Hand([Card(d, 1, Territory('a'))])
359 >>> h._increment([0, 1, 2], [2, 3, 4])
361 >>> h._increment([0, 1, 3], [2, 3, 4])
363 >>> h._increment([0, 1, 4], [2, 3, 4])
366 moveable = [i for i,m in zip(indices, stop) if i < m]
367 assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
368 key = indices.index(moveable[-1])
369 new = indices[key] + 1
370 for i in range(key, len(indices)):
371 indices[i] = new + i-key
374 """Return a list of all possible scoring subhands.
376 for h in self.subhands():
380 class Player (NameMixin, ID_CmpMixin):
381 """Represent a risk player.
383 This class implements a very basic AI player. Subclasses should
384 consider overriding the "action-required" methods:
392 And the "report" methods:
397 def __init__(self, name):
398 NameMixin.__init__(self, name)
399 ID_CmpMixin.__init__(self)
402 self._message_index = 0
403 def territories(self, world):
404 """Iterate through all territories owned by this player.
406 for t in world.territories():
409 def report(self, world, log):
410 """Send reports about death and game endings.
412 These events mark the end of contact and require no change in
413 player status or response, so they get a special command
414 seperate from the usual action family. The action commands in
415 Player subclasses can notify the player (possibly by calling
416 report internally) if they feel so inclined.
420 draw - another notification-only method
422 print 'Reporting for %s:\n %s' \
423 % (self, '\n '.join([str(e) for e in log[self._message_index:]]))
424 self._message_index = len(log)
425 def draw(self, world, log, cards=[]):
426 """Only called if you earned a new card (or cards).
430 report - another notification-only method
433 def select_territory(self, world, log, error=None):
434 """Return the selected territory's name.
436 free_territories = [t for t in world.territories() if t.player == None]
437 return random.sample(free_territories, 1)[0].name
438 def play_cards(self, world, log, error=None,
440 """Decide whether or not to turn in a set of cards.
442 Return a list of cards to turn in or None. If play_required
443 is True, you *must* play.
445 if play_required == True:
446 return random.sample(list(self.hand.possible()), 1)[0]
447 def place_armies(self, world, log, error=None,
448 remaining=1, this_round=1):
449 """Both during setup and before each turn.
451 Return {territory_name: num_armies, ...}
453 terr = random.sample([t for t in self.territories(world)
454 if t.border()], 1)[0]
455 return {terr.name: this_round}
456 def attack_and_fortify(self, world, log, error=None,
458 """Return list of (source, target, armies) tuples. Place None
459 in the list to end this phase.
461 assert mode != 'fortify', mode
462 possible_attacks = []
463 for t in self.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(
471 (t.name, tg.name, min(3, t.armies-1)))
472 if len(possible_attacks) == 0:
474 for t in self.territories(world):
475 if t.border() or t.armies == 1:
479 fortifications.append(
480 (t.name, tg.name, t.armies-1))
481 if len(fortifications) > 1:
482 fortifications = random.sample(fortifications, 1)
483 # stop attack phase, fortify, stop fortification phase
484 return [None] + fortifications + [None]
485 return random.sample(possible_attacks, 1)
486 def support_attack(self, world, log, error,
488 """Follow up on a conquest by moving additional armies.
490 return source.armies-1
492 class Engine (ID_CmpMixin):
495 Basic usage will be along the lines of
497 >>> world = generate_earth()
498 >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
499 >>> e = Engine(world, players)
500 >>> e.run() # doctest: +ELLIPSIS
503 def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
504 ID_CmpMixin.__init__(self)
506 self.deck = deck_class(world.territories())
507 self.log = logger_class()
508 self.players = players
510 return '<engine %s %s>' % (self.world, self.players)
512 return self.__str__()
514 """The main entry point.
520 """Setup phase. Pick territories, place initial armies, and
523 for p in self.players:
525 random.shuffle(self.players)
526 self.log(BeginGame(self.players))
528 self.select_territories()
529 self.place_initial_armies()
530 for p in self.players:
533 """Main gameplay phase. Take turns until only one Player survives.
537 living = len(self.living_players())
539 self.play_turn(self.players[active_player])
540 living = len(self.living_players())
541 active_player = (active_player + 1) % len(self.players)
543 while self.players[active_player].alive == False:
544 active_player = (active_player + 1) % len(self.players)
547 """The end of the game.
549 Currently just a notification hook.
551 self.log(EndGame(self.players))
552 for p in self.players:
553 p.report(self.world, self.log)
554 def play_turn(self, player):
555 """Work through the phases of player's turn.
557 self.log(StartTurn(player, self.players, self.world))
558 self.play_cards_and_place_armies(player)
559 captures = self.attack_and_fortify(player)
560 self.end_of_turn_cards(player, captures)
561 def select_territories(self):
562 for t in self.world.territories():
564 num_terrs = len(list(self.world.territories()))
565 for i in range(num_terrs-1):
566 p = self.players[i % len(self.players)]
570 t_name = p.select_territory(self.world, self.log, error)
572 t = self.world.territory_by_name(t_name)
574 raise PlayerError('Invalid territory "%s"' % t_name)
576 raise PlayerError('Cannot select %s owned by %s'
579 except PlayerError, error:
581 self.log(SelectTerritory(p, t))
584 # last player has no choice.
585 p = self.players[(num_terrs-1) % len(self.players)]
586 t = [t for t in self.world.territories() if t.player == None][0]
589 def place_initial_armies(self):
590 already_placed = [len(list(p.territories(self.world))) for p in self.players]
591 s = list(set(already_placed))
592 assert len(s) in [1,2], already_placed
593 if len(s) == 2: # catch up the players who are one territory short
594 assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
595 for p,placed in zip(self.players, already_placed):
597 self.player_place_armies(p, remaining, 1)
598 remaining = self.world.initial_armies[len(self.players)] - max(s)
600 for p in self.players:
601 self.player_place_armies(p, remaining, 1)
603 def player_place_armies(self, player, remaining=1, this_round=1):
607 placements = player.place_armies(self.world, self.log, error,
608 remaining, this_round)
609 if sum(placements.values()) != this_round:
610 raise PlayerError('Placing more than %d armies' % this_round)
611 for ter_name,armies in placements.items():
613 t = self.world.territory_by_name(ter_name)
615 raise PlayerError('Invalid territory "%s"' % t_name)
616 if t.player != player:
617 raise PlayerError('Placing armies in %s owned by %s'
620 raise PlayerError('Placing a negative number of armies (%d) in %s'
623 except PlayerError, error:
625 self.log(PlaceArmies(player, placements))
626 for terr_name,armies in placements.items():
627 t = self.world.territory_by_name(terr_name)
629 def deal(self, player, number):
631 for i in range(number):
632 cards.append(self.deck.pop())
633 player.hand.extend(cards)
634 player.draw(self.world, self.log, cards)
635 self.log(DealtCards(player, number, len(self.deck)))
636 def play_cards_and_place_armies(self, player, additional_armies=0):
637 cards_required = len(player.hand) >= 5
641 cards = player.play_cards(
642 self.world, self.log, error, play_required=cards_required)
643 if cards_required == True and cards == None:
644 raise PlayerError('You have %d >= 5 cards in your hand, you must play'
646 c_prod,c_terr_prod = self.deck.production(player, cards)
648 except PlayerError, error:
650 w_prod,w_terr_prod = self.world.production(player)
651 self.log(EarnsArmies(player, w_prod, w_terr_prod))
654 player.hand.remove(c)
655 self.log(PlayCards(player, cards, c_prod, c_terr_prod))
656 for terr,prod in c_terr_prod.items():
657 if terr in w_terr_prod:
658 w_terr_prod[terr] += prod
660 w_terr_prod[terr] = prod
661 self.world.place_territory_production(w_terr_prod)
662 armies = w_prod + c_prod
663 self.player_place_armies(player, armies, armies)
664 def attack_and_fortify(self, player):
670 actions = player.attack_and_fortify(self.world, self.log, error, mode)
671 for action in actions:
677 assert mode == 'fortify', mode
679 source_name,target_name,armies = action
681 source = self.world.territory_by_name(source_name)
683 raise PlayerError('Invalid territory "%s"' % source_name)
685 target = self.world.territory_by_name(target_name)
687 raise PlayerError('Invalid territory "%s"' % targer_name)
689 tplayer = target.player
690 capture = self.attack(source, target, armies)
693 if len(list(tplayer.territories(self.world))) == 0:
694 self.player_killed(tplayer, killer=player)
696 assert mode == 'fortify', mode
697 self.fortify(source, target, armies)
698 return captures # only allow one fortification
699 except PlayerError, error:
701 def attack(self, source, target, armies):
702 if source.player == target.player:
703 raise PlayerError('%s attacking %s, but you own both.'
706 raise PlayerError('%s attacking %s with 0 armies.'
708 if armies >= source.armies:
709 raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
710 % (source, target, armies, source.armies-1))
711 if not source.borders(target):
712 raise PlayerError('Cannot reach %s from %s to attack'
714 s_dice = sorted([random.randint(1, 6) for i in range(armies)],
716 t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
720 for a,d in zip(s_dice, t_dice):
725 source.armies -= s_dead
726 target.armies -= t_dead
727 if target.armies == 0:
728 self.takeover(source, target, remaining_attackers=armies-s_dead)
729 self.log(Conquer(source, target, s_dice, t_dice, s_dead, t_dead))
730 assert target.armies > 0, target
732 self.log(Attack(source, target, s_dice, t_dice, s_dead, t_dead))
733 assert target.armies > 0, target
735 def takeover(self, source, target, remaining_attackers):
736 source.armies -= remaining_attackers
737 target.armies += remaining_attackers
738 target.player = source.player
739 if source.armies > 1:
743 support = source.player.support_attack(
744 self.world, self.log, error, source, target)
745 if support < 0 or support >= source.armies:
747 'Cannot support from %s to %s with %d armies, only %d available'
748 % (source, target, support, source.armies-1))
750 except PlayerError, error:
752 source.armies -= support
753 target.armies += support
754 def fortify(self, source, target, armies):
755 if source.player != target.player:
756 raise PlayerError('%s (%s) cannot fortifiy %s (%s).'
757 % (source, source.player, target, target.player))
760 if armies >= source.armies:
761 raise PlayerError('%s fortifying %s with %d armies, but only %d are available.'
762 % (source, target, armies, source.armies-1))
763 if not source.borders(target):
764 raise PlayerError('Cannot reach %s from %s to fortify'
766 source.armies -= armies
767 target.armies += armies
768 self.log(Fortify(source, target, armies))
769 def player_killed(self, player, killer):
771 killer.hand.extend(player.hand)
772 if len(self.living_players()) > 1:
773 while len(killer.hand) > 5:
774 self.play_cards_and_place_armies(killer)
775 self.log(Killed(player, killer))
776 if len(self.living_players()) > 1:
777 player.report(self.world, self.log)
778 # else the game is over, and killed will hear about this then.
779 def end_of_turn_cards(self, player, captures):
780 """Deal end-of-turn reward for any territory captures.
782 if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
784 def living_players(self):
785 return [p for p in self.players if p.alive == True]
788 def generate_earth():
790 c = Continent('North America', 5)
791 c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))
792 c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
793 c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
794 c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
795 c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
796 c.append(Territory('Quebec', 'que', 3, ['eus']))
797 c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
798 c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
799 c.append(Territory('Central America', 'cam', 3, ['ven']))
802 c = Continent('Europe', 5)
803 c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
804 c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
805 c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
806 c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
807 c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
808 c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
809 c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
812 c = Continent('Asia', 7)
813 c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
814 c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
815 c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
816 c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
817 c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
818 c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
819 c.append(Territory('Japan', 'jap', 2))
820 c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
821 c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
822 c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
823 c.append(Territory('India', 'indi', 3, ['sia']))
824 c.append(Territory('Siam', 'sia', 1, ['indo']))
828 c = Continent('South America', 2)
829 c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
830 c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
831 c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
832 c.append(Territory('Argentina', 'arg', 2))
835 c = Continent('Africa', 3)
836 c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
837 c.append(Territory('Egypt', 'egy', 1, ['eaf']))
838 c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
839 c.append(Territory('Congo', 'con', 3, ['saf']))
840 c.append(Territory('South Africa', 'saf', 1, ['mad']))
841 c.append(Territory('Madagascar', 'mad', 2))
844 c = Continent('Australia', 2)
845 c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
846 c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
847 c.append(Territory('Western Australia', 'wau', 2, ['eau']))
848 c.append(Territory('Eastern Australia', 'eau', 3))
851 w._resolve_link_names()
856 failures,tests = doctest.testmod(sys.modules[__name__])
860 world = generate_earth()
861 players = [Player('Alice'), Player('Bob'), Player('Charlie')]
862 e = Engine(world, players)
865 if __name__ == '__main__':
867 failures = self.test()