From 9715d3e8a874a87b299086f3c4cea012a721c7d5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 25 Mar 2010 17:28:52 -0400 Subject: [PATCH] Began versioning (v0.1), engine and basic AI. --- risk/__init__.py | 0 risk/base.py | 554 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100644 risk/__init__.py create mode 100755 risk/base.py diff --git a/risk/__init__.py b/risk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/risk/base.py b/risk/base.py new file mode 100755 index 0000000..dbff83b --- /dev/null +++ b/risk/base.py @@ -0,0 +1,554 @@ +#!/usr/bin/python +# +# A Python engine for Risk-like games + +import random + + +VERSION='0.1' + + +class PlayerError (Exception): + pass + +class ID_CmpMixin (object): + def __str__(self): + return self.name + def __repr__(self): + return self.__str__() + 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 (list, ID_CmpMixin): + def __init__(self, name, short_name=None, type=-1, + link_names=[], continent=None, player=None): + list.__init__(self) + ID_CmpMixin.__init__(self) + self.name = name + self.short_name = short_name + if short_name == None: + self.short_name = name + self._card_type = type + self._link_names = list(link_names) + self.continent = continent + self.player = player + self.card = None + self.armies = 0 + def __str__(self): + if self.short_name == self.name: + return self.name + return '%s (%s)' % (self.name, self.short_name) + def __repr__(self): + return self.__str__() + def borders(self, other): + for t in self: + if id(t) == id(other): + return True + return False + +class Continent (list, ID_CmpMixin): + def __init__(self, name, production, territories=[]): + list.__init__(self, territories) + ID_CmpMixin.__init__(self) + self.name = name + self.production = production + def append(self, territory): + list.append(self, territory) + territory.continent = self + def territory_by_name(self, name): + for t in self: + if name.lower() in [t.short_name.lower(), t.name.lower()]: + #assert self.contains_territory(t), t + return t + raise KeyError(name) + def contains_territory(self, territory): + for t in self: + if t == territory: + return True + return False + def single_player(self): + p = self[0].player + for territory in self: + if territory.player != p: + return False + return True + +class World (list, ID_CmpMixin): + def __init__(self, name, continents=[]): + list.__init__(self, continents) + ID_CmpMixin.__init__(self) + self.name = name + self.initial_armies = { # num_players:num_armies + 2: 40, 3:35, 4:30, 5:25, 6:20 + } + def territories(self): + for continent in self: + for territory in continent: + yield territory + def territory_by_name(self, name): + for continent in self: + try: + return continent.territory_by_name(name) + except KeyError: + pass + raise KeyError(name) + def contains_territory(self, territory): + for continent in self: + if continent.contains_territory(territory): + return True + return False + def continent_by_name(self, name): + for continent in self: + if continent.name.lower() == name.lower(): + return continent + raise KeyError(name) + def _resolve_link_names(self): + 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): + ts = {} + for t in self.territories(): + if t.short_name not in ts: + ts[t.short_name] = t + else: + raise ValueError('%s shared by %s and %s' + % (t.short_name, ts[t.short_name], t)) + def production(self, player): + ts = list(player.territories(world)) + 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): + for territory_name,production in territory_production.items(): + t = self.territory_by_name(territory_name) + t.armies += production + +class Card (ID_CmpMixin): + 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 '' % (self.deck.type_names[self.type]) + + return '' % (self.territory, + self.deck.type_names[self.type]) + def __repr__(self): + return self.__str__() + +class Deck (list): + def __init__(self, territories=[]): + list.__init__(self, [Card(self, t._card_type, t) for t in territories]) + random.shuffle(self) + self.type_names = ['Wild', 'Infantry', 'Cavalry', 'Artillery', 'Wild'] + for i in range(2): + self.append(Card(self, 0)) + self._production_sequence = [4, 6, 8, 10, 12, 15] + self._production_index = 0 + 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: You must play cards in groups of 3, not 2 + ([, ]) + >>> 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, {}) + if len(cards) != 3: + raise PlayerError('You must play cards in groups of 3, not %d\n(%s)' + % (len(cards), cards)) + h = Hand(cards) + if h.set() or h.run(): + 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 neither a set nor a run' % cards) + +class Hand (list): + def __init__(self, cards=[]): + list.__init__(self, cards) + def set(self): + 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(set([card.type for card in self])) == 3: + return True + return False + def possible(self): + if len(self) >= 3: + for i,c1 in enumerate(self[:-2]): + for j,c2 in enumerate(self[i+1:-1]): + for c3 in self[i+j+2:]: + h = Hand([c1, c2, c3]) + if h.set() or h.run(): + yield h + +class Player (ID_CmpMixin): + def __init__(self, name): + self.name = name + ID_CmpMixin.__init__(self) + self.alive = True + self.hand = Hand() + def territories(self, world): + for t in world.territories(): + if t.player == self: + yield t + def border_territories(self, world): + for t in self.territories(world): + for neighbor in t: + if neighbor.player != self: + yield t + break + def phase_select_territory(self, world): + """Return the selected territory + """ + free_territories = [t for t in world.territories() if t.player == None] + return random.sample(free_territories, 1)[0] + def phase_play_cards(self, world, play_required=True): + if play_required == True: + return random.sample(list(self.hand.possible()), 1)[0] + def phase_place_armies(self, world, 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 phase_attack(self, world): + """Return list of (source, target, armies) tuples. Place None + in the list to end this phase. + """ + 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] + return random.sample(possible_attacks, 1) # + [None] + def phase_support_attack(self, world, source, target): + return source.armies-1 + def phase_fortify(self, world): + """Return list of (source, target, armies) tuples. Place None + in the list to end this phase. + """ + return [None] + def phase_draw(self, cards=[]): + """Only called if you earned a new card (or cards)""" + self.hand.extend(cards) + +class Engine (ID_CmpMixin): + def __init__(self, world, players, deck_class=Deck): + ID_CmpMixin.__init__(self) + self.world = world + self.deck = deck_class(world.territories()) + self.players = players + def __str__(self): + return '' % (self.world, self.players) + def __repr__(self): + return self.__str__() + def run(self): + self.setup() + self.play() + def setup(self): + for p in self.players: + p.alive = True + random.shuffle(players) + self.select_territories() + self.place_initial_armies() + self.deal() + def play(self): + 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) % living + turn += 1 + def play_turn(self, player): + print "%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_phase(player) + if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1: + player.phase_draw([self.deck.pop()]) + 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 = p.phase_select_territory(self.world) + if t.player != None: + raise PlayerError('Cannot select %s owned by %s' + % (t, t.player)) + print '%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.phase_place_armies(self.world, 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 = 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)) + print '%s places %s' % (player, placements) + for ter_name,armies in placements.items(): + t = world.territory_by_name(ter_name) + t.armies += armies + def deal(self): + for p in self.players: + cards = [] + for i in range(3): + cards.append(self.deck.pop()) + p.phase_draw(cards) + print 'Initial hands dealt' + def play_cards_and_place_armies(self, player, additional_armies=0): + cards_required = len(player.hand) >= 5 + cards = player.phase_play_cards( + self.world, 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) + print '%s earned %d armies from territories' % (player, w_prod) + c_prod,c_terr_prod = self.deck.production(player, cards) + if c_prod > 0: + print '%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: + print '%s was required to place %s' % (player, w_terr_prod) + armies = w_prod + c_prod + self.player_place_armies(player, armies, armies) + def attack_phase(self, player): + captures = 0 + while True: + attacks = player.phase_attack(self.world) + for attack in attacks: + if attack == None: + return captures + source_name,target_name,armies = attack + source = self.world.territory_by_name(source_name) + target = self.world.territory_by_name(target_name) + 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) + 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) + print '%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 + print '%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 + support = source.player.phase_support_attack(self.world, 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) + print '%s killed by %s' % (player, killer) + 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 + +if __name__ == '__main__': + import doctest + import sys + + failures,tests = doctest.testmod() + if failures > 0: + sys.exit(1) + + world = generate_earth() + players = [Player('A'), Player('B')] + e = Engine(world, players) + e.run() -- 2.26.2