aboutsummaryrefslogtreecommitdiff
path: root/schuessel.py
diff options
context:
space:
mode:
Diffstat (limited to 'schuessel.py')
-rw-r--r--schuessel.py184
1 files changed, 184 insertions, 0 deletions
diff --git a/schuessel.py b/schuessel.py
new file mode 100644
index 0000000..5f80fd5
--- /dev/null
+++ b/schuessel.py
@@ -0,0 +1,184 @@
+"""Implementation of a cyberschuessel"""
+import json
+import logging
+import random
+
+import aiohttp
+import aiohttp_jinja2
+
+log = logging.getLogger(__name__)
+
+
+class Schuessel:
+ """Class representing a Salatschüssel and providing an aiohttp handler"""
+
+ def __init__(self):
+ self.phase = 0
+ self.words = []
+ self.guessed = []
+ self.draw_counter = 0
+
+ self.session_counter = 0
+ self.clients = {}
+
+ def reset(self):
+ """Reset the state of the schuessel"""
+ self.phase = 0
+ self.words = []
+ self.guessed = []
+
+ def schuessel_state(self):
+ """Return the client facing state of the schuessel"""
+ return {
+ 'words': len(self.words),
+ 'guessed': len(self.guessed),
+ 'draw_counter': self.draw_counter,
+ 'phase': self.phase
+ }
+
+ def add(self, word):
+ """Add a word to the schuessel"""
+ self.words.append(word)
+
+ def draw(self, last_word=None):
+ """Draw a word from the schuessel
+
+ If a last_word is provided which was the last drawn word this
+ word will be counted as guessed and moved to the guessed word list.
+
+ If the word list is empty transfer the guessed list into the words list
+ and start the next phase"""
+ phase_change = False
+
+ # first draw -> start game
+ if self.phase == 0:
+ self.phase = 1
+
+ # we ignore invalid or outdated last_words
+ elif last_word and last_word['word'] in self.words and last_word[
+ 'draw_counter'] == self.draw_counter:
+ self.guessed.append(last_word['word'])
+ del self.words[self.words.index(last_word['word'])]
+
+ if len(self.words) == 0:
+ # empty schuessel
+ if len(self.guessed) == 0:
+ return None, False
+
+ # move guessed words back into the schuessel
+ tmp = self.guessed
+ self.guessed = self.words
+ self.words = tmp
+ self.phase = self.phase + 1
+ phase_change = True
+
+ self.draw_counter += 1
+
+ word = random.choice(self.words)
+ return word, phase_change
+
+ async def broadcast(self, msg):
+ """broadcast any json encodable object to all open websockets"""
+ log.info('broadcasting %s to %d', msg, len(self.clients.values()))
+ broken_sessions = []
+ for session_id, ws_conn in self.clients.items():
+ try:
+ await ws_conn.send_json(msg)
+ except ConnectionResetError:
+ broken_sessions.append(session_id)
+
+ for session_id in broken_sessions:
+ log.info('remove broken session_id %d', session_id)
+ del self.clients[session_id]
+
+ async def close_all(self):
+ """close all open websockets"""
+ for ws_conn in list(self.clients.values()):
+ await ws_conn.close()
+
+ async def handler(self, request):
+ """aiohttp handler
+
+ This function delivers the webpage or upgrades the connection to a websocket.
+ Then it listens for events on the socket and executes the
+ commands received from the socket.
+
+ The server logic is implemented in this fnction."""
+ ws_conn = aiohttp.web.WebSocketResponse()
+ ws_ready = ws_conn.can_prepare(request)
+ if not ws_ready.ok:
+ return aiohttp_jinja2.render_template('index.html', request, {})
+
+ await ws_conn.prepare(request)
+
+ session_id = self.session_counter
+ self.session_counter += 1
+
+ self.clients[session_id] = ws_conn
+
+ init_msg = {'action': 'init'}
+ init_msg.update(self.schuessel_state())
+ log.info('send init action with %s to %d', init_msg, session_id)
+ try:
+ await ws_conn.send_json(init_msg)
+ except ConnectionResetError:
+ del self.clients[session_id]
+ return ws_conn
+
+ async for msg in ws_conn:
+ log.info('received msg: %s from %d', msg, session_id)
+ if msg.type == aiohttp.WSMsgType.TEXT:
+ data = json.loads(msg.data)
+
+ if data['action'] == 'draw':
+ last_word = data['last_word']
+ new_word, phase_change = self.draw(last_word)
+
+ drawn_msg = {'action': 'drawn'}
+ drawn_msg.update(self.schuessel_state())
+ await self.broadcast(drawn_msg)
+
+ # Make sure to always send the 'draw' msg after the 'drawn'
+ # msg the javascript part expects this behavior
+ log.info('send draw action with %s to %d', new_word,
+ session_id)
+ try:
+ await ws_conn.send_json({
+ 'action':
+ 'draw',
+ 'drawn_word':
+ new_word,
+ 'phase_change':
+ phase_change,
+ 'draw_counter':
+ self.draw_counter,
+ })
+ except ConnectionResetError:
+ del self.clients[session_id]
+ return ws_conn
+
+ elif data['action'] == 'add':
+ new_word = data['word']
+ self.add(new_word)
+ log.info('words in schuessel %s', self.words)
+ await self.broadcast({'action': 'add'})
+
+ elif data['action'] == 'reset':
+ self.reset()
+ reset_msg = {'action': 'init'}
+ reset_msg.update(self.schuessel_state())
+ await self.broadcast(reset_msg)
+
+ else:
+ log.error('unknown action received: %s from %d', data,
+ session_id)
+ elif msg.type == aiohttp.WSMsgType.ERROR:
+ del self.clients[session_id]
+ print(
+ f'{session_id} connection closed with exception {ws_conn.exception()}'
+ )
+
+ del self.clients[session_id]
+ log.info('websocket connection %d closed', session_id)
+
+ return ws_conn