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