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