1cdd30f155f143560ee7f7183f93b5f91f238214
[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, error=None):
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, error=None,
441                    play_required=True):
442         """Decide whether or not to turn in a set of cards.
443
444         Return a list of cards to turn in or None.  If play_required
445         is True, you *must* play.
446         """
447         if play_required == True:
448             return random.sample(list(self.hand.possible()), 1)[0]
449     def place_armies(self, world, log, error=None,
450                      remaining=1, this_round=1):
451         """Both during setup and before each turn.
452
453         Return {territory_name: num_armies, ...}
454         """
455         t = random.sample(list(self.border_territories(world)), 1)[0]
456         return {t.name: this_round}
457     def attack_and_fortify(self, world, log, error=None,
458                            mode='attack'):
459         """Return list of (source, target, armies) tuples.  Place None
460         in the list to end this phase.
461         """
462         assert mode != 'fortify', mode
463         possible_attacks = []
464         for t in self.border_territories(world):
465             if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
466                 continue
467             targets = [border_t for border_t in t if border_t.player != self]
468             for tg in targets:
469                 possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
470         if len(possible_attacks) == 0:
471             return [None, None] # stop attack phase, then stop fortification phase
472         return random.sample(possible_attacks, 1) # + [None]
473     def support_attack(self, world, log, error,
474                        source, target):
475         """Follow up on a conquest by moving additional armies.
476         """
477         return source.armies-1
478
479 class Engine (ID_CmpMixin):
480     """Drive the game.
481
482     Basic usage will be along the lines of
483
484     >>> world = generate_earth()
485     >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
486     >>> e = Engine(world, players)
487     >>> e.run() # doctest: +ELLIPSIS
488     ...
489     """
490     def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
491         ID_CmpMixin.__init__(self)
492         self.world = world
493         self.deck = deck_class(world.territories())
494         self.log = logger_class()
495         self.players = players
496     def __str__(self):
497         return '<engine %s %s>' % (self.world, self.players)
498     def __repr__(self):
499         return self.__str__()
500     def run(self):
501         """The main entry point.
502         """
503         self.setup()
504         self.play()
505         self.game_over()
506     def setup(self):
507         """Setup phase.  Pick territories, place initial armies, and
508         deal initial hands.
509         """
510         for p in self.players:
511             p.alive = True
512         random.shuffle(self.players)
513         self.log('Game started with %s' % self.players)
514         self.deck.shuffle()
515         self.select_territories()
516         self.place_initial_armies()
517         for p in self.players:
518             self.deal(p, 3)
519     def play(self):
520         """Main gameplay phase.  Take turns until only one Player survives.
521         """
522         turn = 0
523         active_player = 0
524         living = len(self.living_players())
525         while living > 1:
526             self.play_turn(self.players[active_player])
527             living = len(self.living_players())
528             active_player = (active_player + 1) % len(self.players)
529             if living > 1:
530                 while self.players[active_player].alive == False:
531                     active_player = (active_player + 1) % len(self.players)
532             turn += 1
533     def game_over(self):
534         """The end of the game.
535
536         Currently just a notification hook.
537         """
538         self.log('Game over.')
539         for p in self.players:
540             p.report(self.world, self.log)
541     def play_turn(self, player):
542         """Work through the phases of player's turn.
543         """
544         self.log("%s's turn (territory score: %s)"
545                  % (player, [(p,len(list(p.territories(self.world))))
546                              for p in self.players]))
547         self.play_cards_and_place_armies(player)
548         captures = self.attack_and_fortify(player)
549         self.end_of_turn_cards(player, captures)
550     def select_territories(self):
551         for t in self.world.territories():
552             t.player = None
553         num_terrs = len(list(self.world.territories()))
554         for i in range(num_terrs-1):
555             p = self.players[i % len(self.players)]
556             error = None
557             while True:
558                 try:
559                     t_name = p.select_territory(self.world, self.log, error)
560                     try:
561                         t = self.world.territory_by_name(t_name)
562                     except KeyError:
563                         raise PlayerError('Invalid territory "%s"' % t_name)
564                     if t.player != None:
565                         raise PlayerError('Cannot select %s owned by %s'
566                                           % (t, t.player))
567                     break
568                 except PlayerError, error:
569                     continue
570             self.log('%s selects %s' % (p, t))
571             t.player = p
572             t.armies = 1
573         # last player has no choice.
574         p = self.players[(num_terrs-1) % len(self.players)]
575         t = [t for t in self.world.territories() if t.player == None][0]
576         t.player = p
577         t.armies = 1
578     def place_initial_armies(self):
579         already_placed = [len(list(p.territories(self.world))) for p in self.players]
580         s = list(set(already_placed))
581         assert len(s) in [1,2], already_placed
582         if len(s) == 2: # catch up the players who are one territory short
583             assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
584             for p,placed in zip(self.players, already_placed):
585                 if placed == min(s):
586                     self.player_place_armies(p, remaining, 1)
587         remaining = self.world.initial_armies[len(self.players)] - max(s)
588         while remaining > 0:
589             for p in self.players:
590                 self.player_place_armies(p, remaining, 1)
591             remaining -= 1
592     def player_place_armies(self, player, remaining=1, this_round=1):
593         error = None
594         while True:
595             try:
596                 placements = player.place_armies(self.world, self.log, error,
597                                                  remaining, this_round)
598                 if sum(placements.values()) != this_round:
599                     raise PlayerError('Placing more than %d armies' % this_round)
600                 for ter_name,armies in placements.items():
601                     try:
602                         t = self.world.territory_by_name(ter_name)
603                     except KeyError:
604                         raise PlayerError('Invalid territory "%s"' % t_name)
605                     if t.player != player:
606                         raise PlayerError('Placing armies in %s owned by %s'
607                                           % (t, t.player))
608                     if armies < 0:
609                         raise PlayerError('Placing a negative number of armies (%d) in %s'
610                                           % (armies, t))
611                 break
612             except PlayerError, error:
613                 continue
614         self.log('%s places %s' % (player, placements))
615         for terr_name,armies in placements.items():
616             t = self.world.territory_by_name(terr_name)
617             t.armies += armies
618     def deal(self, player, number):
619         cards = []
620         for i in range(number):
621             cards.append(self.deck.pop())
622         player.hand.extend(cards)
623         player.draw(self.world, self.log, cards)
624         self.log('%s dealt %d cards' % (player, number))
625     def play_cards_and_place_armies(self, player, additional_armies=0):
626         cards_required = len(player.hand) >= 5
627         error = None
628         while True:
629             try:
630                 cards = player.play_cards(
631                     self.world, self.log, error, play_required=cards_required)
632                 if cards_required == True and cards == None:
633                     raise PlayerError('You have %d >= 5 cards in your hand, you must play'
634                                       % len(player.hand))
635                 c_prod,c_terr_prod = self.deck.production(player, cards)
636                 break
637             except PlayerError, error:
638                 continue
639         w_prod,w_terr_prod = self.world.production(player)
640         self.log('%s earned %d armies from territories' % (player, w_prod))
641         if c_prod > 0:
642             self.log('%s played %s, earning %d armies'
643                      % (player, cards, c_prod+sum(c_terr_prod.values())))
644         if cards != None:
645             for c in cards:
646                 player.hand.remove(c)
647         for terr,prod in c_terr_prod.items():
648             if terr in w_terr_prod:
649                 w_terr_prod[terr] += prod
650             else:
651                 w_terr_prod[terr] = prod
652         self.world.place_territory_production(w_terr_prod)
653         if len(w_terr_prod) > 0:
654             self.log('%s was required to place %s' % (player, w_terr_prod))
655         armies = w_prod + c_prod
656         self.player_place_armies(player, armies, armies)
657     def attack_and_fortify(self, player):
658         captures = 0
659         mode = 'attack'
660         error = None
661         while True:
662             try:
663                 actions = player.attack_and_fortify(self.world, self.log, error, mode)
664                 for action in actions:
665                     if action == None:
666                         if mode == 'attack':
667                             mode = 'fortify'
668                             continue
669                         else:
670                             assert mode == 'fortify', mode
671                             return captures
672                     source_name,target_name,armies = action
673                     try:
674                         source = self.world.territory_by_name(source_name)
675                     except KeyError:
676                         raise PlayerError('Invalid territory "%s"' % source_name)
677                     try:
678                         target = self.world.territory_by_name(target_name)
679                     except KeyError:
680                         raise PlayerError('Invalid territory "%s"' % targer_name)
681                     if not source.borders(target):
682                         raise PlayerError('Cannot reach %s from %s to %s'
683                                           % (target, source, mode))
684                     if mode == 'attack':
685                         tplayer = target.player
686                         capture = self.attack(source, target, armies)
687                         if capture == True:
688                             captures += 1
689                             if len(list(tplayer.territories(self.world))) == 0:
690                                 self.player_killed(tplayer, killer=player)
691                     else:
692                         assert mode == 'fortify', mode
693                         self.fortify(source, target, armies)
694             except PlayerError, error:
695                 continue
696     def attack(self, source, target, armies):
697         if source.player == target.player:
698             raise PlayerError('%s attacking %s, but you own both.'
699                               % (source, target))
700         if armies == 0:
701             raise PlayerError('%s attacking %s with 0 armies.'
702                               % (source, target))
703         if armies >= source.armies:
704             raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
705                               % (source, target, armies, source.armies-1))
706         a_dice = sorted([random.randint(1, 6) for i in range(armies)],
707                         reverse=True)
708         t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
709                         reverse=True)
710         a_dead = 0
711         t_dead = 0
712         for a,d in zip(a_dice, t_dice):
713             if d >= a:
714                 a_dead += 1
715             else:
716                 t_dead += 1
717         source.armies -= a_dead
718         target.armies -= t_dead
719         if target.armies == 0:
720             self.takeover(source, target, remaining_attackers=armies-a_dead)
721             self.log('%s conquered %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d'
722                      % (source.player, target, source, armies, len(t_dice),
723                         a_dead, t_dead, source.armies, target.armies))
724             assert target.armies > 0, target
725             return True
726         self.log('%s attacked %s from %s with %d:%d.  Deaths %d:%d.  Remaining %d:%d' \
727                      % (source.player, target, source, armies, len(t_dice),
728                         a_dead, t_dead, source.armies, target.armies))
729         assert target.armies > 0, target
730         return False
731     def takeover(self, source, target, remaining_attackers):
732         source.armies -= remaining_attackers
733         target.armies += remaining_attackers
734         target.player = source.player
735         if source.armies > 1:
736             error = None
737             while True:
738                 try:
739                     support = source.player.support_attack(
740                         self.world, self.log, error, source, target)
741                     if support < 0 or support >= source.armies:
742                         raise PlayerError(
743                             'Cannot support from %s to %s with %d armies, only %d available'
744                             % (source, target, support, source.armies-1))
745                     break
746                 except PlayerError, error:
747                     continue
748             source.armies -= support
749             target.armies += support
750     def player_killed(self, player, killer):
751         player.alive = False
752         killer.hand.extend(player.hand)
753         if len(self.living_players()) > 1:
754             while len(killer.hand) > 5:
755                 self.play_cards_and_place_armies(killer)
756         self.log('%s killed by %s' % (player, killer))
757         if len(self.living_players()) > 1:
758             player.report(self.world, self.log)
759             # else the game is over, and killed will hear about this then.
760     def end_of_turn_cards(self, player, captures):
761         """Deal end-of-turn reward for any territory captures.
762         """
763         if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
764             self.deal(player, 1)
765     def living_players(self):
766         return [p for p in self.players if p.alive == True]
767
768
769 def generate_earth():
770     w = World('Earth')
771     c = Continent('North America', 5)
772     c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))    
773     c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
774     c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
775     c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
776     c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
777     c.append(Territory('Quebec', 'que', 3, ['eus']))
778     c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
779     c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
780     c.append(Territory('Central America', 'cam', 3, ['ven']))
781     w.append(c)
782     
783     c = Continent('Europe', 5)
784     c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
785     c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
786     c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
787     c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
788     c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
789     c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
790     c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
791     w.append(c)
792
793     c = Continent('Asia', 7)
794     c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
795     c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
796     c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
797     c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
798     c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
799     c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
800     c.append(Territory('Japan', 'jap', 2))
801     c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
802     c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
803     c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
804     c.append(Territory('India', 'indi', 3, ['sia']))
805     c.append(Territory('Siam', 'sia', 1, ['indo']))
806
807     w.append(c)
808
809     c = Continent('South America', 2)
810     c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
811     c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
812     c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
813     c.append(Territory('Argentina', 'arg', 2))
814     w.append(c)
815
816     c = Continent('Africa', 3)
817     c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
818     c.append(Territory('Egypt', 'egy', 1, ['eaf']))
819     c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
820     c.append(Territory('Congo', 'con', 3, ['saf']))
821     c.append(Territory('South Africa', 'saf', 1, ['mad']))
822     c.append(Territory('Madagascar', 'mad', 2))
823     w.append(c)
824
825     c = Continent('Australia', 2)
826     c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
827     c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
828     c.append(Territory('Western Australia', 'wau', 2, ['eau']))
829     c.append(Territory('Eastern Australia', 'eau', 3))
830     w.append(c)
831
832     w._resolve_link_names()
833     return w
834
835 def test():
836     import doctest, sys
837     failures,tests = doctest.testmod(sys.modules[__name__])
838     return failures
839
840 def random_game():
841     world = generate_earth()
842     players = [Player('Alice'), Player('Bob'), Player('Charlie')]
843     e = Engine(world, players)
844     e.run()
845
846 if __name__ == '__main__':
847     import sys
848     failures = self.test()
849     if failures > 0:
850         sys.exit(1)
851     self.random_game()