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