6f62c78aa471ffa14233fb2d5ba2b69a03bb3bcf
[pyrisk.git] / risk / base.py
1 """A Python engine for Risk-like games
2 """
3
4 import random
5
6 from .log import Logger
7
8 VERSION='0.1'
9
10
11 class PlayerError (Exception):
12     pass
13
14 class NameMixin (object):
15     """Simple mixin for pretty-printing named objects.
16     """
17     def __init__(self, name):
18         self.name = name
19     def __str__(self):
20         return self.name
21     def __repr__(self):
22         return self.__str__()
23
24
25 class ID_CmpMixin (object):
26     """Simple mixin to ensure the fancier comparisons are all based on
27     __cmp__().
28     """
29     def __cmp__(self, other):
30         return cmp(id(self), id(other))
31     def __eq__(self, other):
32         return self.__cmp__(other) == 0
33     def __ne__(self, other):
34         return self.__cmp__(other) != 0
35
36 class Territory (NameMixin, ID_CmpMixin, list):
37     """An occupiable territory.
38
39     Contains a list of neighboring territories.
40     """
41     def __init__(self, name, short_name=None, type=-1,
42                  link_names=[], continent=None, player=None):
43         NameMixin.__init__(self, name)
44         ID_CmpMixin.__init__(self)
45         list.__init__(self)
46         self.short_name = short_name
47         if short_name == None:
48             self.short_name = name
49         self._card_type = type     # for Deck construction
50         self._link_names = list(link_names) # used by World._resolve_link_names
51         self.continent = continent # used by World.production
52         self.player = player       # who owns this territory
53         self.armies = 0            # number of occupying armies
54     def __str__(self):
55         if self.short_name == self.name:
56             return self.name
57         return '%s (%s)' % (self.name, self.short_name)
58     def borders(self, other):
59         for t in self:
60             if id(t) == id(other):
61                 return True
62         return False
63
64 class Continent (NameMixin, ID_CmpMixin, list):
65     """A group of Territories.
66
67     Stores the army-production bonus if it's owned by a single player.
68     """
69     def __init__(self, name, production, territories=[]):
70         NameMixin.__init__(self, name)
71         ID_CmpMixin.__init__(self)
72         list.__init__(self, territories)
73         self.production = production
74     def append(self, territory):
75         """Add a new territory (setting the territory's .continent
76         attribute).
77         """
78         list.append(self, territory)
79         territory.continent = self
80     def territory_by_name(self, name):
81         """Find a Territory instance by name (long or short, case
82         insensitive).
83         """
84         for t in self:
85             if name.lower() in [t.short_name.lower(), t.name.lower()]:
86                 return t
87         raise KeyError(name)
88     def single_player(self):
89         """Is the continent owned by a single player?
90         """
91         p = self[0].player
92         for territory in self:
93             if territory.player != p:
94                 return False
95         return True
96
97 class World (NameMixin, ID_CmpMixin, list):
98     """Store the world map and current world state.
99
100     Holds list of Continents.  Also controls territory-based army
101     production (via production).
102     """
103     def __init__(self, name, continents=[]):
104         NameMixin.__init__(self, name)
105         ID_CmpMixin.__init__(self)
106         list.__init__(self, continents)
107         self.initial_armies = { # num_players:num_armies
108             2: 40, 3:35, 4:30, 5:25, 6:20
109                 }
110     def territories(self):
111         """Iterate through all the territories in the world.
112         """
113         for continent in self:
114             for territory in continent:
115                 yield territory
116     def territory_by_name(self, name):
117         """Find a Territory instance by name (long or short, case
118         insensitive).
119         """
120         for continent in self:
121             try:
122                 return continent.territory_by_name(name)
123             except KeyError:
124                 pass
125         raise KeyError(name)
126     def continent_by_name(self, name):
127         """Find a Continent instance by name (case insensitive).
128         """
129         for continent in self:
130             if continent.name.lower() == name.lower():
131                 return continent
132         raise KeyError(name)
133     def _resolve_link_names(self):
134         """Initialize Territory links.
135
136         The Territory class doesn't actually link to neighbors after
137         initialization, but one of each linked pair has the others
138         name in _link_names.  This method goes through the territories,
139         looks up the referenced link target, and joins the pair.
140         """
141         self._check_short_names()
142         for territory in self.territories():
143             for name in territory._link_names:
144                 other = self.territory_by_name(name)
145                 if not territory.borders(other):
146                     territory.append(other)
147                 if not other.borders(territory):
148                     other.append(territory)
149     def _check_short_names(self):
150         """Ensure there are no short_name collisions.
151         """
152         ts = {}
153         for t in self.territories():
154             if t.short_name.lower() not in ts:
155                 ts[t.short_name.lower()] = t
156             else:
157                 raise ValueError('%s shared by %s and %s'
158                     % (t.short_name.lower(), ts[t.short_name.lower()], t))
159     def production(self, player):
160         """Calculate the number of armies a player should earn based
161         on territory occupation.
162         """
163         ts = list(player.territories(self))
164         production = max(3, len(ts) / 3)
165         continents = set([t.continent.name for t in ts])
166         for c_name in continents:
167             c = self.continent_by_name(c_name)
168             if c.single_player() == True:
169                 production += c.production
170         return (production, {})
171     def place_territory_production(self, territory_production):
172         """Place armies based on {territory_name: num_armies, ...}.
173         """
174         for territory_name,production in territory_production.items():
175             t = self.territory_by_name(territory_name)
176             t.armies += production
177
178 class Card (ID_CmpMixin):
179     """Represent a territory card (or wild)
180
181     Nothing exciting going on here, just a class for pretty-printing
182     card names.
183     """
184     def __init__(self, deck, type_, territory=None):
185         ID_CmpMixin.__init__(self)
186         self.deck = deck
187         self.territory = territory
188         self.type = type_
189     def __str__(self):
190         if self.territory == None:
191             return '<Card %s>' % (self.deck.type_names[self.type])
192
193         return '<Card %s %s>' % (self.territory,
194                                  self.deck.type_names[self.type])
195     def __repr__(self):
196         return self.__str__()
197
198 class Deck (list):
199     """All the cards yet to be handed out in a given game.
200
201     Controls the type branding (via type_names) and army production
202     values for scoring sets (via production_value).
203     """
204     def __init__(self, territories=[], num_wilds=2,
205                  type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
206         list.__init__(self, [Card(self, t._card_type, t) for t in territories])
207         self.type_names = type_names
208         for i in range(num_wilds):
209             self.append(Card(self, 0))
210         self._production_sequence = [4, 6, 8, 10, 12, 15]
211         self._production_index = 0
212     def shuffle(self):
213         """Shuffle the remaining cards in the deck.
214         """
215         random.shuffle(self)
216     def production_value(self, index):
217         """
218         >>> d = Deck()
219         >>> [d.production_value(i) for i in range(8)]
220         [4, 6, 8, 10, 12, 15, 20, 25]
221         """
222         if index < len(self._production_sequence):
223             return self._production_sequence[index]
224         extra = index - len(self._production_sequence) + 1
225         return self._production_sequence[-1] + 5 * extra
226     def production(self, player, cards=None):
227         """
228         >>> d = Deck()
229         >>> a = Player('Alice')
230         >>> b = Player('Bob')
231         >>> d.production(a, None)
232         (0, {})
233         >>> d.production(a, [Card(d, 1, Territory('a')),
234         ...                  Card(d, 1, Territory('b'))])
235         Traceback (most recent call last):
236           ...
237         PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
238         >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
239         ...                  Card(d, 1, Territory('b', player=b)),
240         ...                  Card(d, 1, Territory('c'))])
241         (4, {'a': 1})
242         >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
243         ...                     Card(d, 2, Territory('b', player=a)),
244         ...                     Card(d, 0, Territory('c', player=a))])
245         >>> p
246         6
247         >>> sorted(tp.items())
248         [('a', 1), ('b', 1), ('c', 1)]
249         """
250         if cards == None:
251             return (0, {})
252         h = Hand(cards)
253         if h.scores():
254             p = self.production_value(self._production_index)
255             self._production_index += 1
256             territory_production = {}
257             for c in cards:
258                 if c.territory != None and c.territory.player == player:
259                     territory_production[c.territory.name] = 1
260             return (p, territory_production)
261         raise PlayerError('%s is not a scoring set' % h)
262
263 class Hand (list):
264     """Represent a hand of cards.
265
266     This is the place to override the set of allowed scoring
267     combinations.  You should override one of
268
269     * set
270     * run
271     * scores
272
273     Adding additional scoring methods as needed (e.g. flush).
274     """
275     def __init__(self, cards=[]):
276         list.__init__(self, cards)
277     def set(self):
278         if len(self) != 3:
279             return False
280         s = sorted(set([card.type for card in self]))
281         if len(s) == 1 \
282                 or (len(s) == 2 and s[0] == 0):
283             return True
284         return False
285     def run(self):
286         if len(self) != 3:
287             return False
288         if len(set([card.type for card in self])) == 3:
289             return True
290         return False
291     def scores(self):
292         """The hand is any valid scoring combination.
293         """
294         return self.set() or self.run()
295     def subhands(self, lengths=None):
296         """Return all possible subhands.
297
298         Lengths can either be a list of allowed subhand lengths or
299         None.  If None, all possible subhand lengths are allowed.
300
301         >>> d = Deck()
302         >>> h = Hand([Card(d, 1, Territory('a')),
303         ...           Card(d, 1, Territory('b')),
304         ...           Card(d, 1, Territory('c')),
305         ...           Card(d, 1, Territory('d'))])
306         >>> for hand in h.subhands():
307         ...     print hand
308         [<Card a Infantry>]
309         [<Card b Infantry>]
310         [<Card c Infantry>]
311         [<Card d Infantry>]
312         [<Card a Infantry>, <Card b Infantry>]
313         [<Card a Infantry>, <Card c Infantry>]
314         [<Card a Infantry>, <Card d Infantry>]
315         [<Card b Infantry>, <Card c Infantry>]
316         [<Card b Infantry>, <Card d Infantry>]
317         [<Card c Infantry>, <Card d Infantry>]
318         [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
319         [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
320         [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
321         [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
322         [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
323         """
324         for i in range(len(self)):
325             i += 1 # check all sub-hands of length i
326             if lengths != None and i not in lengths:
327                 continue # don't check this length
328             indices = range(i)
329             stop = range(len(self)-i, len(self))
330             while indices != stop:
331                 yield Hand([self[i] for i in indices])
332                 indices = self._increment(indices, stop)
333             yield Hand([self[i] for i in indices])
334     def _increment(self, indices, stop):
335         """
336         >>> d = Deck()
337         >>> h = Hand([Card(d, 1, Territory('a'))])
338         >>> h._increment([0, 1, 2], [2, 3, 4])
339         [0, 1, 3]
340         >>> h._increment([0, 1, 3], [2, 3, 4])
341         [0, 1, 4]
342         >>> h._increment([0, 1, 4], [2, 3, 4])
343         [0, 2, 3]
344         """
345         moveable = [i for i,m in zip(indices, stop) if i < m]
346         assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
347         key = indices.index(moveable[-1])
348         new = indices[key] + 1
349         for i in range(key, len(indices)):
350             indices[i] = new + i-key
351         return indices
352     def possible(self):
353         """Return a list of all possible scoring subhands.
354         """
355         for h in self.subhands():
356             if h.scores():
357                 yield h
358
359 class Player (NameMixin, ID_CmpMixin):
360     """Represent a risk player.
361
362     This class implements a very basic AI player.  Subclasses should
363     consider overriding the "action-required" methods:
364
365     * select_territory
366     * play_cards
367     * place_armies
368     * attack_and_fortify
369     * support_attack
370
371     And the "report" methods:
372     
373     * report
374     * draw
375     """
376     def __init__(self, name):
377         NameMixin.__init__(self, name)
378         ID_CmpMixin.__init__(self)
379         self.alive = True
380         self.hand = Hand()
381         self._message_index = 0
382     def territories(self, world):
383         """Iterate through all territories owned by this player.
384         """
385         for t in world.territories():
386             if t.player == self:
387                 yield t
388     def border_territories(self, world):
389         """Iterate through all territories owned by this player which
390         border another player's territories.
391         """
392         for t in self.territories(world):
393             for neighbor in t:
394                 if neighbor.player != self:
395                     yield t
396                     break
397     def report(self, world, log):
398         """Send reports about death and game endings.
399
400         These events mark the end of contact and require no change in
401         player status or response, so they get a special command
402         seperate from the usual action family.  The action commands in
403         Player subclasses can notify the player (possibly by calling
404         report internally) if they feel so inclined.
405         
406         See also
407         --------
408         draw - another notification-only method
409         """
410         print 'Reporting for %s:\n  %s' \
411             % (self, '\n  '.join(log[self._message_index:]))
412         self._message_index = len(log)
413     def draw(self, world, log, cards=[]):
414         """Only called if you earned a new card (or cards).
415
416         See also
417         --------
418         report - another notification-only method
419         """
420         pass
421     def select_territory(self, world, log):
422         """Return the selected territory
423         """
424         free_territories = [t for t in world.territories() if t.player == None]
425         return random.sample(free_territories, 1)[0].name
426     def play_cards(self, world, log, play_required=True):
427         """Decide whether or not to turn in a set of cards.
428
429         Return a list of cards to turn in or None.  If play_required
430         is True, you *must* play.
431         """
432         if play_required == True:
433             return random.sample(list(self.hand.possible()), 1)[0]
434     def place_armies(self, world, log, remaining=1, this_round=1):
435         """Both during setup and before each turn.
436
437         Return {territory_name: num_armies, ...}
438         """
439         t = random.sample(list(self.border_territories(world)), 1)[0]
440         return {t.name: this_round}
441     def attack_and_fortify(self, world, log, mode='attack'):
442         """Return list of (source, target, armies) tuples.  Place None
443         in the list to end this phase.
444         """
445         assert mode != 'fortify', mode
446         possible_attacks = []
447         for t in self.border_territories(world):
448             if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
449                 continue
450             targets = [border_t for border_t in t if border_t.player != self]
451             for tg in targets:
452                 possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
453         if len(possible_attacks) == 0:
454             return [None, None] # stop attack phase, then stop fortification phase
455         return random.sample(possible_attacks, 1) # + [None]
456     def support_attack(self, world, log, source, target):
457         """Follow up on a conquest by moving additional armies.
458         """
459         return source.armies-1
460
461 class Engine (ID_CmpMixin):
462     """Drive the game.
463
464     Basic usage will be along the lines of
465
466     >>> world = generate_earth()
467     >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
468     >>> e = Engine(world, players)
469     >>> e.run() # doctest: +ELLIPSIS
470     ...
471     """
472     def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
473         ID_CmpMixin.__init__(self)
474         self.world = world
475         self.deck = deck_class(world.territories())
476         self.log = logger_class()
477         self.players = players
478     def __str__(self):
479         return '<engine %s %s>' % (self.world, self.players)
480     def __repr__(self):
481         return self.__str__()
482     def run(self):
483         """The main entry point.
484         """
485         self.setup()
486         self.play()
487         self.game_over()
488     def setup(self):
489         """Setup phase.  Pick territories, place initial armies, and
490         deal initial hands.
491         """
492         for p in self.players:
493             p.alive = True
494         random.shuffle(self.players)
495         self.deck.shuffle()
496         self.select_territories()
497         self.place_initial_armies()
498         for p in self.players:
499             self.deal(p, 3)
500     def play(self):
501         """Main gameplay phase.  Take turns until only one Player survives.
502         """
503         turn = 0
504         active_player = 0
505         living = len(self.living_players())
506         while living > 1:
507             self.play_turn(self.players[active_player])
508             living = len(self.living_players())
509             active_player = (active_player + 1) % len(self.players)
510             if living > 1:
511                 while self.players[active_player].alive == False:
512                     active_player = (active_player + 1) % len(self.players)
513             turn += 1
514     def game_over(self):
515         """The end of the game.
516
517         Currently just a notification hook.
518         """
519         self.log('Game over.')
520         for p in self.players:
521             p.report(self.world, self.log)
522     def play_turn(self, player):
523         """Work through the phases of player's turn.
524         """
525         self.log("%s's turn (territory score: %s)"
526                  % (player, [(p,len(list(p.territories(self.world))))
527                              for p in self.players]))
528         self.play_cards_and_place_armies(player)
529         captures = self.attack_and_fortify(player)
530         self.end_of_turn_cards(player, captures)
531     def select_territories(self):
532         for t in self.world.territories():
533             t.player = None
534         for i in range(len(list(self.world.territories()))):
535             p = self.players[i % len(self.players)]
536             t_name = p.select_territory(self.world, self.log)
537             t = self.world.territory_by_name(t_name)
538             if t.player != None:
539                 raise PlayerError('Cannot select %s owned by %s'
540                                   % (t, t.player))
541             self.log('%s selects %s' % (p, t))
542             t.player = p
543             t.armies = 1
544     def place_initial_armies(self):
545         already_placed = [len(list(p.territories(self.world))) for p in self.players]
546         s = list(set(already_placed))
547         assert len(s) in [1,2], already_placed
548         if len(s) == 2: # catch up the players who are one territory short
549             assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
550             for p,placed in zip(self.players, already_placed):
551                 if placed == min(s):
552                     self.player_place_armies(p, remaining, 1)
553         remaining = self.world.initial_armies[len(self.players)] - max(s)
554         while remaining > 0:
555             for p in self.players:
556                 self.player_place_armies(p, remaining, 1)
557             remaining -= 1
558     def player_place_armies(self, player, remaining=1, this_round=1):
559         placements = player.place_armies(self.world, self.log, remaining, this_round)
560         if sum(placements.values()) != this_round:
561             raise PlayerError('Placing more than %d armies' % this_round)
562         for ter_name,armies in placements.items():
563             t = self.world.territory_by_name(ter_name)
564             if t.player != player:
565                 raise PlayerError('Placing armies in %s owned by %s'
566                                   % (t, t.player))
567             if armies < 0:
568                 raise PlayerError('Placing a negative number of armies (%d) in %s'
569                                   % (armies, t))
570         self.log('%s places %s' % (player, placements))
571         for terr_name,armies in placements.items():
572             t = self.world.territory_by_name(terr_name)
573             t.armies += armies
574     def deal(self, player, number):
575         cards = []
576         for i in range(number):
577             cards.append(self.deck.pop())
578         player.hand.extend(cards)
579         player.draw(self.world, self.log, cards)
580         self.log('%s dealt %d cards' % (player, number))
581     def play_cards_and_place_armies(self, player, additional_armies=0):
582         cards_required = len(player.hand) >= 5
583         cards = player.play_cards(
584             self.world, self.log, play_required=cards_required)
585         if cards_required == True and cards == None:
586             raise PlayerError('You have %d >= 5 cards in your hand, you must play'
587                               % len(player.hand))
588         w_prod,w_terr_prod = self.world.production(player)
589         self.log('%s earned %d armies from territories' % (player, w_prod))
590         c_prod,c_terr_prod = self.deck.production(player, cards)
591         if c_prod > 0:
592             self.log('%s played %s, earning %d armies'
593                      % (player, cards, c_prod+sum(c_terr_prod.values())))
594         if cards != None:
595             for c in cards:
596                 player.hand.remove(c)
597         for terr,prod in c_terr_prod.items():
598             if terr in w_terr_prod:
599                 w_terr_prod[terr] += prod
600             else:
601                 w_terr_prod[terr] = prod
602         self.world.place_territory_production(w_terr_prod)
603         if len(w_terr_prod) > 0:
604             self.log('%s was required to place %s' % (player, w_terr_prod))
605         armies = w_prod + c_prod
606         self.player_place_armies(player, armies, armies)
607     def attack_and_fortify(self, player):
608         captures = 0
609         mode = 'attack'
610         while True:
611             actions = player.attack_and_fortify(self.world, self.log, mode)
612             for action in actions:
613                 if action == None:
614                     if mode == 'attack':
615                         mode = 'fortify'
616                         continue
617                     else:
618                         assert mode == 'fortify', mode
619                         return captures
620                 source_name,target_name,armies = action
621                 source = self.world.territory_by_name(source_name)
622                 target = self.world.territory_by_name(target_name)
623                 if not source.borders(target):
624                     raise PlayerError('Cannot reach %s from %s to %s'
625                                       % (target, source, mode))
626                 if mode == 'attack':
627                     tplayer = target.player
628                     capture = self.attack(source, target, armies)
629                     if capture == True:
630                         captures += 1
631                         if len(list(tplayer.territories(self.world))) == 0:
632                             self.player_killed(tplayer, killer=player)
633                 else:
634                     assert mode == 'fortify', mode
635                     self.fortify(source, target, armies)
636     def attack(self, source, target, armies):
637         if source.player == target.player:
638             raise PlayerError('%s attacking %s, but you own both.'
639                               % (source, target))
640         if armies == 0:
641             raise PlayerError('%s attacking %s with 0 armies.'
642                               % (source, target))
643         if armies >= source.armies:
644             raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
645                               % (source, target, armies, source.armies-1))
646         a_dice = sorted([random.randint(1, 6) for i in range(armies)],
647                         reverse=True)
648         t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
649                         reverse=True)
650         a_dead = 0
651         t_dead = 0
652         for a,d in zip(a_dice, t_dice):
653             if d >= a:
654                 a_dead += 1
655             else:
656                 t_dead += 1
657         source.armies -= a_dead
658         target.armies -= t_dead
659         if target.armies == 0:
660             self.takeover(source, target, remaining_attackers=armies-a_dead)
661             self.log('%s conquered %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d'
662                      % (source.player, target, source, armies, len(t_dice),
663                         a_dead, t_dead, source.armies, target.armies))
664             assert target.armies > 0, target
665             return True
666         self.log('%s attacked %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d' \
667                      % (source.player, target, source, armies, len(t_dice),
668                         a_dead, t_dead, source.armies, target.armies))
669         assert target.armies > 0, target
670         return False
671     def takeover(self, source, target, remaining_attackers):
672         source.armies -= remaining_attackers
673         target.armies += remaining_attackers
674         target.player = source.player
675         support = source.player.support_attack(self.world, self.log, source, target)
676         if support < 0 or support >= source.armies:
677             raise PlayerError('Cannot support from %s to %s with %d armies, only %d available'
678                               % (source, target, support, source.armies-1))
679         source.armies -= support
680         target.armies += support
681     def player_killed(self, player, killer):
682         player.alive = False
683         killer.hand.extend(player.hand)
684         if len(self.living_players()) > 1:
685             while len(killer.hand) > 5:
686                 self.play_cards_and_place_armies(killer)
687         self.log('%s killed by %s' % (player, killer))
688         if len(self.living_players()) > 1:
689             player.report(self.world, self.log)
690             # else the game is over, and killed will hear about this then.
691     def end_of_turn_cards(self, player, captures):
692         """Deal end-of-turn reward for any territory captures.
693         """
694         if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
695             self.deal(player, 1)
696     def living_players(self):
697         return [p for p in self.players if p.alive == True]
698
699
700 def generate_earth():
701     w = World('Earth')
702     c = Continent('North America', 5)
703     c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))    
704     c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
705     c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
706     c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
707     c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
708     c.append(Territory('Quebec', 'que', 3, ['eus']))
709     c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
710     c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
711     c.append(Territory('Central America', 'cam', 3, ['ven']))
712     w.append(c)
713     
714     c = Continent('Europe', 5)
715     c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
716     c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
717     c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
718     c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
719     c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
720     c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
721     c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
722     w.append(c)
723
724     c = Continent('Asia', 7)
725     c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
726     c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
727     c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
728     c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
729     c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
730     c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
731     c.append(Territory('Japan', 'jap', 2))
732     c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
733     c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
734     c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
735     c.append(Territory('India', 'indi', 3, ['sia']))
736     c.append(Territory('Siam', 'sia', 1, ['indo']))
737
738     w.append(c)
739
740     c = Continent('South America', 2)
741     c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
742     c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
743     c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
744     c.append(Territory('Argentina', 'arg', 2))
745     w.append(c)
746
747     c = Continent('Africa', 3)
748     c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
749     c.append(Territory('Egypt', 'egy', 1, ['eaf']))
750     c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
751     c.append(Territory('Congo', 'con', 3, ['saf']))
752     c.append(Territory('South Africa', 'saf', 1, ['mad']))
753     c.append(Territory('Madagascar', 'mad', 2))
754     w.append(c)
755
756     c = Continent('Australia', 2)
757     c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
758     c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
759     c.append(Territory('Western Australia', 'wau', 2, ['eau']))
760     c.append(Territory('Eastern Australia', 'eau', 3))
761     w.append(c)
762
763     w._resolve_link_names()
764     return w
765
766 def test():
767     import doctest, sys
768     failures,tests = doctest.testmod(sys.modules[__name__])
769     return failures
770
771 def random_game():
772     world = generate_earth()
773     players = [Player('Alice'), Player('Bob'), Player('Charlie')]
774     e = Engine(world, players)
775     e.run()
776
777 if __name__ == '__main__':
778     import sys
779     failures = self.test()
780     if failures > 0:
781         sys.exit(1)
782     self.random_game()