/server.py

https://github.com/bui/taiko-web · Python · 399 lines · 363 code · 9 blank · 27 comment · 133 complexity · 929f6aef21f874f9b1e390b5afc98550 MD5 · raw file

  1. #!/usr/bin/env python
  2. import asyncio
  3. import websockets
  4. import json
  5. import random
  6. import sys
  7. server_status = {
  8. "waiting": {},
  9. "users": [],
  10. "invites": {}
  11. }
  12. consonants = "bcdfghjklmnpqrstvwxyz"
  13. def msgobj(msg_type, value=None):
  14. if value == None:
  15. return json.dumps({"type": msg_type})
  16. else:
  17. return json.dumps({"type": msg_type, "value": value})
  18. def status_event():
  19. value = []
  20. for id, userDiff in server_status["waiting"].items():
  21. value.append({
  22. "id": id,
  23. "diff": userDiff["diff"]
  24. })
  25. return msgobj("users", value)
  26. def get_invite():
  27. return "".join([random.choice(consonants) for x in range(5)])
  28. async def notify_status():
  29. ready_users = [user for user in server_status["users"] if "ws" in user and user["action"] == "ready"]
  30. if ready_users:
  31. sent_msg = status_event()
  32. await asyncio.wait([user["ws"].send(sent_msg) for user in ready_users])
  33. async def connection(ws, path):
  34. # User connected
  35. user = {
  36. "ws": ws,
  37. "action": "ready",
  38. "session": False,
  39. "name": None,
  40. "don": None
  41. }
  42. server_status["users"].append(user)
  43. try:
  44. # Notify user about other users
  45. await ws.send(status_event())
  46. while True:
  47. try:
  48. message = await asyncio.wait_for(ws.recv(), timeout=10)
  49. except asyncio.TimeoutError:
  50. # Keep user connected
  51. pong_waiter = await ws.ping()
  52. try:
  53. await asyncio.wait_for(pong_waiter, timeout=10)
  54. except asyncio.TimeoutError:
  55. # Disconnect
  56. break
  57. except websockets.exceptions.ConnectionClosed:
  58. # Connection closed
  59. break
  60. else:
  61. # Message received
  62. try:
  63. data = json.loads(message)
  64. except json.decoder.JSONDecodeError:
  65. data = {}
  66. action = user["action"]
  67. msg_type = data["type"] if "type" in data else None
  68. value = data["value"] if "value" in data else None
  69. if action == "ready":
  70. # Not playing or waiting
  71. if msg_type == "join":
  72. if value == None:
  73. continue
  74. waiting = server_status["waiting"]
  75. id = value["id"] if "id" in value else None
  76. diff = value["diff"] if "diff" in value else None
  77. user["name"] = value["name"] if "name" in value else None
  78. user["don"] = value["don"] if "don" in value else None
  79. if not id or not diff:
  80. continue
  81. if id not in waiting:
  82. # Wait for another user
  83. user["action"] = "waiting"
  84. user["gameid"] = id
  85. waiting[id] = {
  86. "user": user,
  87. "diff": diff
  88. }
  89. await ws.send(msgobj("waiting"))
  90. else:
  91. # Join the other user and start game
  92. user["name"] = value["name"] if "name" in value else None
  93. user["don"] = value["don"] if "don" in value else None
  94. user["other_user"] = waiting[id]["user"]
  95. waiting_diff = waiting[id]["diff"]
  96. del waiting[id]
  97. if "ws" in user["other_user"]:
  98. user["action"] = "loading"
  99. user["other_user"]["action"] = "loading"
  100. user["other_user"]["other_user"] = user
  101. user["other_user"]["player"] = 1
  102. user["player"] = 2
  103. await asyncio.wait([
  104. ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})),
  105. user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})),
  106. ws.send(msgobj("name", {
  107. "name": user["other_user"]["name"],
  108. "don": user["other_user"]["don"]
  109. })),
  110. user["other_user"]["ws"].send(msgobj("name", {
  111. "name": user["name"],
  112. "don": user["don"]
  113. }))
  114. ])
  115. else:
  116. # Wait for another user
  117. del user["other_user"]
  118. user["action"] = "waiting"
  119. user["gameid"] = id
  120. waiting[id] = {
  121. "user": user,
  122. "diff": diff
  123. }
  124. await ws.send(msgobj("waiting"))
  125. # Update others on waiting players
  126. await notify_status()
  127. elif msg_type == "invite":
  128. if value and "id" in value and value["id"] == None:
  129. # Session invite link requested
  130. invite = get_invite()
  131. server_status["invites"][invite] = user
  132. user["action"] = "invite"
  133. user["session"] = invite
  134. user["name"] = value["name"] if "name" in value else None
  135. user["don"] = value["don"] if "don" in value else None
  136. await ws.send(msgobj("invite", invite))
  137. elif value and "id" in value and value["id"] in server_status["invites"]:
  138. # Join a session with the other user
  139. user["name"] = value["name"] if "name" in value else None
  140. user["don"] = value["don"] if "don" in value else None
  141. user["other_user"] = server_status["invites"][value["id"]]
  142. del server_status["invites"][value["id"]]
  143. if "ws" in user["other_user"]:
  144. user["other_user"]["other_user"] = user
  145. user["action"] = "invite"
  146. user["session"] = value["id"]
  147. user["other_user"]["player"] = 1
  148. user["player"] = 2
  149. await asyncio.wait([
  150. ws.send(msgobj("session", {"player": 2})),
  151. user["other_user"]["ws"].send(msgobj("session", {"player": 1})),
  152. ws.send(msgobj("invite")),
  153. ws.send(msgobj("name", {
  154. "name": user["other_user"]["name"],
  155. "don": user["other_user"]["don"]
  156. })),
  157. user["other_user"]["ws"].send(msgobj("name", {
  158. "name": user["name"],
  159. "don": user["don"]
  160. }))
  161. ])
  162. else:
  163. del user["other_user"]
  164. await ws.send(msgobj("gameend"))
  165. else:
  166. # Session code is invalid
  167. await ws.send(msgobj("gameend"))
  168. elif action == "waiting" or action == "loading" or action == "loaded":
  169. # Waiting for another user
  170. if msg_type == "leave":
  171. # Stop waiting
  172. if user["session"]:
  173. if "other_user" in user and "ws" in user["other_user"]:
  174. user["action"] = "songsel"
  175. await asyncio.wait([
  176. ws.send(msgobj("left")),
  177. user["other_user"]["ws"].send(msgobj("users", []))
  178. ])
  179. else:
  180. user["action"] = "ready"
  181. user["session"] = False
  182. await asyncio.wait([
  183. ws.send(msgobj("gameend")),
  184. ws.send(status_event())
  185. ])
  186. else:
  187. del server_status["waiting"][user["gameid"]]
  188. del user["gameid"]
  189. user["action"] = "ready"
  190. await asyncio.wait([
  191. ws.send(msgobj("left")),
  192. notify_status()
  193. ])
  194. if action == "loading":
  195. if msg_type == "gamestart":
  196. user["action"] = "loaded"
  197. if user["other_user"]["action"] == "loaded":
  198. user["action"] = "playing"
  199. user["other_user"]["action"] = "playing"
  200. sent_msg = msgobj("gamestart")
  201. await asyncio.wait([
  202. ws.send(sent_msg),
  203. user["other_user"]["ws"].send(sent_msg)
  204. ])
  205. elif action == "playing":
  206. # Playing with another user
  207. if "other_user" in user and "ws" in user["other_user"]:
  208. if msg_type == "note"\
  209. or msg_type == "drumroll"\
  210. or msg_type == "branch"\
  211. or msg_type == "gameresults":
  212. await user["other_user"]["ws"].send(msgobj(msg_type, value))
  213. elif msg_type == "songsel" and user["session"]:
  214. user["action"] = "songsel"
  215. user["other_user"]["action"] = "songsel"
  216. sent_msg1 = msgobj("songsel")
  217. sent_msg2 = msgobj("users", [])
  218. await asyncio.wait([
  219. ws.send(sent_msg1),
  220. ws.send(sent_msg2),
  221. user["other_user"]["ws"].send(sent_msg1),
  222. user["other_user"]["ws"].send(sent_msg2)
  223. ])
  224. elif msg_type == "gameend":
  225. # User wants to disconnect
  226. user["action"] = "ready"
  227. user["other_user"]["action"] = "ready"
  228. sent_msg1 = msgobj("gameend")
  229. sent_msg2 = status_event()
  230. await asyncio.wait([
  231. ws.send(sent_msg1),
  232. ws.send(sent_msg2),
  233. user["other_user"]["ws"].send(sent_msg1),
  234. user["other_user"]["ws"].send(sent_msg2)
  235. ])
  236. del user["other_user"]["other_user"]
  237. del user["other_user"]
  238. else:
  239. # Other user disconnected
  240. user["action"] = "ready"
  241. user["session"] = False
  242. await asyncio.wait([
  243. ws.send(msgobj("gameend")),
  244. ws.send(status_event())
  245. ])
  246. elif action == "invite":
  247. if msg_type == "leave":
  248. # Cancel session invite
  249. if user["session"] in server_status["invites"]:
  250. del server_status["invites"][user["session"]]
  251. user["action"] = "ready"
  252. user["session"] = False
  253. if "other_user" in user and "ws" in user["other_user"]:
  254. user["other_user"]["action"] = "ready"
  255. user["other_user"]["session"] = False
  256. sent_msg = status_event()
  257. await asyncio.wait([
  258. ws.send(msgobj("left")),
  259. ws.send(sent_msg),
  260. user["other_user"]["ws"].send(msgobj("gameend")),
  261. user["other_user"]["ws"].send(sent_msg)
  262. ])
  263. else:
  264. await asyncio.wait([
  265. ws.send(msgobj("left")),
  266. ws.send(status_event())
  267. ])
  268. elif msg_type == "songsel" and "other_user" in user:
  269. if "ws" in user["other_user"]:
  270. user["action"] = "songsel"
  271. user["other_user"]["action"] = "songsel"
  272. sent_msg = msgobj(msg_type)
  273. await asyncio.wait([
  274. ws.send(sent_msg),
  275. user["other_user"]["ws"].send(sent_msg)
  276. ])
  277. else:
  278. user["action"] = "ready"
  279. user["session"] = False
  280. await asyncio.wait([
  281. ws.send(msgobj("gameend")),
  282. ws.send(status_event())
  283. ])
  284. elif action == "songsel":
  285. # Session song selection
  286. if "other_user" in user and "ws" in user["other_user"]:
  287. if msg_type == "songsel" or msg_type == "catjump":
  288. # Change song select position
  289. if user["other_user"]["action"] == "songsel" and type(value) is dict:
  290. value["player"] = user["player"]
  291. sent_msg = msgobj(msg_type, value)
  292. await asyncio.wait([
  293. ws.send(sent_msg),
  294. user["other_user"]["ws"].send(sent_msg)
  295. ])
  296. elif msg_type == "crowns" or msg_type == "getcrowns":
  297. if user["other_user"]["action"] == "songsel":
  298. sent_msg = msgobj(msg_type, value)
  299. await asyncio.wait([
  300. user["other_user"]["ws"].send(sent_msg)
  301. ])
  302. elif msg_type == "join":
  303. # Start game
  304. if value == None:
  305. continue
  306. id = value["id"] if "id" in value else None
  307. diff = value["diff"] if "diff" in value else None
  308. if not id or not diff:
  309. continue
  310. if user["other_user"]["action"] == "waiting":
  311. user["action"] = "loading"
  312. user["other_user"]["action"] = "loading"
  313. await asyncio.wait([
  314. ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})),
  315. user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff}))
  316. ])
  317. else:
  318. user["action"] = "waiting"
  319. user["gamediff"] = diff
  320. await user["other_user"]["ws"].send(msgobj("users", [{
  321. "id": id,
  322. "diff": diff
  323. }]))
  324. elif msg_type == "gameend":
  325. # User wants to disconnect
  326. user["action"] = "ready"
  327. user["session"] = False
  328. user["other_user"]["action"] = "ready"
  329. user["other_user"]["session"] = False
  330. sent_msg1 = msgobj("gameend")
  331. sent_msg2 = status_event()
  332. await asyncio.wait([
  333. ws.send(sent_msg1),
  334. user["other_user"]["ws"].send(sent_msg1)
  335. ])
  336. await asyncio.wait([
  337. ws.send(sent_msg2),
  338. user["other_user"]["ws"].send(sent_msg2)
  339. ])
  340. del user["other_user"]["other_user"]
  341. del user["other_user"]
  342. else:
  343. # Other user disconnected
  344. user["action"] = "ready"
  345. user["session"] = False
  346. await asyncio.wait([
  347. ws.send(msgobj("gameend")),
  348. ws.send(status_event())
  349. ])
  350. finally:
  351. # User disconnected
  352. del user["ws"]
  353. del server_status["users"][server_status["users"].index(user)]
  354. if "other_user" in user and "ws" in user["other_user"]:
  355. user["other_user"]["action"] = "ready"
  356. user["other_user"]["session"] = False
  357. await asyncio.wait([
  358. user["other_user"]["ws"].send(msgobj("gameend")),
  359. user["other_user"]["ws"].send(status_event())
  360. ])
  361. del user["other_user"]["other_user"]
  362. if user["action"] == "waiting":
  363. del server_status["waiting"][user["gameid"]]
  364. await notify_status()
  365. elif user["action"] == "invite" and user["session"] in server_status["invites"]:
  366. del server_status["invites"][user["session"]]
  367. port = int(sys.argv[1]) if len(sys.argv) > 1 else 34802
  368. print('Starting server on port %d' % port)
  369. loop = asyncio.get_event_loop()
  370. tasks = asyncio.gather(
  371. websockets.serve(connection, "localhost", port)
  372. )
  373. try:
  374. loop.run_until_complete(tasks)
  375. loop.run_forever()
  376. except KeyboardInterrupt:
  377. print("Stopping server")
  378. def shutdown_exception_handler(loop, context):
  379. if "exception" not in context or not isinstance(context["exception"], asyncio.CancelledError):
  380. loop.default_exception_handler(context)
  381. loop.set_exception_handler(shutdown_exception_handler)
  382. tasks = asyncio.gather(*asyncio.all_tasks(loop=loop), loop=loop, return_exceptions=True)
  383. tasks.add_done_callback(lambda t: loop.stop())
  384. tasks.cancel()
  385. while not tasks.done() and not loop.is_closed():
  386. loop.run_forever()
  387. finally:
  388. if hasattr(loop, "shutdown_asyncgens"):
  389. loop.run_until_complete(loop.shutdown_asyncgens())
  390. loop.close()