From 06acfdc1e2464d9ced8aef7df6cfeb66fd28f133 Mon Sep 17 00:00:00 2001 From: Matthias Bilger Date: Sun, 14 Mar 2021 13:59:13 +0100 Subject: [PATCH] gamelogic --- README.md | 8 +- pywerwolf/gamelogic.py | 193 ++++++++++++++++++++++++++++++++++++----- pywerwolf/models.py | 5 +- tests/test_game.py | 14 +++ 4 files changed, 192 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 97d7609..b44d350 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ MainPhaseDay --> MainPhaseNight: Some Werewolfes left ```plantuml @startuml(id=TheNight) hide empty description -[*] --> NightPhaseCupin: Game has Cupin -[*] --> NightPhaseMain: no Cupin +[*] --> NightPhaseCupin: -NightPhaseCupin --> InformLovedOnes -InformLovedOnes --> NightPhaseMain +NightPhaseCupin --> InformLovedOnes: Cupin selected +InformLovedOnes --> NightPhaseMain: ack +NightPhaseCupin --> NightPhaseMain : Cupin already selected or No Cupin ' state NightPhaseCupinEnd <> ' NightPhaseCupin --> WaitToContinueCupin: !Cupin ' NightPhaseCupin --> SelectLovedOnes: Cupin diff --git a/pywerwolf/gamelogic.py b/pywerwolf/gamelogic.py index 2e69ff2..d1fbec1 100644 --- a/pywerwolf/gamelogic.py +++ b/pywerwolf/gamelogic.py @@ -1,48 +1,197 @@ from pywerwolf import models +import types import typing as t import uuid +import collections +from dataclasses import dataclass +import logging + +_logger = logging.getLogger(__name__) + +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) + +class GameState(object): + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +GameState.initial = GameState("Initial") +GameState.waiting_for_players = GameState("Waiting for Players") +GameState.start_game = GameState("Start Game") +GameState.night_phase = GameState("Night Phase") +GameState.day_phase = GameState("Day Phase") +GameState.award = GameState("Award") + + +class Input: + pass + +class TimeOut(Input): + pass + +class GameMasterStartGame(Input): + pass + + +class Condition: + def condition(self, input) -> bool: + raise RuntimeError("not implemented") + + +class Transition: + def transition(self, input): + raise RuntimeError("transition not implemented") + + +@dataclass +class TransitionObject: + name: str + inputType: type + transition: Transition + condition: t.List[Condition] + next: GameState + + +class StateMachine: + """Generic Statemachine + + Executes actions on transitions + """ + + transitionTable: t.Dict[GameState, TransitionObject] + state: GameState + data: object + + def __init__( + self, + initialState: GameState, + tranTable: t.Dict[GameState, TransitionObject], + data: object, + ): + self.state = initialState + self.transitionTable = tranTable + self.data = data + _logger.info("Created Statemachine") + + def nextState(self, input): + """Transfer into the next state + + Depending on the + - input + - the current state + - the condition + """ + for transition in self.transitionTable[self.state]: + _logger.debug("Checking Transition: %s", transition.name) + if transition.inputType is not None and not isinstance( + input, transition.inputType + ): + _logger.debug("Input type is required and not given. Expected: %s got %s", transition.inputType, type(input)) + continue + + if transition.condition is not None: + def check_condition(x): + _logger.debug("Check condition %s", x.__doc__) + if not x(input): + _logger.debug("Condition %s not met", x.__doc__) + return True + _logger.debug("Condition %s met", x.__doc__) + return False + + if any(map(check_condition, transition.condition)): + continue + + if transition.transition is not None: + t = transition.transition + t(input) + + self.state = transition.next + _logger.info(f"Transition done to: {self.state}") + break + else: + _logger.warning("No transition performed for input") + + +class Player(object): + def __init__(self, name="Someone"): + self.ready = False + self.name = name class Game(object): - state: models.GameState + statemachine: StateMachine + players: t.List[Player] def __init__(self, game_id: t.Union[str, uuid.UUID] = None): + self.players = [] + self.states = { + GameState.initial: [ + TransitionObject( + name = "Initial 2 Waiting", + inputType=None, + condition=None, + transition=self.create_game_id, + next=GameState.waiting_for_players, + ) + ], + GameState.waiting_for_players: [ + TransitionObject( + name = "Waiting 2 MainPhase", + inputType=GameMasterStartGame, + condition=[self.players_ready], + transition=self.assign_roles, + next=GameState.start_game, + ) + ], + } + if game_id is None: self.create_new_game_state() else: self.load_game_state(game_id) def create_new_game_state(self): + _logger.info("Creating a new game") + self.statemachine = StateMachine(GameState.initial, self.states, self) + self.statemachine.nextState(None) + + def players_ready(self, input) -> bool: + """Players Ready?""" + _logger.debug("[%s] check if players ready", self.game_id) + return all(map(lambda x: x.ready, self.players)) + + def remove_players_ready_flag(self): + _logger.debug("[%s] remove players ready flag", self.game_id) + for p in self.players: + p.ready = False + + def assign_roles(self, _): pass + def start_game(self): + self.statemachine.nextState(GameMasterStartGame()) + + def add_player(self): + self.players.append(Player()) + + def create_game_id(self, *args): + self.game_id = uuid.uuid4() + _logger.info("[%s] created game", self.game_id) + def load_game_state(self, game_id: t.Union[str, uuid.UUID]): + _logger.info("Loading game") pass def valid_game(self): # check if game has been created or loaded return True - def check_victory(self): - """check for a winner of the current game""" - if not self.valid_game(): - return None - - if self.state.currentPhase == models.GamePhase.Award: - return True - - if sum(1 for _ in self.werewolfes) > 0: - if sum(1 for _ in self.villagers) <= 0: - return models.RoleGroup.Werewolfs - pass - else: - return models.RoleGroup.Villagers - - if sum(1 for _ in self.player_alive) == 2: - living = list(self.player_alive) - if living[0].lovedOne == living[1]: - return models.RoleGroup.LovedOnes - - return None @property def player_alive(self): diff --git a/pywerwolf/models.py b/pywerwolf/models.py index 123f5e8..20cb17e 100644 --- a/pywerwolf/models.py +++ b/pywerwolf/models.py @@ -46,11 +46,11 @@ class Roles(enum.Enum): Leaderwolf = enum.auto() @classmethod - def isWerwolf(cls, role: Roles): + def isWerwolf(cls, role: "Roles"): return role in [cls.Werewolf, cls.Leaderwolf] @classmethod - def getGroup(cls, role: Roles): + def getGroup(cls, role: "Roles"): return RoleGroup.Werewolfs if role in [cls.Werewolf, cls.Leaderwolf] else RoleGroup.Villagers class Rules(object): @@ -98,6 +98,7 @@ class Player(object): diedInRound: int accusedBy: "Player" + @property def isWerwolf(self): return Roles.isWerwolf(self.role) diff --git a/tests/test_game.py b/tests/test_game.py index d94e4e9..493349f 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -5,4 +5,18 @@ import pywerwolf.gamelogic as gl def test_game_init(): g = gl.Game() + assert g.game_id is not None + +def test_game_start_next_transition(): + g = gl.Game() + g.start_game() + assert g.game_id is not None + assert g.statemachine.state == gl.GameState.start_game is not None + +def test_game_start_with_player_not_ready(): + g = gl.Game() + g.add_player() + g.start_game() + assert g.game_id is not None + assert g.statemachine.state == gl.GameState.waiting_for_players is not None