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