-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgame_state.py
225 lines (200 loc) · 9.11 KB
/
game_state.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
import itertools
import json
import logging
import random
import string
from time import time
from enum import Enum
from tornado.ioloop import IOLoop
from tornado.web import MissingArgumentError
from tornado.websocket import WebSocketHandler
from tables import async_db_call, player_stats
MAX_NAME_LENGTH = 22
_game_map = {} # game_id -> set(websockets)
class GameStates(Enum):
WRITE = 'WRITE'
ASSIGN = 'ASSIGN'
READ = 'READ'
class GameHandler(WebSocketHandler):
def check_origin(self, origin):
return True
def __init__(self, application, request, **kwargs):
super(GameHandler, self).__init__(application, request, **kwargs)
self.name = None
self.game_id = None
self.is_host = False
self.game_state = None
self.open_ts = int(time())
self.supers_written = set()
self.supers_assigned = []
self.supers_received = []
def open(self):
try:
self.name = self.get_argument(name='name')
if not self.name.strip():
raise MissingArgumentError(arg_name='name')
except MissingArgumentError:
send_message(self, error='Please tell us your name.')
else:
game_id = self.get_argument(name='game', default=None)
if game_id is None:
# if enough games are active this can loop infinitely
# but 36^4 = 1.6million which means that is not a
# realistic scenario in any near-term timeframe
while game_id is None or game_id in _game_map:
game_id = u''.join(
random.choice(string.ascii_uppercase + string.digits)
for _ in range(4)
)
_game_map[game_id] = set()
self.is_host = True
else:
# treat all user-typed game_ids as case insensitive
game_id = game_id.upper()
try:
if len(self.name) > MAX_NAME_LENGTH:
raise ValueError('Please restart with a shorter name.')
current_names = {player.name for player in _game_map[game_id]}
if self.name in current_names:
raise ValueError('That name is already taken.')
arbitrary_player = next(iter(_game_map[game_id]), None)
if arbitrary_player and arbitrary_player.game_state is not None:
raise ValueError('This game is already in progress.')
_game_map[game_id].add(self)
self.game_id = game_id
except KeyError:
send_message(self, error='Game {} not found.'.format(game_id))
except ValueError as e:
send_message(self, error=str(e))
else:
broadcast_message(self, message='login', data={
'game':self.game_id,
'players':[player.name for player in _game_map[self.game_id]]
})
async def on_message(self, message):
raw = json.loads(message)
# for now, if we receive an error, log it and quit.
if raw['error']:
logging.error(raw['error'])
return
msg = raw['msg']
data = raw['data']
if msg == 'change_state':
try:
state = GameStates(data['state'].upper())
except (KeyError, ValueError, TypeError):
logging.debug(data)
send_message(self, error='Issues updating game state.')
else:
if state == GameStates.WRITE:
if self.is_host:
for player in _game_map[self.game_id]:
player.game_state = GameStates.WRITE
send_message(player, message='change_state', data={'state': GameStates.WRITE.value})
else:
send_message(self, error='Only the host can start the game.')
elif state == GameStates.ASSIGN and self.is_host and data.get('force', False):
for player in _game_map[self.game_id]:
player.game_state = GameStates.ASSIGN
send_message(player, message='change_state', data={'state': GameStates.ASSIGN.value})
self.send_assigned_supers()
else:
self.game_state = state
waiters = [player for player in _game_map[self.game_id]
if player.game_state == state]
if len(waiters) == len(_game_map[self.game_id]):
broadcast_message(self, message=msg, data=data)
if state == GameStates.ASSIGN:
self.send_assigned_supers()
elif state == GameStates.READ:
self.send_received_supers()
else:
slackers = [player.name for player in _game_map[self.game_id]
if player.game_state != state]
for player in waiters:
send_message(player, message='waiting_on', data={
'players': slackers
})
elif msg == 'write_supers':
if not self.game_state == GameStates.WRITE:
send_message(self, error='Not a valid message in this game state.')
elif not data['super'].strip():
send_message(self, error='Please enter a non-empty value')
else:
self.supers_written.add(data['super'])
elif msg == 'edit_super':
if not self.game_state == GameStates.WRITE:
send_message(self, error='Not a valid message in this game state.')
elif not data['to'].strip() or not data['from'].strip():
send_message(self, error='Please enter a non-empty value')
else:
self.supers_written.remove(data['from'])
self.supers_written.add(data['to'])
elif msg == 'remove_super':
if not self.game_state == GameStates.WRITE:
send_message(self, error='Not a valid message in this game state.')
elif not data['super'].strip():
send_message(self, error='Please enter a non-empty value')
else:
self.supers_written.remove(data['super'])
elif msg == 'assign_super':
if not self.game_state == GameStates.ASSIGN:
send_message(self, error='Not a valid message in this game state.')
else:
try:
recipient = next(player for player in _game_map[self.game_id]
if player.name == data['name'])
recipient.supers_received.append(data['super'])
send_message(recipient, message='assign_super', data={
'super': data['super']
})
except KeyError:
send_message(self, error='Issue assigning award.')
elif msg == 'ping':
send_message(self, message='pong')
else:
raise ValueError('WebSocket Message {} does not exist'.format(msg))
def on_close(self):
IOLoop.current().spawn_callback(self.log_player_state)
if self.game_id in _game_map:
_game_map[self.game_id].discard(self)
if not _game_map[self.game_id]:
del _game_map[self.game_id]
async def log_player_state(self):
async def inner_query(conn):
return await conn.execute(player_stats.insert().values(
open_ts=self.open_ts,
close_ts=int(time()),
state=self.game_state.value if self.game_state else None,
game_id=self.game_id,
))
await async_db_call(inner_query)
def send_assigned_supers(self):
all_supers = list(itertools.chain.from_iterable(
(players.supers_written for players in _game_map[self.game_id])
))
random.shuffle(all_supers)
num_players = len(_game_map[self.game_id])
supers_to_assign = [all_supers[i::num_players] for i in range(num_players)]
for i, player in enumerate(_game_map[self.game_id]):
player.supers_assigned = supers_to_assign[i]
send_message(player, message='assign_supers_list', data={
'supers': supers_to_assign[i]
})
def send_received_supers(self):
for player in _game_map[self.game_id]:
send_message(player, message='read_supers_list', data={
'supers': player.supers_received
})
def send_message(socket, message=None, data=None, error=None):
assert any(v is not None for v in (message, data, error))
msg = json.dumps({
'msg': message,
'data': data,
'error': error,
})
logging.debug(msg)
socket.write_message(msg)
def broadcast_message(socket, message=None, data=None, error=None):
for g in _game_map[socket.game_id]:
send_message(g, message, data, error)