diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:24:33 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:24:33 +0100 |
| commit | 6e3a9c5836aa3bb273529959d84dfae074857c78 (patch) | |
| tree | 51596de780bfa105b02812fefdd3db04550b4c15 /schuessel.py | |
| parent | 550c6ce51c2f95fa0902adc99534fbf11661bb89 (diff) | |
| download | cyberschuessel-6e3a9c5836aa3bb273529959d84dfae074857c78.tar.gz cyberschuessel-6e3a9c5836aa3bb273529959d84dfae074857c78.zip | |
move python code into the project's root
Diffstat (limited to 'schuessel.py')
| -rw-r--r-- | schuessel.py | 184 |
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 |
