Added README and renamed risk/ -> pyrisk/
[pyrisk.git] / pyrisk / player / email.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 """An email interface for players.
18 """
19
20 from ..base import Player
21
22 class EmailPlayer (Player):
23     """Human Player with an email interface.
24
25     TODO: details on procmail setup
26     """
27     def __init__(self, name, address, return_address):
28         Player.__init__(self, name)
29         self.address = address
30         self.return_address = return_address
31     def _send_email(self, world, log, subject, body):
32         wpart = self._world_part(world)
33         lpart = self._world_part(log)
34         print 'Subject: %s\n\n%s\n' % (subject, body)
35     def _get_email(self):
36         body = raw_input('response? ')
37         body = body.replace(r'\n', '\n')
38         if len(body) == 0 or body[-1] != '\n':
39             body += '\n'
40         return body
41     def _world_part(self, world):
42         pass
43     def _log_part(self, log):
44         pass
45     def report(self, world, log):
46         """Send reports about death and game endings.
47
48         These events mark the end of contact and require no change in
49         player status or response, so they get a special command
50         seperate from the usual action family.  The action commands in
51         Player subclasses can notify the player (possibly by calling
52         report internally) if they feel so inclined.
53         
54         See also
55         --------
56         draw - another notification-only method
57         """
58         self._send_email(world, log, 'Report', Player.report())
59     def draw(self, world, log, cards=[]):
60         """Only called if you earned a new card (or cards).
61
62         See also
63         --------
64         report - another notification-only method
65         """
66         Player.draw(self, world, log, cards)
67         body = ['New cards:']
68         body.extend(['  %s' % c for c in cards])
69         body = ['Current Hand:']
70         for c in self.hand:
71             if c.territory != None and c.territory.player == self:
72                 body.append('  %s (owned)' % c)
73             else:
74                 body.append('  %s' % c)
75         self._send_email(world, log, 'Drawing cards', '\n'.join(body))
76     def select_territory(self, world, log):
77         """Return the selected territory's name.
78         """
79         body = [
80             'Reply with first line of the body of your email set',
81             'to the name (long or short, case insenitive) of the',
82             'territory you wish to occupy.  Available territories',
83             'are:']
84         for t in world.territories():
85             if t.player == None:
86                 body.append('  %s' % t)
87         self._send_email(world, log, 'Select territory', '\n'.join(body))
88         body = self._get_email()
89         name = body.splitlines()[0].strip()
90         return name
91     def play_cards(self, world, log, play_required=True):
92         """Decide whether or not to turn in a set of cards.
93
94         Return a list of cards to turn in or None.  If play_required
95         is True, you *must* play.
96         """
97         possibles = list(self.hand.possible())
98         if len(possibles) == 0:
99             return None
100         subject = 'Play cards'
101         if play_required == True:
102             subject += ' (required)'
103         body = [
104             'Reply with first line of the body of your email set',
105             'to the number of the set you wish to play (leave body',
106             'blank to pass).  Available sets are:']
107         for i,h in enumerate(possibles):
108             body.append('  %d: %s' % (i, h))
109         self._send_email(world, log, subject, '\n'.join(body))
110         body = self._get_email()
111         text = body.splitlines()[0].strip()
112         if text == '':
113             return None
114         return possibles[int(text)]
115     def place_armies(self, world, log, remaining=1, this_round=1):
116         """Both during setup and before each turn.
117
118         Return {territory_name: num_armies, ...}
119         """
120         subject = 'Place %d of %d armies' % (this_round, remaining)
121         body = [
122             'You can place %d armies this round (out of %d in'
123             % (this_round, remaining),
124             'this phase).',
125             '',
126             'Reply with first line(s) of the body of your email set',
127             'to "<number_of_armies> : <territory_name>" followed by',
128             'a blank line.  For example',
129             '  1 : gbr',
130             '  4 : indo',
131             '  ...',
132             ''
133             'Your current disposition is:']
134         for t in self.territories(world):
135             body.append('  %d : %s' % (t.armies, t))
136         self._send_email(world, log, subject, '\n'.join(body))
137         body = self._get_email()
138         placements = {}
139         for line in body.splitlines():
140             line = line.strip()
141             if len(line) == 0:
142                 break
143             armies,terr_name = [x.strip() for x in line.split(':')]
144             placements[terr_name] = int(armies)
145         return placements
146     def attack_and_fortify(self, world, log, mode='attack'):
147         """Return list of (source, target, armies) tuples.  Place None
148         in the list to end this phase.
149         """
150         if mode == 'attack':
151             subject = 'Attack and fortify'
152             body = [
153                 'You can attack as many times as you like, and fortify',
154                 'once at the end of the round.  Reply with first line(s)',
155                 'of the body of your email set to',
156                 '  "<source_name> : <target_name> : <number_of_armies>',
157                 'When you are done attacking or in place of a',
158                 'fortification, insert the line "Pass".  For example',
159                 '  gbr : ice : 3',
160                 '  jap : chi : 3',
161                 '  jap : chi : 3',
162                 '  Pass',
163                 '  gbr : seu : 7',
164                 '  ',
165                 'or',
166                 '  jap : chi : 3',
167                 '  Pass',
168                 '  Pass',
169                 '  ']
170         else:
171             assert mode == 'fortify', mode
172             subject = 'Fortify'
173             body = [
174                 'You can fortify once.  Reply with first line of the',
175                 'body of your email set to',
176                 '  "<source_name> : <target_name> : <number_of_armies>',
177                 'Or, if you choose to pass, either a blank line or',
178                 '"Pass".  For example',
179                 '  gbr : seu : 7',
180                 'or',
181                 '  ',
182                 'or',
183                 '  Pass']
184         self._send_email(world, log, subject, '\n'.join(body))
185         body = self._get_email()
186         if mode == 'fortify':
187             return [self._parse_attack_or_fortify_line(
188                         body.splitlines()[0], mode)]
189         actions = []
190         pass_count = 0
191         for line in body.splitlines():
192             action = self._parse_attack_or_fortify_line(line, mode)
193             if action == None:
194                 pass_count += 1
195                 if pass_count == 2:
196                     break
197             actions.append(action)
198         return actions
199     def _parse_attack_or_fortify_line(self, line, mode):
200         line = line.strip()
201         if line.count(':') == 2:
202             fields = [x.strip() for x in line.split(':')]
203             fields[2] = int(fields[2])
204             return fields
205         elif line.lower() == 'pass' \
206                 or (mode == 'fortify' and len(line) == 0):
207             return None
208     def support_attack(self, world, log, source, target):
209         """Follow up on a conquest by moving additional armies.
210         """
211         subject = 'Support conquest of %s by %s' % (target, source)
212         body = [
213             'You can move up to %d of the %d armies remaining on'
214             % (source.armies - 1, source.armies),
215             '%s to %s following your conquest.'
216             % (source, target),
217             '',
218             'Reply with first line(s) of the body of your email set',
219             'to "<number_of_armies>", or leave the first line blank',
220             'to pass.']
221         self._send_email(world, log, subject, '\n'.join(body))
222         body = self._get_email()
223         text = body.splitlines()[0].strip()
224         if text == '':
225             return 0
226         return int(text)