Added risk.player.email with EmailPlayer.
authorW. Trevor King <wking@drexel.edu>
Fri, 26 Mar 2010 05:46:54 +0000 (01:46 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 26 Mar 2010 05:46:54 +0000 (01:46 -0400)
Currently only a text-mode interface (no actual email), but it
supports all the necessary methods.

risk/base.py
risk/player/__init__.py [new file with mode: 0644]
risk/player/email.py [new file with mode: 0644]

index 6f62c78aa471ffa14233fb2d5ba2b69a03bb3bcf..d54b52fd3c04d9e86c67f42363506685f5b04ea2 100644 (file)
@@ -419,7 +419,7 @@ class Player (NameMixin, ID_CmpMixin):
         """
         pass
     def select_territory(self, world, log):
-        """Return the selected territory
+        """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
@@ -672,12 +672,15 @@ class Engine (ID_CmpMixin):
         source.armies -= remaining_attackers
         target.armies += remaining_attackers
         target.player = source.player
-        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
+        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)
@@ -769,8 +772,10 @@ def test():
     return failures
 
 def random_game():
+    from player.email import EmailPlayer
     world = generate_earth()
-    players = [Player('Alice'), Player('Bob'), Player('Charlie')]
+    players = [EmailPlayer('Alice', 'alice@example.com', 'server@example.com'),
+               Player('Bob'), Player('Charlie')]
     e = Engine(world, players)
     e.run()
 
diff --git a/risk/player/__init__.py b/risk/player/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/risk/player/email.py b/risk/player/email.py
new file mode 100644 (file)
index 0000000..369f547
--- /dev/null
@@ -0,0 +1,210 @@
+"""An email interface for players.
+"""
+
+from ..base import Player
+
+class EmailPlayer (Player):
+    """Human Player with an email interface.
+
+    TODO: details on procmail setup
+    """
+    def __init__(self, name, address, return_address):
+        Player.__init__(self, name)
+        self.address = address
+        self.return_address = return_address
+    def _send_email(self, world, log, subject, body):
+        wpart = self._world_part(world)
+        lpart = self._world_part(log)
+        print 'Subject: %s\n\n%s\n' % (subject, body)
+    def _get_email(self):
+        body = raw_input('response? ')
+        body = body.replace(r'\n', '\n')
+        if len(body) == 0 or body[-1] != '\n':
+            body += '\n'
+        return body
+    def _world_part(self, world):
+        pass
+    def _log_part(self, log):
+        pass
+    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
+        """
+        self._send_email(world, log, 'Report', Player.report())
+    def draw(self, world, log, cards=[]):
+        """Only called if you earned a new card (or cards).
+
+        See also
+        --------
+        report - another notification-only method
+        """
+        Player.draw(self, world, log, cards)
+        body = ['New cards:']
+        body.extend(['  %s' % c for c in cards])
+        body = ['Current Hand:']
+        for c in self.hand:
+            if c.territory != None and c.territory.player == self:
+                body.append('  %s (owned)' % c)
+            else:
+                body.append('  %s' % c)
+        self._send_email(world, log, 'Drawing cards', '\n'.join(body))
+    def select_territory(self, world, log):
+        """Return the selected territory's name.
+        """
+        body = [
+            'Reply with first line of the body of your email set',
+            'to the name (long or short, case insenitive) of the',
+            'territory you wish to occupy.  Available territories',
+            'are:']
+        for t in world.territories():
+            if t.player == None:
+                body.append('  %s' % t)
+        self._send_email(world, log, 'Select territory', '\n'.join(body))
+        body = self._get_email()
+        name = body.splitlines()[0].strip()
+        return 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.
+        """
+        possibles = list(self.hand.possible())
+        if len(possibles) == 0:
+            return None
+        subject = 'Play cards'
+        if play_required == True:
+            subject += ' (required)'
+        body = [
+            'Reply with first line of the body of your email set',
+            'to the number of the set you wish to play (leave body',
+            'blank to pass).  Available sets are:']
+        for i,h in enumerate(possibles):
+            body.append('  %d: %s' % (i, h))
+        self._send_email(world, log, subject, '\n'.join(body))
+        body = self._get_email()
+        text = body.splitlines()[0].strip()
+        if text == '':
+            return None
+        return possibles[int(text)]
+    def place_armies(self, world, log, remaining=1, this_round=1):
+        """Both during setup and before each turn.
+
+        Return {territory_name: num_armies, ...}
+        """
+        subject = 'Place %d of %d armies' % (this_round, remaining)
+        body = [
+            'You can place %d armies this round (out of %d in'
+            % (this_round, remaining),
+            'this phase).',
+            '',
+            'Reply with first line(s) of the body of your email set',
+            'to "<number_of_armies> : <territory_name>" followed by',
+            'a blank line.  For example',
+            '  1 : gbr',
+            '  4 : indo',
+            '  ...',
+            ''
+            'Your current disposition is:']
+        for t in self.territories(world):
+            body.append('  %d : %s' % (t.armies, t))
+        self._send_email(world, log, subject, '\n'.join(body))
+        body = self._get_email()
+        placements = {}
+        for line in body.splitlines():
+            line = line.strip()
+            if len(line) == 0:
+                break
+            armies,terr_name = [x.strip() for x in line.split(':')]
+            placements[terr_name] = int(armies)
+        return placements
+    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.
+        """
+        if mode == 'attack':
+            subject = 'Attack and fortify'
+            body = [
+                'You can attack as many times as you like, and fortify',
+                'once at the end of the round.  Reply with first line(s)',
+                'of the body of your email set to',
+                '  "<source_name> : <target_name> : <number_of_armies>',
+                'When you are done attacking or in place of a',
+                'fortification, insert the line "Pass".  For example',
+                '  gbr : ice : 3',
+                '  jap : chi : 3',
+                '  jap : chi : 3',
+                '  Pass',
+                '  gbr : seu : 7',
+                '  ',
+                'or',
+                '  jap : chi : 3',
+                '  Pass',
+                '  Pass',
+                '  ']
+        else:
+            assert mode == 'fortify', mode
+            subject = 'Fortify'
+            body = [
+                'You can fortify once.  Reply with first line of the',
+                'body of your email set to',
+                '  "<source_name> : <target_name> : <number_of_armies>',
+                'Or, if you choose to pass, either a blank line or',
+                '"Pass".  For example',
+                '  gbr : seu : 7',
+                'or',
+                '  ',
+                'or',
+                '  Pass']
+        self._send_email(world, log, subject, '\n'.join(body))
+        body = self._get_email()
+        if mode == 'fortify':
+            return [self._parse_attack_or_fortify_line(
+                        body.splitlines()[0], mode)]
+        actions = []
+        pass_count = 0
+        for line in body.splitlines():
+            action = self._parse_attack_or_fortify_line(line, mode)
+            if action == None:
+                pass_count += 1
+                if pass_count == 2:
+                    break
+            actions.append(action)
+        return actions
+    def _parse_attack_or_fortify_line(self, line, mode):
+        line = line.strip()
+        if line.count(':') == 2:
+            fields = [x.strip() for x in line.split(':')]
+            fields[2] = int(fields[2])
+            return fields
+        elif line.lower() == 'pass' \
+                or (mode == 'fortify' and len(line) == 0):
+            return None
+    def support_attack(self, world, log, source, target):
+        """Follow up on a conquest by moving additional armies.
+        """
+        subject = 'Support conquest of %s by %s' % (target, source)
+        body = [
+            'You can move up to %d of the %d armies remaining on'
+            % (source.armies - 1, source.armies),
+            '%s to %s following your conquest.'
+            % (source, target),
+            '',
+            'Reply with first line(s) of the body of your email set',
+            'to "<number_of_armies>", or leave the first line blank',
+            'to pass.']
+        self._send_email(world, log, subject, '\n'.join(body))
+        body = self._get_email()
+        text = body.splitlines()[0].strip()
+        if text == '':
+            return 0
+        return int(text)