commit ccc9c44ebd48f0e6ce2342b737bc20a438a6ef02 Author: daniel Date: Mon Jun 17 18:17:59 2019 +0200 initial commit backend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3a6164 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + diff --git a/actors.py b/actors.py new file mode 100644 index 0000000..7057725 --- /dev/null +++ b/actors.py @@ -0,0 +1,34 @@ +import chat.response as response + + +class ActorManager: + def __init__(self): + self.actors = dict() + + def add_character(self, char_dict): + name = char_dict['name'] + self.actors[name] = char_dict + print(self.actors) + return response.build_new_character(None, char_dict) + + def remove_actor(self, name): + if not name in self.actors: + return None # TODO FEHLERMELDUNG, CHARAKTER EXISTIERT NICHT + self.actors.pop(name) + return response.build_remove_actor(None, None, name) + + def move_by(self, name, delta): + if not name in self.actors: + return None # TODO FEHLERMELDUNG, CHARAKTER EXISTIERT NICHT + actor = self.actors[name] + actor.tick += delta + return response.build_new_character(None, None, actor) + + def move_to(self, name, tick): + if not name in self.actors: + return None # TODO FEHLERMELDUNG, CHARAKTER EXISTIERT NICHT + actor = self.actors[name] + actor.state = TickState.ACTING.value + actor.tick = tick + return response.build_new_character(None, None, actor) + diff --git a/chat/__init__.py b/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/chat_commands.py b/chat/chat_commands.py new file mode 100644 index 0000000..90f682d --- /dev/null +++ b/chat/chat_commands.py @@ -0,0 +1,79 @@ +import chat.dice as dice +import chat.response as response +from chat.command_exception import CommandException +import re + + +class CommandHandler: + def __init__(self): + self.commands = { + '/roll': self.custom_roll, + '/r': self.custom_roll, + '/whisper': self.whisper, + '/w': self.whisper + } + self.skill_map = {'Tera': {'Akrobatik': 25, + 'Arkane Kunde': 10} + } + + def handle(self, char, content): + message = content.strip('\n') + if message[0] == '.': + return self.skill_roll(char, message) + else: + message = message.split(' ', 1) + if (len(message) > 1) and (message[0] in self.commands): + func = self.commands[message[0]] + r = func(char, message) + return r + return response.build_public_message(char, content) + + def custom_roll(self, char, message): + pattern = '(?P\d+)(?:d|w)(?P\d+)(?P(?:(?:\+|\-)\d+)*)' + command = re.sub('\s+', '', message[1]) + + match = re.match(pattern, command) + if match is None: + return response.build_system_message(char, + 'Ungültige Formatierung des Befehls: {}'.format(' '.join(message))) + + eyes, result = dice.custom_roll(int(match.group('dice_num')), + int(match.group('dice_val')), + match.group('maths')) + return response.build_dice_roll(char, ' '.join(message), eyes, result) + + def skill_roll(self, char, message): + print(char) + print(message) + print(self.skill_map) + if char not in self.skill_map: + return response.build_system_message(char, 'Ungültiger Charakter: {}'.format(char)) + + pattern = '(?P[a-zA-ZäöüÄÖÜß .&]+)(?P(?:\s*(?:\+|\-)\s*\d+)*)(?P[nrs]?)' + match = re.match(pattern, message) + if match is None: + return response.build_system_message(char, + 'Unültige Formatierung des Befehls: {}'.format(message)) + + skills = self.skill_map[char] + if match.group('skill')[1:] not in skills: + return response.build_system_message(char, + '{} hat die Fertigkeit {} nicht.'.format(char, match.group('skill'))) + + skill = skills[match.group('skill')[1:]] + type_ = match.group('type') + if len(type_) == 0 or type_ == 'n': + type_ = dice.RollTypes.NORMAL + elif type_ == 'r': + type_ = dice.RollTypes.RISKY + else: + type_ = dice.RollTypes.SAFE + eyes, result = dice.skill_roll(skill, + match.group('maths'), + type_) + print(eyes, result) + return response.build_dice_roll(char, message, eyes, result) + + def whisper(self, char, message): + recipient, message = message.split(' ', 1) + return response.build_private_message(char, message, recipient) diff --git a/chat/command_exception.py b/chat/command_exception.py new file mode 100644 index 0000000..525311e --- /dev/null +++ b/chat/command_exception.py @@ -0,0 +1,3 @@ +class CommandException(Exception): + def __init__(self, *args): + super().__init__(*args) diff --git a/chat/dice.py b/chat/dice.py new file mode 100644 index 0000000..9425bf7 --- /dev/null +++ b/chat/dice.py @@ -0,0 +1,47 @@ +from enum import auto, Enum +import random + + +class RollTypes(Enum): + NORMAL = auto() + RISKY = auto() + SAFE = auto() + + +def skill_roll(skill, modifiers, type_=RollTypes.NORMAL): + eyes, result = roll_dice(type_) + result += skill + if len(modifiers) > 0: + result += eval(modifiers) + + return eyes, result + + +def custom_roll(n, eyes, modifiers=None): + eyes = n_random_eyes(n, eyes) + result = sum(eyes) + + if len(modifiers) > 0: + result += eval(modifiers) + + return eyes, result + + +def roll_dice(type_): + if type_ is RollTypes.NORMAL: + eyes = n_random_eyes(2) + result = sum(eyes) + elif type_ is RollTypes.RISKY: + eyes = n_random_eyes(4) + eyes.sort(reverse=True) + result = sum(eyes[:2]) + else: + eyes = n_random_eyes(2) + result = max(eyes) + return eyes, result + + +def n_random_eyes(n, eyes=10): + return [random.randint(1, eyes) for _ in range(n)] + + diff --git a/chat/response.py b/chat/response.py new file mode 100644 index 0000000..50db7e3 --- /dev/null +++ b/chat/response.py @@ -0,0 +1,67 @@ +from abc import ABC +from dataclasses import dataclass +from events import Events + + +@dataclass +class Response(ABC): + sender: str + event: Events + + def to_json(self) -> dict: + d = self.__dict__ + d.pop('event') + return d + + +@dataclass +class NewCharacter(Response): + character: dict + + +@dataclass +class RemoveActor(Response): + message: str + name: str + + +@dataclass +class PublicMessage(Response): + message: str + + +@dataclass +class PrivateMessage(Response): + message: str + recipient: str + + +@dataclass +class DiceRoll(Response): + message: str + dice: list + result: int + + +def build_dice_roll(sender, message, dice, result): + return DiceRoll(sender, Events.DICE_ROLL, message, dice, result) + + +def build_new_character(sender, char_dict): + return NewCharacter(sender, Events.NEW_CHARACTER, char_dict) + + +def build_remove_actor(sender, message, name): + return RemoveActor(sender, Events.REMOVE_ACTOR, message, name) + + +def build_private_message(sender, message, recipient): + return PrivateMessage(sender, Events.PRIVATE_CHAT, message, recipient) + + +def build_public_message(sender, message): + return PublicMessage(sender, Events.PUBLIC_CHAT, message) + + +def build_system_message(user, message): + return PrivateMessage('System', Events.SYSTEM_MESSAGE, message, user) diff --git a/events.py b/events.py new file mode 100644 index 0000000..5a84689 --- /dev/null +++ b/events.py @@ -0,0 +1,14 @@ +from enum import Enum + +class Events(Enum): + PUBLIC_CHAT = 'public message' + PRIVATE_CHAT = 'private message' + SYSTEM_MESSAGE = 'system message' + DICE_ROLL = 'dice roll' + NEW_CHARACTER = 'new character' + REMOVE_ACTOR = 'remove actor' + ACTOR_ADDED = 'actor added' + USER_EDITED = 'user edited' + USER_ADDED = 'user added' + USER_REMOVED = 'user removed' + diff --git a/main.py b/main.py new file mode 100755 index 0000000..89d6c13 --- /dev/null +++ b/main.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +# WS server example + +import eventlet +import socketio + +from actors import ActorManager +from events import Events + +from chat.chat_commands import CommandHandler +from users.users import UserManager + + +sio = socketio.Server() +app = socketio.WSGIApp(sio) +actor_manager = ActorManager() +command_handler = CommandHandler() +user_manager = UserManager() + + +@sio.on('connect') +def connect(sid, environ): + print("CONNECTED: {}".format(sid)) + for user in user_manager.get_users(): + sio.emit(Events.USER_ADDED.value, user, room=sid) + +@sio.on('disconnect') +def disconnect(sid): + print("DISCONNECTED: {}".format(sid)) + name = user_manager.remove_user(sid) + sio.emit(Events.USER_REMOVED.value, name) + +@sio.on(Events.PUBLIC_CHAT.value) +def message(sid, data): + # print(data) + response = command_handler.handle(data['char'], data['message']) + # print(response) + # print("\n") + sio.emit(response.event. + value, response.to_json()) + + +@sio.on(Events.NEW_CHARACTER.value) +def message(sid, data): + print("INCOMING\n", data) + actor_manager.add_character(data) + sio.emit(Events.NEW_CHARACTER.value, data) + +@sio.on(Events.USER_EDITED.value) +def message(sid, data): + print("USER EDITED: ", sid, data) + if user_manager.has_user(data['old']): + user_manager.remove_user(sid) + sio.emit(Events.USER_REMOVED.value, data['old']) + user_manager.add_user(data['new']['characterName'], sid) + d = data['new'] + d['characterLoaded'] = True + d['skills'] = 'Akrobatik\nArkane Kunde\nSeefahrt' + sio.emit(Events.USER_EDITED.value, d, room=sid) + sio.emit(Events.USER_ADDED.value, data['new']['characterName']) + + +if __name__ == '__main__': + eventlet.wsgi.server(eventlet.listen(('', 3101)), app) + diff --git a/test.py b/test.py new file mode 100644 index 0000000..99ace50 --- /dev/null +++ b/test.py @@ -0,0 +1,3 @@ +from chat import chat_commands + +print(chat_commands) diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/sheet_reader.py b/users/sheet_reader.py new file mode 100644 index 0000000..e69de29 diff --git a/users/users.py b/users/users.py new file mode 100644 index 0000000..6dccfe3 --- /dev/null +++ b/users/users.py @@ -0,0 +1,24 @@ +import chat.response as response + + +class UserManager: + def __init__(self): + self.users = dict() + self.skill_map = dict() + + def add_user(self, name, sid): + self.users[sid] = name + print("ADDED USER, USERS NOW IS:", self.users) + return name + + def remove_user(self, sid): + print("REMOVING USER:", sid) + if sid in self.users: + return self.users.pop(sid) + + def has_user(self, name): + return name in self.users.values() + + def get_users(self): + return self.users.values() +