--- /dev/null
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""A Python engine for Risk-like games
+"""
+
+import random
+
+from .log import Logger
+
+VERSION='0.1'
+
+
+class PlayerError (Exception):
+ pass
+
+class NameMixin (object):
+ """Simple mixin for pretty-printing named objects.
+ """
+ def __init__(self, name):
+ self.name = name
+ def __str__(self):
+ return self.name
+ def __repr__(self):
+ return self.__str__()
+
+
+class ID_CmpMixin (object):
+ """Simple mixin to ensure the fancier comparisons are all based on
+ __cmp__().
+ """
+ def __cmp__(self, other):
+ return cmp(id(self), id(other))
+ def __eq__(self, other):
+ return self.__cmp__(other) == 0
+ def __ne__(self, other):
+ return self.__cmp__(other) != 0
+
+class Territory (NameMixin, ID_CmpMixin, list):
+ """An occupiable territory.
+
+ Contains a list of neighboring territories.
+ """
+ def __init__(self, name, short_name=None, type=-1,
+ link_names=[], continent=None, player=None):
+ NameMixin.__init__(self, name)
+ ID_CmpMixin.__init__(self)
+ list.__init__(self)
+ self.short_name = short_name
+ if short_name == None:
+ self.short_name = name
+ self._card_type = type # for Deck construction
+ self._link_names = list(link_names) # used by World._resolve_link_names
+ self.continent = continent # used by World.production
+ self.player = player # who owns this territory
+ self.armies = 0 # number of occupying armies
+ def __str__(self):
+ if self.short_name == self.name:
+ return self.name
+ return '%s (%s)' % (self.name, self.short_name)
+ def borders(self, other):
+ for t in self:
+ if id(t) == id(other):
+ return True
+ return False
+
+class Continent (NameMixin, ID_CmpMixin, list):
+ """A group of Territories.
+
+ Stores the army-production bonus if it's owned by a single player.
+ """
+ def __init__(self, name, production, territories=[]):
+ NameMixin.__init__(self, name)
+ ID_CmpMixin.__init__(self)
+ list.__init__(self, territories)
+ self.production = production
+ def append(self, territory):
+ """Add a new territory (setting the territory's .continent
+ attribute).
+ """
+ list.append(self, territory)
+ territory.continent = self
+ def territory_by_name(self, name):
+ """Find a Territory instance by name (long or short, case
+ insensitive).
+ """
+ for t in self:
+ if name.lower() in [t.short_name.lower(), t.name.lower()]:
+ return t
+ raise KeyError(name)
+ def single_player(self):
+ """Is the continent owned by a single player?
+ """
+ p = self[0].player
+ for territory in self:
+ if territory.player != p:
+ return False
+ return True
+
+class World (NameMixin, ID_CmpMixin, list):
+ """Store the world map and current world state.
+
+ Holds list of Continents. Also controls territory-based army
+ production (via production).
+ """
+ def __init__(self, name, continents=[]):
+ NameMixin.__init__(self, name)
+ ID_CmpMixin.__init__(self)
+ list.__init__(self, continents)
+ self.initial_armies = { # num_players:num_armies
+ 2: 40, 3:35, 4:30, 5:25, 6:20
+ }
+ def territories(self):
+ """Iterate through all the territories in the world.
+ """
+ for continent in self:
+ for territory in continent:
+ yield territory
+ def territory_by_name(self, name):
+ """Find a Territory instance by name (long or short, case
+ insensitive).
+ """
+ for continent in self:
+ try:
+ return continent.territory_by_name(name)
+ except KeyError:
+ pass
+ raise KeyError(name)
+ def continent_by_name(self, name):
+ """Find a Continent instance by name (case insensitive).
+ """
+ for continent in self:
+ if continent.name.lower() == name.lower():
+ return continent
+ raise KeyError(name)
+ def _resolve_link_names(self):
+ """Initialize Territory links.
+
+ The Territory class doesn't actually link to neighbors after
+ initialization, but one of each linked pair has the others
+ name in _link_names. This method goes through the territories,
+ looks up the referenced link target, and joins the pair.
+ """
+ self._check_short_names()
+ for territory in self.territories():
+ for name in territory._link_names:
+ other = self.territory_by_name(name)
+ if not territory.borders(other):
+ territory.append(other)
+ if not other.borders(territory):
+ other.append(territory)
+ def _check_short_names(self):
+ """Ensure there are no short_name collisions.
+ """
+ ts = {}
+ for t in self.territories():
+ if t.short_name.lower() not in ts:
+ ts[t.short_name.lower()] = t
+ else:
+ raise ValueError('%s shared by %s and %s'
+ % (t.short_name.lower(), ts[t.short_name.lower()], t))
+ def production(self, player):
+ """Calculate the number of armies a player should earn based
+ on territory occupation.
+ """
+ ts = list(player.territories(self))
+ production = max(3, len(ts) / 3)
+ continents = set([t.continent.name for t in ts])
+ for c_name in continents:
+ c = self.continent_by_name(c_name)
+ if c.single_player() == True:
+ production += c.production
+ return (production, {})
+ def place_territory_production(self, territory_production):
+ """Place armies based on {territory_name: num_armies, ...}.
+ """
+ for territory_name,production in territory_production.items():
+ t = self.territory_by_name(territory_name)
+ t.armies += production
+
+class Card (ID_CmpMixin):
+ """Represent a territory card (or wild)
+
+ Nothing exciting going on here, just a class for pretty-printing
+ card names.
+ """
+ def __init__(self, deck, type_, territory=None):
+ ID_CmpMixin.__init__(self)
+ self.deck = deck
+ self.territory = territory
+ self.type = type_
+ def __str__(self):
+ if self.territory == None:
+ return '<Card %s>' % (self.deck.type_names[self.type])
+
+ return '<Card %s %s>' % (self.territory,
+ self.deck.type_names[self.type])
+ def __repr__(self):
+ return self.__str__()
+
+class Deck (list):
+ """All the cards yet to be handed out in a given game.
+
+ Controls the type branding (via type_names) and army production
+ values for scoring sets (via production_value).
+ """
+ def __init__(self, territories=[], num_wilds=2,
+ type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
+ list.__init__(self, [Card(self, t._card_type, t) for t in territories])
+ self.type_names = type_names
+ for i in range(num_wilds):
+ self.append(Card(self, 0))
+ self._production_sequence = [4, 6, 8, 10, 12, 15]
+ self._production_index = 0
+ def shuffle(self):
+ """Shuffle the remaining cards in the deck.
+ """
+ random.shuffle(self)
+ def production_value(self, index):
+ """
+ >>> d = Deck()
+ >>> [d.production_value(i) for i in range(8)]
+ [4, 6, 8, 10, 12, 15, 20, 25]
+ """
+ if index < len(self._production_sequence):
+ return self._production_sequence[index]
+ extra = index - len(self._production_sequence) + 1
+ return self._production_sequence[-1] + 5 * extra
+ def production(self, player, cards=None):
+ """
+ >>> d = Deck()
+ >>> a = Player('Alice')
+ >>> b = Player('Bob')
+ >>> d.production(a, None)
+ (0, {})
+ >>> d.production(a, [Card(d, 1, Territory('a')),
+ ... Card(d, 1, Territory('b'))])
+ Traceback (most recent call last):
+ ...
+ PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
+ >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
+ ... Card(d, 1, Territory('b', player=b)),
+ ... Card(d, 1, Territory('c'))])
+ (4, {'a': 1})
+ >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
+ ... Card(d, 2, Territory('b', player=a)),
+ ... Card(d, 0, Territory('c', player=a))])
+ >>> p
+ 6
+ >>> sorted(tp.items())
+ [('a', 1), ('b', 1), ('c', 1)]
+ """
+ if cards == None:
+ return (0, {})
+ h = Hand(cards)
+ if h.scores():
+ p = self.production_value(self._production_index)
+ self._production_index += 1
+ territory_production = {}
+ for c in cards:
+ if c.territory != None and c.territory.player == player:
+ territory_production[c.territory.name] = 1
+ return (p, territory_production)
+ raise PlayerError('%s is not a scoring set' % h)
+
+class Hand (list):
+ """Represent a hand of cards.
+
+ This is the place to override the set of allowed scoring
+ combinations. You should override one of
+
+ * set
+ * run
+ * scores
+
+ Adding additional scoring methods as needed (e.g. flush).
+ """
+ def __init__(self, cards=[]):
+ list.__init__(self, cards)
+ def set(self):
+ if len(self) != 3:
+ return False
+ s = sorted(set([card.type for card in self]))
+ if len(s) == 1 \
+ or (len(s) == 2 and s[0] == 0):
+ return True
+ return False
+ def run(self):
+ if len(self) != 3:
+ return False
+ if len(set([card.type for card in self])) == 3:
+ return True
+ return False
+ def scores(self):
+ """The hand is any valid scoring combination.
+ """
+ return self.set() or self.run()
+ def subhands(self, lengths=None):
+ """Return all possible subhands.
+
+ Lengths can either be a list of allowed subhand lengths or
+ None. If None, all possible subhand lengths are allowed.
+
+ >>> d = Deck()
+ >>> h = Hand([Card(d, 1, Territory('a')),
+ ... Card(d, 1, Territory('b')),
+ ... Card(d, 1, Territory('c')),
+ ... Card(d, 1, Territory('d'))])
+ >>> for hand in h.subhands():
+ ... print hand
+ [<Card a Infantry>]
+ [<Card b Infantry>]
+ [<Card c Infantry>]
+ [<Card d Infantry>]
+ [<Card a Infantry>, <Card b Infantry>]
+ [<Card a Infantry>, <Card c Infantry>]
+ [<Card a Infantry>, <Card d Infantry>]
+ [<Card b Infantry>, <Card c Infantry>]
+ [<Card b Infantry>, <Card d Infantry>]
+ [<Card c Infantry>, <Card d Infantry>]
+ [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
+ [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
+ [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
+ [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
+ [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
+ """
+ for i in range(len(self)):
+ i += 1 # check all sub-hands of length i
+ if lengths != None and i not in lengths:
+ continue # don't check this length
+ indices = range(i)
+ stop = range(len(self)-i, len(self))
+ while indices != stop:
+ yield Hand([self[i] for i in indices])
+ indices = self._increment(indices, stop)
+ yield Hand([self[i] for i in indices])
+ def _increment(self, indices, stop):
+ """
+ >>> d = Deck()
+ >>> h = Hand([Card(d, 1, Territory('a'))])
+ >>> h._increment([0, 1, 2], [2, 3, 4])
+ [0, 1, 3]
+ >>> h._increment([0, 1, 3], [2, 3, 4])
+ [0, 1, 4]
+ >>> h._increment([0, 1, 4], [2, 3, 4])
+ [0, 2, 3]
+ """
+ moveable = [i for i,m in zip(indices, stop) if i < m]
+ assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
+ key = indices.index(moveable[-1])
+ new = indices[key] + 1
+ for i in range(key, len(indices)):
+ indices[i] = new + i-key
+ return indices
+ def possible(self):
+ """Return a list of all possible scoring subhands.
+ """
+ for h in self.subhands():
+ if h.scores():
+ yield h
+
+class Player (NameMixin, ID_CmpMixin):
+ """Represent a risk player.
+
+ This class implements a very basic AI player. Subclasses should
+ consider overriding the "action-required" methods:
+
+ * select_territory
+ * play_cards
+ * place_armies
+ * attack_and_fortify
+ * support_attack
+
+ And the "report" methods:
+
+ * report
+ * draw
+ """
+ def __init__(self, name):
+ NameMixin.__init__(self, name)
+ ID_CmpMixin.__init__(self)
+ self.alive = True
+ self.hand = Hand()
+ self._message_index = 0
+ def territories(self, world):
+ """Iterate through all territories owned by this player.
+ """
+ for t in world.territories():
+ if t.player == self:
+ yield t
+ def border_territories(self, world):
+ """Iterate through all territories owned by this player which
+ border another player's territories.
+ """
+ for t in self.territories(world):
+ for neighbor in t:
+ if neighbor.player != self:
+ yield t
+ break
+ def report(self, world, log):
+ """Send reports about death and game endings.
+
+ These events mark the end of contact and require no change in
+ player status or response, so they get a special command
+ seperate from the usual action family. The action commands in
+ Player subclasses can notify the player (possibly by calling
+ report internally) if they feel so inclined.
+
+ See also
+ --------
+ draw - another notification-only method
+ """
+ print 'Reporting for %s:\n %s' \
+ % (self, '\n '.join(log[self._message_index:]))
+ self._message_index = len(log)
+ def draw(self, world, log, cards=[]):
+ """Only called if you earned a new card (or cards).
+
+ See also
+ --------
+ report - another notification-only method
+ """
+ pass
+ def select_territory(self, world, log):
+ """Return the selected territory's name.
+ """
+ free_territories = [t for t in world.territories() if t.player == None]
+ return random.sample(free_territories, 1)[0].name
+ def play_cards(self, world, log, play_required=True):
+ """Decide whether or not to turn in a set of cards.
+
+ Return a list of cards to turn in or None. If play_required
+ is True, you *must* play.
+ """
+ if play_required == True:
+ return random.sample(list(self.hand.possible()), 1)[0]
+ def place_armies(self, world, log, remaining=1, this_round=1):
+ """Both during setup and before each turn.
+
+ Return {territory_name: num_armies, ...}
+ """
+ t = random.sample(list(self.border_territories(world)), 1)[0]
+ return {t.name: this_round}
+ def attack_and_fortify(self, world, log, mode='attack'):
+ """Return list of (source, target, armies) tuples. Place None
+ in the list to end this phase.
+ """
+ assert mode != 'fortify', mode
+ possible_attacks = []
+ for t in self.border_territories(world):
+ if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
+ continue
+ targets = [border_t for border_t in t if border_t.player != self]
+ for tg in targets:
+ possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
+ if len(possible_attacks) == 0:
+ return [None, None] # stop attack phase, then stop fortification phase
+ return random.sample(possible_attacks, 1) # + [None]
+ def support_attack(self, world, log, source, target):
+ """Follow up on a conquest by moving additional armies.
+ """
+ return source.armies-1
+
+class Engine (ID_CmpMixin):
+ """Drive the game.
+
+ Basic usage will be along the lines of
+
+ >>> world = generate_earth()
+ >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
+ >>> e = Engine(world, players)
+ >>> e.run() # doctest: +ELLIPSIS
+ ...
+ """
+ def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
+ ID_CmpMixin.__init__(self)
+ self.world = world
+ self.deck = deck_class(world.territories())
+ self.log = logger_class()
+ self.players = players
+ def __str__(self):
+ return '<engine %s %s>' % (self.world, self.players)
+ def __repr__(self):
+ return self.__str__()
+ def run(self):
+ """The main entry point.
+ """
+ self.setup()
+ self.play()
+ self.game_over()
+ def setup(self):
+ """Setup phase. Pick territories, place initial armies, and
+ deal initial hands.
+ """
+ for p in self.players:
+ p.alive = True
+ random.shuffle(self.players)
+ self.deck.shuffle()
+ self.select_territories()
+ self.place_initial_armies()
+ for p in self.players:
+ self.deal(p, 3)
+ def play(self):
+ """Main gameplay phase. Take turns until only one Player survives.
+ """
+ turn = 0
+ active_player = 0
+ living = len(self.living_players())
+ while living > 1:
+ self.play_turn(self.players[active_player])
+ living = len(self.living_players())
+ active_player = (active_player + 1) % len(self.players)
+ if living > 1:
+ while self.players[active_player].alive == False:
+ active_player = (active_player + 1) % len(self.players)
+ turn += 1
+ def game_over(self):
+ """The end of the game.
+
+ Currently just a notification hook.
+ """
+ self.log('Game over.')
+ for p in self.players:
+ p.report(self.world, self.log)
+ def play_turn(self, player):
+ """Work through the phases of player's turn.
+ """
+ self.log("%s's turn (territory score: %s)"
+ % (player, [(p,len(list(p.territories(self.world))))
+ for p in self.players]))
+ self.play_cards_and_place_armies(player)
+ captures = self.attack_and_fortify(player)
+ self.end_of_turn_cards(player, captures)
+ def select_territories(self):
+ for t in self.world.territories():
+ t.player = None
+ for i in range(len(list(self.world.territories()))):
+ p = self.players[i % len(self.players)]
+ t_name = p.select_territory(self.world, self.log)
+ t = self.world.territory_by_name(t_name)
+ if t.player != None:
+ raise PlayerError('Cannot select %s owned by %s'
+ % (t, t.player))
+ self.log('%s selects %s' % (p, t))
+ t.player = p
+ t.armies = 1
+ def place_initial_armies(self):
+ already_placed = [len(list(p.territories(self.world))) for p in self.players]
+ s = list(set(already_placed))
+ assert len(s) in [1,2], already_placed
+ if len(s) == 2: # catch up the players who are one territory short
+ assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
+ for p,placed in zip(self.players, already_placed):
+ if placed == min(s):
+ self.player_place_armies(p, remaining, 1)
+ remaining = self.world.initial_armies[len(self.players)] - max(s)
+ while remaining > 0:
+ for p in self.players:
+ self.player_place_armies(p, remaining, 1)
+ remaining -= 1
+ def player_place_armies(self, player, remaining=1, this_round=1):
+ placements = player.place_armies(self.world, self.log, remaining, this_round)
+ if sum(placements.values()) != this_round:
+ raise PlayerError('Placing more than %d armies' % this_round)
+ for ter_name,armies in placements.items():
+ t = self.world.territory_by_name(ter_name)
+ if t.player != player:
+ raise PlayerError('Placing armies in %s owned by %s'
+ % (t, t.player))
+ if armies < 0:
+ raise PlayerError('Placing a negative number of armies (%d) in %s'
+ % (armies, t))
+ self.log('%s places %s' % (player, placements))
+ for terr_name,armies in placements.items():
+ t = self.world.territory_by_name(terr_name)
+ t.armies += armies
+ def deal(self, player, number):
+ cards = []
+ for i in range(number):
+ cards.append(self.deck.pop())
+ player.hand.extend(cards)
+ player.draw(self.world, self.log, cards)
+ self.log('%s dealt %d cards' % (player, number))
+ def play_cards_and_place_armies(self, player, additional_armies=0):
+ cards_required = len(player.hand) >= 5
+ cards = player.play_cards(
+ self.world, self.log, play_required=cards_required)
+ if cards_required == True and cards == None:
+ raise PlayerError('You have %d >= 5 cards in your hand, you must play'
+ % len(player.hand))
+ w_prod,w_terr_prod = self.world.production(player)
+ self.log('%s earned %d armies from territories' % (player, w_prod))
+ c_prod,c_terr_prod = self.deck.production(player, cards)
+ if c_prod > 0:
+ self.log('%s played %s, earning %d armies'
+ % (player, cards, c_prod+sum(c_terr_prod.values())))
+ if cards != None:
+ for c in cards:
+ player.hand.remove(c)
+ for terr,prod in c_terr_prod.items():
+ if terr in w_terr_prod:
+ w_terr_prod[terr] += prod
+ else:
+ w_terr_prod[terr] = prod
+ self.world.place_territory_production(w_terr_prod)
+ if len(w_terr_prod) > 0:
+ self.log('%s was required to place %s' % (player, w_terr_prod))
+ armies = w_prod + c_prod
+ self.player_place_armies(player, armies, armies)
+ def attack_and_fortify(self, player):
+ captures = 0
+ mode = 'attack'
+ while True:
+ actions = player.attack_and_fortify(self.world, self.log, mode)
+ for action in actions:
+ if action == None:
+ if mode == 'attack':
+ mode = 'fortify'
+ continue
+ else:
+ assert mode == 'fortify', mode
+ return captures
+ source_name,target_name,armies = action
+ source = self.world.territory_by_name(source_name)
+ target = self.world.territory_by_name(target_name)
+ if not source.borders(target):
+ raise PlayerError('Cannot reach %s from %s to %s'
+ % (target, source, mode))
+ if mode == 'attack':
+ tplayer = target.player
+ capture = self.attack(source, target, armies)
+ if capture == True:
+ captures += 1
+ if len(list(tplayer.territories(self.world))) == 0:
+ self.player_killed(tplayer, killer=player)
+ else:
+ assert mode == 'fortify', mode
+ self.fortify(source, target, armies)
+ def attack(self, source, target, armies):
+ if source.player == target.player:
+ raise PlayerError('%s attacking %s, but you own both.'
+ % (source, target))
+ if armies == 0:
+ raise PlayerError('%s attacking %s with 0 armies.'
+ % (source, target))
+ if armies >= source.armies:
+ raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
+ % (source, target, armies, source.armies-1))
+ a_dice = sorted([random.randint(1, 6) for i in range(armies)],
+ reverse=True)
+ t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
+ reverse=True)
+ a_dead = 0
+ t_dead = 0
+ for a,d in zip(a_dice, t_dice):
+ if d >= a:
+ a_dead += 1
+ else:
+ t_dead += 1
+ source.armies -= a_dead
+ target.armies -= t_dead
+ if target.armies == 0:
+ self.takeover(source, target, remaining_attackers=armies-a_dead)
+ self.log('%s conquered %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d'
+ % (source.player, target, source, armies, len(t_dice),
+ a_dead, t_dead, source.armies, target.armies))
+ assert target.armies > 0, target
+ return True
+ self.log('%s attacked %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d' \
+ % (source.player, target, source, armies, len(t_dice),
+ a_dead, t_dead, source.armies, target.armies))
+ assert target.armies > 0, target
+ return False
+ def takeover(self, source, target, remaining_attackers):
+ source.armies -= remaining_attackers
+ target.armies += remaining_attackers
+ target.player = source.player
+ if source.armies > 1:
+ support = source.player.support_attack(
+ self.world, self.log, source, target)
+ if support < 0 or support >= source.armies:
+ raise PlayerError(
+ 'Cannot support from %s to %s with %d armies, only %d available'
+ % (source, target, support, source.armies-1))
+ source.armies -= support
+ target.armies += support
+ def player_killed(self, player, killer):
+ player.alive = False
+ killer.hand.extend(player.hand)
+ if len(self.living_players()) > 1:
+ while len(killer.hand) > 5:
+ self.play_cards_and_place_armies(killer)
+ self.log('%s killed by %s' % (player, killer))
+ if len(self.living_players()) > 1:
+ player.report(self.world, self.log)
+ # else the game is over, and killed will hear about this then.
+ def end_of_turn_cards(self, player, captures):
+ """Deal end-of-turn reward for any territory captures.
+ """
+ if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
+ self.deal(player, 1)
+ def living_players(self):
+ return [p for p in self.players if p.alive == True]
+
+
+def generate_earth():
+ w = World('Earth')
+ c = Continent('North America', 5)
+ c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))
+ c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
+ c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
+ c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
+ c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
+ c.append(Territory('Quebec', 'que', 3, ['eus']))
+ c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
+ c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
+ c.append(Territory('Central America', 'cam', 3, ['ven']))
+ w.append(c)
+
+ c = Continent('Europe', 5)
+ c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
+ c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
+ c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
+ c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
+ c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
+ c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
+ c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
+ w.append(c)
+
+ c = Continent('Asia', 7)
+ c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
+ c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
+ c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
+ c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
+ c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
+ c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
+ c.append(Territory('Japan', 'jap', 2))
+ c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
+ c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
+ c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
+ c.append(Territory('India', 'indi', 3, ['sia']))
+ c.append(Territory('Siam', 'sia', 1, ['indo']))
+
+ w.append(c)
+
+ c = Continent('South America', 2)
+ c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
+ c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
+ c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
+ c.append(Territory('Argentina', 'arg', 2))
+ w.append(c)
+
+ c = Continent('Africa', 3)
+ c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
+ c.append(Territory('Egypt', 'egy', 1, ['eaf']))
+ c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
+ c.append(Territory('Congo', 'con', 3, ['saf']))
+ c.append(Territory('South Africa', 'saf', 1, ['mad']))
+ c.append(Territory('Madagascar', 'mad', 2))
+ w.append(c)
+
+ c = Continent('Australia', 2)
+ c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
+ c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
+ c.append(Territory('Western Australia', 'wau', 2, ['eau']))
+ c.append(Territory('Eastern Australia', 'eau', 3))
+ w.append(c)
+
+ w._resolve_link_names()
+ return w
+
+def test():
+ import doctest, sys
+ failures,tests = doctest.testmod(sys.modules[__name__])
+ return failures
+
+def random_game():
+ from player.email import EmailPlayer
+ world = generate_earth()
+ players = [EmailPlayer('Alice', 'alice@example.com', 'server@example.com'),
+ Player('Bob'), Player('Charlie')]
+ e = Engine(world, players)
+ e.run()
+
+if __name__ == '__main__':
+ import sys
+ failures = self.test()
+ if failures > 0:
+ sys.exit(1)
+ self.random_game()