-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
executable file
·318 lines (256 loc) · 11.4 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#!/usr/bin/env python3
"""VoiceXML battelships
Usage:
main.py [options]
Options:
-h, --help Show this screen.
-p NUMBER, --port=NUMBER Port to listen on [default: 8080]
-l ADDR, --listen=ADDR Address to listen on, by default all interfaces
"""
import datetime
import logging
logging.basicConfig(format="%(created)s:%(levelname)s:%(name)s:%(module)s:%(message)s")
import uuid
import math
import traceback
from docopt import docopt
from lxml import etree
import tornado.concurrent
import tornado.ioloop
import tornado.gen
import tornado.web
import game
class SessionTokenToGame(object):
"""
"""
def __init__(self):
self.session_token_game_dict = {}
self.unmatched_players = set()
self.futures = {}
def get_game(self, player_token):
if player_token not in self.session_token_game_dict:
self.session_token_game_dict[player_token] = None
self.futures[player_token] = tornado.concurrent.Future()
self.unmatched_players.add(player_token)
if self.session_token_game_dict[player_token] is None and len(self.unmatched_players) > 1:
unmatched_player = self.unmatched_players.pop()
if unmatched_player == player_token: # oops, we popped ourselves, let's pop once more
unmatched_player = self.unmatched_players.pop()
logger.debug('Matching players %s and %s' % (unmatched_player, player_token))
self.unmatched_players.discard(player_token)
new_game = game.Game(unmatched_player, player_token)
self.session_token_game_dict[unmatched_player] = game.GameProxy(new_game, unmatched_player)
self.session_token_game_dict[player_token] = game.GameProxy(new_game, player_token)
self.futures[unmatched_player].set_result(self.session_token_game_dict[unmatched_player])
self.futures[player_token].set_result(self.session_token_game_dict[player_token])
return self.futures[player_token]
def get(self, key, default=None):
return self.session_token_game_dict.get(key, default)
def __getitem__(self, key):
return self.session_token_game_dict[key]
def __delitem__(self, key):
del self.session_token_game_dict[key]
del self.futures[key]
@classmethod
def generate_token(self):
return uuid.uuid1().hex
class XMLHandler(tornado.web.RequestHandler):
# called before running any other methods (get, post, etc)
def prepare(self):
self.set_header("Content-Type", "application/xml")
def write_xml(self, tagname="response", **attributes):
xml_doc = etree.ElementTree(etree.Element(tagname, **attributes))
response = etree.tostring(xml_doc, xml_declaration=True)
logger.debug("Response: %s", response)
self.write(response)
# new instances of the request handlers are created on every request,
# so changes to member variables won't be seen across requests
class DialogHandler(XMLHandler):
def get(self):
explicit_feedback = bool(int(self.get_argument('explicit_feedback', '1')))
template_data = {
'token': SessionTokenToGame.generate_token(),
'grid_size': game.Game.GRID_SIZE,
'ships': game.Game.AVAILABLE_SHIPS,
'explicit_feedback': explicit_feedback,
'feedback_timeout': '2s'
}
self.render("dialog.xml", **template_data)
class DynamicDataHandler(XMLHandler):
def prepare(self):
super().prepare()
self.token = self.get_argument('token')
self.out = {}
def write_error(self, status_code, **kwargs):
(exc_type, value, tb) = kwargs['exc_info']
error_data = {
'type': exc_type.__name__,
'message': getattr(value, 'message', '') or getattr(value, 'log_message', ''),
'traceback': traceback.extract_tb(tb),
}
self.render("error.xml", **error_data)
class PollDynamicDataHandler(DynamicDataHandler):
POLL_INTERVAL_SECONDS = 0.5
def check_with_timeout(self, what, as_long_as_returns=None, timeout_seconds=0.0):
# TODO: refactor to be idiomatic wrt. to Tornado!
if timeout_seconds > 0:
times = math.floor(timeout_seconds / self.POLL_INTERVAL_SECONDS)
else:
times = 1
if not times:
times = 1
for _ in range(times):
result = what()
if result != as_long_as_returns:
return result
yield tornado.gen.sleep(self.POLL_INTERVAL_SECONDS)
return result
class GameVanishedException(Exception):
pass
class GameDynamicDataHandler(DynamicDataHandler):
'''
To be used for any handlers that handle the active game.
If the game no longer exists (e.g. because the opponent hung up) a GameVanishedException is raised,
'''
def prepare(self):
super().prepare()
if GAMES.get(self.token):
self.game = GAMES[self.token]
else:
self.set_status(410)
raise GameVanishedException()
class WaitForGameHandler(PollDynamicDataHandler):
@tornado.gen.coroutine
def get(self):
logger.debug("WaitForGame: %s", self.request.query)
timeout_seconds = float(self.get_argument('timeout', 0))
#ready = yield from self.check_with_timeout(lambda: GAMES.get_game(self.token), as_long_as_returns=None,
#timeout_seconds=timeout_seconds)
try:
yield tornado.gen.with_timeout(datetime.timedelta(seconds=timeout_seconds),
GAMES.get_game(self.token))
self.out['ready'] = "true"
except tornado.gen.TimeoutError:
self.out['ready'] = "false"
self.write_xml(**self.out)
class PlaceShipHandler(GameDynamicDataHandler):
def prepare(self):
super(PlaceShipHandler, self).prepare()
if not self.game:
raise Exception('no_game')
def get(self):
logger.debug("GET-PlaceShip: %s", self.request.query)
self._add_next_ship_info_to_output()
self.write_xml(**self.out)
def _add_next_ship_info_to_output(self):
ship = self.game.get_ship_to_place()
if ship:
self.out.update({
'name': ship.name,
'size': str(ship.size),
})
def post(self):
logger.debug("POST-PlaceShip: %s", self.request.body)
self.out.update({
'conflictingcoords': '',
'allowed': 'yes',
})
coord = game.Coord(*self.get_argument('coord'))
map_orientation = {
'horizontally': game.Orientation.horizontal,
'vertically': game.Orientation.vertical,
}
orientation = map_orientation[self.get_argument('orientation')]
try:
self.game.place_ship(top_left_coord=coord, orientation=orientation)
except game.OccupiedFieldsException as e:
self.out['conflictingcoords'] = ' '.join(str(occ) for occ in e.occupied_fields)
self.out['allowed'] = 'conflict'
except IndexError:
self.out['allowed'] = 'beyondfield'
self._add_next_ship_info_to_output()
self.write_xml(**self.out)
class WaitForTurnHandler(GameDynamicDataHandler, PollDynamicDataHandler):
@tornado.gen.coroutine
def get(self):
logger.debug("WaitForTurn: %s", self.request.query)
timeout_seconds = float(self.get_argument('timeout', 0))
try:
yield tornado.gen.with_timeout(datetime.timedelta(seconds=timeout_seconds),
self.game.wait_for_turn())
except tornado.gen.TimeoutError:
pass
#game_state = yield from self.check_with_timeout(lambda: self.game.get_game_state(),
# as_long_as_returns=game.GameState.wait,
# timeout_seconds=timeout_seconds)
game_state = self.game.get_game_state()
self.out['gamestate'] = game_state.name
if game_state in (game.GameState.canPlay, game.GameState.lost):
(coord, what_was_at_coord) = self.game.get_last_opponent_move()
if coord:
self.out['coordhit'] = str(coord)
if what_was_at_coord:
self.out['shiptypehit'] = what_was_at_coord.name
self.out['shippartsleft'] = str(what_was_at_coord.fields_intact)
self.write_xml(**self.out)
class PutCoordHandler(GameDynamicDataHandler):
def get(self):
logger.debug("PutCoord: %s", self.request.query)
coord = game.Coord(*self.get_argument('coord'))
shot_result = self.game.shoot_field(coord)
self.out['shot'] = shot_result.name
self.write_xml(**self.out)
class QuitAppHandler(DynamicDataHandler):
def post(self):
logger.debug("QuitGame: %s", self.request.query)
game = GAMES.get(self.token)
if game:
opponent_token = game.get_opponent()
del GAMES[self.token]
del GAMES[opponent_token]
self.write_xml(**self.out)
class LogHandler(XMLHandler):
def get(self):
logger.info("GET-LOG: %s", self.request.query)
self.write_xml()
def post(self):
logger.info("POST-LOG: %s", self.request.body)
self.write_xml()
class GetShipCoordsHandler(GameDynamicDataHandler):
def get(self):
self.out['owncoords'] = ' '.join(str(coord) for coord in self.game.get_own_ship_coords())
self.out['opponentcoords'] = ' '.join(str(coord) for coord in self.game.get_opponent_ship_coords())
self.write_xml(**self.out)
class WebViewHandler(tornado.web.RequestHandler):
def get(self):
self.render("webview.html", token=SessionTokenToGame.generate_token(),
gridsize=game.Game.GRID_SIZE)
if __name__ == "__main__":
args = docopt(__doc__)
loop = tornado.ioloop.IOLoop.current()
logger = logging.getLogger('battleships-web')
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.FileHandler("battleships.log"))
# debug=True will reload the application if a file is changed,
# and disable the template cache, among other things
app = tornado.web.Application([
(r"/dialog", DialogHandler),
(r"/waitforgame", WaitForGameHandler),
(r"/placeship", PlaceShipHandler),
(r"/waitforturn", WaitForTurnHandler),
(r"/putcoord", PutCoordHandler),
(r"/getshipcoords", GetShipCoordsHandler),
(r"/quitapp", QuitAppHandler),
(r"/log", LogHandler),
(r"/webview", WebViewHandler),
(r"/static/(.*)", tornado.web.StaticFileHandler, {'path': 'static'})
], template_path="templates", debug=True)
port = int(args['--port'])
address = args['--listen'] or ''
app.listen(port, address=address)
logger.debug('Server listening on %s:%s' % (address, port))
GAMES = SessionTokenToGame()
# this starts the event loop. Tornado is single threaded,
# and will sleep until it is notified of new events on one of
# its sockets.
loop.start()