-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.py
265 lines (216 loc) · 8.82 KB
/
server.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
""" Websocket server that simulates live games """
import asyncio
from glob import glob
from sys import stdout
from json import load, dumps
from random import randint
from datetime import datetime
from socket import gethostname, gethostbyname
from websockets import exceptions, serve
from aiohttp import web
from utils import Colours
from question import Question
SOCKET_PORT = 8765
GAME_RUNNING = False
class GameServer:
"""
A representation of a HQTrivia Game socket server.
Provides questions and answers to any client that connects to the socket.
From https://github.com/freshollie/trivia-sim
"""
PORT = 8765
def __init__(self, game_ids):
self.game_ids = game_ids
self.current_game = None
self.current_question = None
self.active = False
self._players = set()
self._socket = None
@staticmethod
def generate_question_event(question, count):
"""
Generate the question JSON response.
:param Question question: Question object for current round
:param int count: Total number of questions
:return: Object representing question event
:rtype: dict
"""
return {"type": "question",
"questionId": question.id,
"question": question.text,
"category": question.category,
"answers": [{'text': answer} for key, answer in question.answers.items()],
"questionNumber": question.number,
"questionCount": count}
@staticmethod
def generate_round_summary_event(question):
"""
After round is over, generate a round summary using random player counts.
:param Question question: Question object for current round
:return: Object representing question summary event
:rtype: dict
"""
answer_counts = [{
'count': randint(0, 1000),
'correct': (key == question.correct),
'answer': question.answers.get(question.correct)
} for key, answer in question.answers.items()]
return {
"type": "questionSummary",
"questionId": question.id,
"advancingPlayersCount": randint(1, 10000),
"eliminatedPlayersCount": randint(1, 10000),
"answerCounts": answer_counts
}
@staticmethod
def generate_game_status_event(current_game, question):
"""
When a player connects, send the game status event.
:param int current_game: Game ID currently in progress
:param Question question: Question object for current round
:return: Object representing game status event
:rtype: dict
"""
return {
"type": "gameStatus",
"prize": "£1,000,000",
"ts": datetime.utcnow().isoformat(),
"showId": f"{current_game.get('showId')}.local",
"questionId": getattr(question, 'id', None),
"questionNumber": getattr(question, 'number', None),
"questionCount": current_game.get('questionCount')
}
async def _broadcast_event(self, event):
"""
Broadcast the given event to all connected players
:param dict event: Event to broadcast
"""
if self._players:
await asyncio.wait([player.send(dumps(event)) for player in self._players])
async def host_game(self):
"""
Hosts a HQTrivia game on the HQTrivia game socket.
"""
self.active = True
game_ids = self.game_ids.split(',')
game_files = [file for file in sorted(glob('games/json/*.json')) if file[27:-5] in game_ids]
if not game_files or set(game_ids) != set([file[27:-5] for file in game_files]):
print(f'Game ID {self.game_ids} not found.')
exit(1)
print(f'Playing Game IDs {game_ids}')
for file in game_files:
game = load(open(file))
self.current_game = game
if not self._players:
print('Waiting for players to connect...\n')
while not self._players:
await asyncio.sleep(2)
for count in reversed(range(5)):
stdout.write(f'\rNext game in {count + 1} seconds')
stdout.flush()
await asyncio.sleep(1)
stdout.write(f"\rStarting Game ID: {self.current_game.get('showId')}\n")
stdout.flush()
game_length = len(game['questions'])
for question_data in game['questions']:
self.current_question = Question(is_replay=True, **question_data)
print(f'\n Round {self.current_question.number}\n-----------')
# Provide a question and wait for it to be answered
print(' Question: ' + Colours.BOLD.value + self.current_question.text + Colours.ENDC.value)
question_event = self.generate_question_event(self.current_question, game_length)
await self._broadcast_event(question_event)
for count in reversed(range(10)):
await asyncio.sleep(1)
stdout.write('\r {} seconds'.format(count))
stdout.flush()
# And then broadcast the answers
stdout.write('\r Answer: ' + Colours.BOLD.value +
f'{self.current_question.correct} - ' +
f'{self.current_question.answers[self.current_question.correct]}\n' +
Colours.ENDC.value)
stdout.flush()
summary_event = GameServer.generate_round_summary_event(self.current_question)
await self._broadcast_event(summary_event)
await asyncio.sleep(3)
print('Games finished')
self.active = False
self.current_game = None
def _register_player(self, player):
self._players.add(player)
def _unregister_player(self, player):
self._players.remove(player)
async def _player_connection(self, socket, _path):
"""
Handles players connecting to the socket and registers them for broadcasts
"""
print('* Player connected')
self._register_player(socket)
status_event = GameServer.generate_game_status_event(self.current_game, self.current_question)
await self._broadcast_event(status_event)
try:
# Keep listen for answers, but ignore them as they are not used.
async for _ in socket:
pass
except exceptions.ConnectionClosed:
pass
finally:
print('* Player disconnected')
self._unregister_player(socket)
async def start(self):
""""
Start the socket listening for player connections
"""
self._socket = await serve(self._player_connection, "0.0.0.0", GameServer.PORT)
async def close(self):
"""
Drain the player connections and close the socket
"""
if self._socket:
self._socket.close()
await self._socket.wait_closed()
class WebServer:
"""
Represents the HQTrivia Web Server
"""
PORT = "8732"
def __init__(self, game_ids):
self._next_game = None
self._game_server = GameServer(game_ids)
self._event_loop = asyncio.get_event_loop()
@staticmethod
def generate_next_game_info(next_show_time):
""" Return the next show time """
return {"nextShowTime": next_show_time, "nextShowPrize": "£1,000,000"}
@staticmethod
def generate_broadcast_info():
""" Return the socket URL """
return {"broadcast": {"socketUrl": f"ws://{Server.get_ip()}:{GameServer.PORT}"}}
async def _serve_game_info(self, _request):
if self._game_server.active:
return web.json_response(WebServer.generate_broadcast_info())
return web.json_response(WebServer.generate_next_game_info(
self._next_game.strftime('%Y-%m-%dT%H:%M:%S.000Z')
))
async def run(self):
"""
Create a websever and broadcast when the game will begin. Start the game when a player connects.
"""
await self._event_loop.create_server(web.Server(self._serve_game_info), "0.0.0.0", WebServer.PORT)
print(f'Web server started on 0.0.0.0:{WebServer.PORT}')
await self._game_server.start()
await self._game_server.host_game()
await self._game_server.close()
class Server:
"""
Server which manages the WebServer and GameServer loops
"""
@staticmethod
def get_ip():
""" Get IP address of the machine """
return gethostbyname(gethostname())
@staticmethod
def run(game_ids):
"""
Create a WebServer instance and run until completion
"""
asyncio.get_event_loop().run_until_complete(WebServer(game_ids).run())