Update OutgoingEmailDispatcher execute calls -> self._execute
[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.log('Game started with %s' % self.players)
510         self.deck.shuffle()
511         self.select_territories()
512         self.place_initial_armies()
513         for p in self.players:
514             self.deal(p, 3)
515     def play(self):
516         """Main gameplay phase.  Take turns until only one Player survives.
517         """
518         turn = 0
519         active_player = 0
520         living = len(self.living_players())
521         while living > 1:
522             self.play_turn(self.players[active_player])
523             living = len(self.living_players())
524             active_player = (active_player + 1) % len(self.players)
525             if living > 1:
526                 while self.players[active_player].alive == False:
527                     active_player = (active_player + 1) % len(self.players)
528             turn += 1
529     def game_over(self):
530         """The end of the game.
531
532         Currently just a notification hook.
533         """
534         self.log('Game over.')
535         for p in self.players:
536             p.report(self.world, self.log)
537     def play_turn(self, player):
538         """Work through the phases of player's turn.
539         """
540         self.log("%s's turn (territory score: %s)"
541                  % (player, [(p,len(list(p.territories(self.world))))
542                              for p in self.players]))
543         self.play_cards_and_place_armies(player)
544         captures = self.attack_and_fortify(player)
545         self.end_of_turn_cards(player, captures)
546     def select_territories(self):
547         for t in self.world.territories():
548             t.player = None
549         for i in range(len(list(self.world.territories()))):
550             p = self.players[i % len(self.players)]
551             t_name = p.select_territory(self.world, self.log)
552             t = self.world.territory_by_name(t_name)
553             if t.player != None:
554                 raise PlayerError('Cannot select %s owned by %s'
555                                   % (t, t.player))
556             self.log('%s selects %s' % (p, t))
557             t.player = p
558             t.armies = 1
559     def place_initial_armies(self):
560         already_placed = [len(list(p.territories(self.world))) for p in self.players]
561         s = list(set(already_placed))
562         assert len(s) in [1,2], already_placed
563         if len(s) == 2: # catch up the players who are one territory short
564             assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
565             for p,placed in zip(self.players, already_placed):
566                 if placed == min(s):
567                     self.player_place_armies(p, remaining, 1)
568         remaining = self.world.initial_armies[len(self.players)] - max(s)
569         while remaining > 0:
570             for p in self.players:
571                 self.player_place_armies(p, remaining, 1)
572             remaining -= 1
573     def player_place_armies(self, player, remaining=1, this_round=1):
574         placements = player.place_armies(self.world, self.log, remaining, this_round)
575         if sum(placements.values()) != this_round:
576             raise PlayerError('Placing more than %d armies' % this_round)
577         for ter_name,armies in placements.items():
578             t = self.world.territory_by_name(ter_name)
579             if t.player != player:
580                 raise PlayerError('Placing armies in %s owned by %s'
581                                   % (t, t.player))
582             if armies < 0:
583                 raise PlayerError('Placing a negative number of armies (%d) in %s'
584                                   % (armies, t))
585         self.log('%s places %s' % (player, placements))
586         for terr_name,armies in placements.items():
587             t = self.world.territory_by_name(terr_name)
588             t.armies += armies
589     def deal(self, player, number):
590         cards = []
591         for i in range(number):
592             cards.append(self.deck.pop())
593         player.hand.extend(cards)
594         player.draw(self.world, self.log, cards)
595         self.log('%s dealt %d cards' % (player, number))
596     def play_cards_and_place_armies(self, player, additional_armies=0):
597         cards_required = len(player.hand) >= 5
598         cards = player.play_cards(
599             self.world, self.log, play_required=cards_required)
600         if cards_required == True and cards == None:
601             raise PlayerError('You have %d >= 5 cards in your hand, you must play'
602                               % len(player.hand))
603         w_prod,w_terr_prod = self.world.production(player)
604         self.log('%s earned %d armies from territories' % (player, w_prod))
605         c_prod,c_terr_prod = self.deck.production(player, cards)
606         if c_prod > 0:
607             self.log('%s played %s, earning %d armies'
608                      % (player, cards, c_prod+sum(c_terr_prod.values())))
609         if cards != None:
610             for c in cards:
611                 player.hand.remove(c)
612         for terr,prod in c_terr_prod.items():
613             if terr in w_terr_prod:
614                 w_terr_prod[terr] += prod
615             else:
616                 w_terr_prod[terr] = prod
617         self.world.place_territory_production(w_terr_prod)
618         if len(w_terr_prod) > 0:
619             self.log('%s was required to place %s' % (player, w_terr_prod))
620         armies = w_prod + c_prod
621         self.player_place_armies(player, armies, armies)
622     def attack_and_fortify(self, player):
623         captures = 0
624         mode = 'attack'
625         while True:
626             actions = player.attack_and_fortify(self.world, self.log, mode)
627             for action in actions:
628                 if action == None:
629                     if mode == 'attack':
630                         mode = 'fortify'
631                         continue
632                     else:
633                         assert mode == 'fortify', mode
634                         return captures
635                 source_name,target_name,armies = action
636                 source = self.world.territory_by_name(source_name)
637                 target = self.world.territory_by_name(target_name)
638                 if not source.borders(target):
639                     raise PlayerError('Cannot reach %s from %s to %s'
640                                       % (target, source, mode))
641                 if mode == 'attack':
642                     tplayer = target.player
643                     capture = self.attack(source, target, armies)
644                     if capture == True:
645                         captures += 1
646                         if len(list(tplayer.territories(self.world))) == 0:
647                             self.player_killed(tplayer, killer=player)
648                 else:
649                     assert mode == 'fortify', mode
650                     self.fortify(source, target, armies)
651     def attack(self, source, target, armies):
652         if source.player == target.player:
653             raise PlayerError('%s attacking %s, but you own both.'
654                               % (source, target))
655         if armies == 0:
656             raise PlayerError('%s attacking %s with 0 armies.'
657                               % (source, target))
658         if armies >= source.armies:
659             raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
660                               % (source, target, armies, source.armies-1))
661         a_dice = sorted([random.randint(1, 6) for i in range(armies)],
662                         reverse=True)
663         t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
664                         reverse=True)
665         a_dead = 0
666         t_dead = 0
667         for a,d in zip(a_dice, t_dice):
668             if d >= a:
669                 a_dead += 1
670             else:
671                 t_dead += 1
672         source.armies -= a_dead
673         target.armies -= t_dead
674         if target.armies == 0:
675             self.takeover(source, target, remaining_attackers=armies-a_dead)
676             self.log('%s conquered %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d'
677                      % (source.player, target, source, armies, len(t_dice),
678                         a_dead, t_dead, source.armies, target.armies))
679             assert target.armies > 0, target
680             return True
681         self.log('%s attacked %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d' \
682                      % (source.player, target, source, armies, len(t_dice),
683                         a_dead, t_dead, source.armies, target.armies))
684         assert target.armies > 0, target
685         return False
686     def takeover(self, source, target, remaining_attackers):
687         source.armies -= remaining_attackers
688         target.armies += remaining_attackers
689         target.player = source.player
690         if source.armies > 1:
691             support = source.player.support_attack(
692                 self.world, self.log, source, target)
693             if support < 0 or support >= source.armies:
694                 raise PlayerError(
695                     'Cannot support from %s to %s with %d armies, only %d available'
696                     % (source, target, support, source.armies-1))
697             source.armies -= support
698             target.armies += support
699     def player_killed(self, player, killer):
700         player.alive = False
701         killer.hand.extend(player.hand)
702         if len(self.living_players()) > 1:
703             while len(killer.hand) > 5:
704                 self.play_cards_and_place_armies(killer)
705         self.log('%s killed by %s' % (player, killer))
706         if len(self.living_players()) > 1:
707             player.report(self.world, self.log)
708             # else the game is over, and killed will hear about this then.
709     def end_of_turn_cards(self, player, captures):
710         """Deal end-of-turn reward for any territory captures.
711         """
712         if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
713             self.deal(player, 1)
714     def living_players(self):
715         return [p for p in self.players if p.alive == True]
716
717
718 def generate_earth():
719     w = World('Earth')
720     c = Continent('North America', 5)
721     c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))    
722     c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
723     c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
724     c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
725     c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
726     c.append(Territory('Quebec', 'que', 3, ['eus']))
727     c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
728     c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
729     c.append(Territory('Central America', 'cam', 3, ['ven']))
730     w.append(c)
731     
732     c = Continent('Europe', 5)
733     c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
734     c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
735     c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
736     c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
737     c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
738     c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
739     c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
740     w.append(c)
741
742     c = Continent('Asia', 7)
743     c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
744     c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
745     c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
746     c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
747     c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
748     c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
749     c.append(Territory('Japan', 'jap', 2))
750     c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
751     c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
752     c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
753     c.append(Territory('India', 'indi', 3, ['sia']))
754     c.append(Territory('Siam', 'sia', 1, ['indo']))
755
756     w.append(c)
757
758     c = Continent('South America', 2)
759     c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
760     c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
761     c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
762     c.append(Territory('Argentina', 'arg', 2))
763     w.append(c)
764
765     c = Continent('Africa', 3)
766     c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
767     c.append(Territory('Egypt', 'egy', 1, ['eaf']))
768     c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
769     c.append(Territory('Congo', 'con', 3, ['saf']))
770     c.append(Territory('South Africa', 'saf', 1, ['mad']))
771     c.append(Territory('Madagascar', 'mad', 2))
772     w.append(c)
773
774     c = Continent('Australia', 2)
775     c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
776     c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
777     c.append(Territory('Western Australia', 'wau', 2, ['eau']))
778     c.append(Territory('Eastern Australia', 'eau', 3))
779     w.append(c)
780
781     w._resolve_link_names()
782     return w
783
784 def test():
785     import doctest, sys
786     failures,tests = doctest.testmod(sys.modules[__name__])
787     return failures
788
789 def random_game():
790     from player.email import IncomingEmailDispatcher, OutgoingEmailDispatcher, EmailPlayer
791     world = generate_earth()
792     ied = IncomingEmailDispatcher(fifo_path='/tmp/pyrisk.in')
793     oed = OutgoingEmailDispatcher(return_address='server@example.com')
794     players = [EmailPlayer('Alice', 'alice@big.edu', ied, oed),
795                Player('Bob'), Player('Charlie')]
796     e = Engine(world, players)
797     e.run()
798     ied.close()
799
800 if __name__ == '__main__':
801     import sys
802     failures = self.test()
803     if failures > 0:
804         sys.exit(1)
805     self.random_game()