From 6e3a9c5836aa3bb273529959d84dfae074857c78 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Mon, 27 Jan 2025 16:24:33 +0100 Subject: move python code into the project's root --- .gitignore | 3 +- cyberschuessel/__init__.py | 2 - cyberschuessel/__pycache__/__init__.cpython-38.pyc | Bin 164 -> 0 bytes cyberschuessel/__pycache__/player.cpython-38.pyc | Bin 1920 -> 0 bytes cyberschuessel/__pycache__/playlist.cpython-38.pyc | Bin 3969 -> 0 bytes cyberschuessel/__pycache__/song.cpython-38.pyc | Bin 3131 -> 0 bytes cyberschuessel/__pycache__/upload.cpython-38.pyc | Bin 1101 -> 0 bytes cyberschuessel/__pycache__/views.cpython-38.pyc | Bin 2283 -> 0 bytes cyberschuessel/schuessel.py | 184 ------------- cyberschuessel/server.py | 54 ---- cyberschuessel/templates/index.html | 301 --------------------- schuessel.py | 184 +++++++++++++ server.py | 54 ++++ setup.py | 20 +- templates/index.html | 301 +++++++++++++++++++++ 15 files changed, 542 insertions(+), 561 deletions(-) delete mode 100644 cyberschuessel/__init__.py delete mode 100644 cyberschuessel/__pycache__/__init__.cpython-38.pyc delete mode 100644 cyberschuessel/__pycache__/player.cpython-38.pyc delete mode 100644 cyberschuessel/__pycache__/playlist.cpython-38.pyc delete mode 100644 cyberschuessel/__pycache__/song.cpython-38.pyc delete mode 100644 cyberschuessel/__pycache__/upload.cpython-38.pyc delete mode 100644 cyberschuessel/__pycache__/views.cpython-38.pyc delete mode 100644 cyberschuessel/schuessel.py delete mode 100755 cyberschuessel/server.py delete mode 100644 cyberschuessel/templates/index.html create mode 100644 schuessel.py create mode 100755 server.py create mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore index d163863..3d43339 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -build/ \ No newline at end of file +build/ +__pycache__/ diff --git a/cyberschuessel/__init__.py b/cyberschuessel/__init__.py deleted file mode 100644 index 21e095a..0000000 --- a/cyberschuessel/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Cyberschuessel a implementation of the game Salatschuessel for the cyberspace""" -__version__ = '0.0.1' diff --git a/cyberschuessel/__pycache__/__init__.cpython-38.pyc b/cyberschuessel/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index d6cfc78..0000000 Binary files a/cyberschuessel/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/__pycache__/player.cpython-38.pyc b/cyberschuessel/__pycache__/player.cpython-38.pyc deleted file mode 100644 index 1f86895..0000000 Binary files a/cyberschuessel/__pycache__/player.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/__pycache__/playlist.cpython-38.pyc b/cyberschuessel/__pycache__/playlist.cpython-38.pyc deleted file mode 100644 index 4add717..0000000 Binary files a/cyberschuessel/__pycache__/playlist.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/__pycache__/song.cpython-38.pyc b/cyberschuessel/__pycache__/song.cpython-38.pyc deleted file mode 100644 index 47d8a5b..0000000 Binary files a/cyberschuessel/__pycache__/song.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/__pycache__/upload.cpython-38.pyc b/cyberschuessel/__pycache__/upload.cpython-38.pyc deleted file mode 100644 index 9254f7a..0000000 Binary files a/cyberschuessel/__pycache__/upload.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/__pycache__/views.cpython-38.pyc b/cyberschuessel/__pycache__/views.cpython-38.pyc deleted file mode 100644 index 376869b..0000000 Binary files a/cyberschuessel/__pycache__/views.cpython-38.pyc and /dev/null differ diff --git a/cyberschuessel/schuessel.py b/cyberschuessel/schuessel.py deleted file mode 100644 index 5f80fd5..0000000 --- a/cyberschuessel/schuessel.py +++ /dev/null @@ -1,184 +0,0 @@ -"""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 diff --git a/cyberschuessel/server.py b/cyberschuessel/server.py deleted file mode 100755 index ec425f7..0000000 --- a/cyberschuessel/server.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -"""aiohttp based webserver running a cyberschuessel app""" -import argparse -import logging -import os - -import jinja2 - -import aiohttp_jinja2 -from aiohttp import web - -from schuessel import Schuessel - - -async def init_app(): - """Create and initialize a new cyberschuessel aiohttp app""" - app = web.Application() - app.on_shutdown.append(shutdown) - - schuessel = Schuessel() - app['schuessel'] = schuessel - - # aiohttp_jinja2.setup(app, - # loader=jinja2.PackageLoader('cyberschuessel', - # 'templates')) - - template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), - "templates") - - aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(template_path)) - - app.router.add_get('/', schuessel.handler) - - return app - - -async def shutdown(app): - """shutdown a cyberschuessel aiohttp app""" - await app['schuessel'].close_all() - - -def main(args): - """init and run a cyberschuessel aiohttp app""" - logging.basicConfig(level=logging.DEBUG) - - app = init_app() - web.run_app(app, port=args.port) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="aiohttp server example") - parser.add_argument('--port', type=int, default=80) - - main(parser.parse_args()) diff --git a/cyberschuessel/templates/index.html b/cyberschuessel/templates/index.html deleted file mode 100644 index 2cfb3a0..0000000 --- a/cyberschuessel/templates/index.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - - - - Cyberschuessel - - - - - - - - - -
- -

