"""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