-

-

-
-
- - - - -
-
-

- -
- - - - 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 diff --git a/server.py b/server.py new file mode 100755 index 0000000..ec425f7 --- /dev/null +++ b/server.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""aiohttp based webserver running a cyberschuessel app""" +import argparse +import logging +import os + +import jinja2 + +import aiohttp_jinja2 +from aiohttp import web + +from schuessel import Schuessel + + +async def init_app(): + """Create and initialize a new cyberschuessel aiohttp app""" + app = web.Application() + app.on_shutdown.append(shutdown) + + schuessel = Schuessel() + app['schuessel'] = schuessel + + # aiohttp_jinja2.setup(app, + # loader=jinja2.PackageLoader('cyberschuessel', + # 'templates')) + + template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), + "templates") + + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(template_path)) + + app.router.add_get('/', schuessel.handler) + + return app + + +async def shutdown(app): + """shutdown a cyberschuessel aiohttp app""" + await app['schuessel'].close_all() + + +def main(args): + """init and run a cyberschuessel aiohttp app""" + logging.basicConfig(level=logging.DEBUG) + + app = init_app() + web.run_app(app, port=args.port) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="aiohttp server example") + parser.add_argument('--port', type=int, default=80) + + main(parser.parse_args()) diff --git a/setup.py b/setup.py index bfe452b..22ce42b 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,9 @@ -import os -import re - from setuptools import find_packages, setup - -def read_version(): - regexp = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") - init_py = os.path.join(os.path.dirname(__file__), 'cyberschuessel', - '__init__.py') - with open(init_py) as f: - for line in f: - match = regexp.match(line) - if match is not None: - return match.group(1) - else: - msg = 'Cannot find version in cyberschuessel/__init__.py' - raise RuntimeError(msg) - - install_requires = ['aiohttp', 'aiohttp_jinja2'] setup(name='cyberschuessel', - version=read_version(), + version='0.0.1', description='A web server to play the word guessing game Salatschuessel', platforms=['POSIX'], packages=find_packages(), diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2cfb3a0 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,301 @@ + + + + + + + + + + Cyberschuessel + + + + + + + + + +
+ +

+

+

+
+
+ + + + +
+
+

+ +
+ + + + -- cgit v1.2.3