From 6ec3348f0476f01b631b71d64a20d3679c01290a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Luko=C5=A5ka?= Date: Tue, 29 Oct 2024 08:39:12 +0100 Subject: [PATCH] GamePhaseControler basic implementation Only partial tests --- .github/workflows/python-app.yml | 66 +++++ .gitignore | 2 + .pylintrc | 6 + Makefile | 23 ++ stone_age/__init__.py | 0 stone_age/factories.py | 0 stone_age/game_board/__init__.py | 0 stone_age/game_board/factories.py | 0 stone_age/game_board/interfaces.py | 1 + stone_age/game_board/simple_types.py | 0 stone_age/game_phase_controller/__init__.py | 0 stone_age/game_phase_controller/factories.py | 0 .../game_phase_controller.py | 274 ++++++++++++++++++ stone_age/game_phase_controller/interfaces.py | 38 +++ .../game_phase_controller/simple_types.py | 11 + stone_age/interfaces.py | 35 +++ stone_age/player_board/__init__.py | 0 stone_age/player_board/factories.py | 0 stone_age/player_board/interfaces.py | 1 + stone_age/player_board/simple_types.py | 0 stone_age/simple_types.py | 98 +++++++ test/__init__.py | 0 test/game_board/__init__.py | 0 test/game_board/test_integration/__init__.py | 0 test/game_phase_controller/__init__.py | 0 .../test_game_phase_contoller.py | 179 ++++++++++++ .../test_integration/__init__.py | 0 test/player_board/__init__.py | 0 .../player_board/test_integration/__init__.py | 0 test/test_integration/__init__.py | 0 30 files changed, 734 insertions(+) create mode 100644 .github/workflows/python-app.yml create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 Makefile create mode 100644 stone_age/__init__.py create mode 100644 stone_age/factories.py create mode 100644 stone_age/game_board/__init__.py create mode 100644 stone_age/game_board/factories.py create mode 100644 stone_age/game_board/interfaces.py create mode 100644 stone_age/game_board/simple_types.py create mode 100644 stone_age/game_phase_controller/__init__.py create mode 100644 stone_age/game_phase_controller/factories.py create mode 100644 stone_age/game_phase_controller/game_phase_controller.py create mode 100644 stone_age/game_phase_controller/interfaces.py create mode 100644 stone_age/game_phase_controller/simple_types.py create mode 100644 stone_age/interfaces.py create mode 100644 stone_age/player_board/__init__.py create mode 100644 stone_age/player_board/factories.py create mode 100644 stone_age/player_board/interfaces.py create mode 100644 stone_age/player_board/simple_types.py create mode 100644 stone_age/simple_types.py create mode 100644 test/__init__.py create mode 100644 test/game_board/__init__.py create mode 100644 test/game_board/test_integration/__init__.py create mode 100644 test/game_phase_controller/__init__.py create mode 100644 test/game_phase_controller/test_game_phase_contoller.py create mode 100644 test/game_phase_controller/test_integration/__init__.py create mode 100644 test/player_board/__init__.py create mode 100644 test/player_board/test_integration/__init__.py create mode 100644 test/test_integration/__init__.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..c61aad8 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,66 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python application + +on: + push: + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + mypy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + - name: Run mypy + run: | + mypy stone_age --strict + mypy test --strict + + lint: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Run lint + run: | + pylint stone_age/ + pylint test/ + + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Tests + run: | + python3 -m unittest + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c4323f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.mypy_cache +__pycache__ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..ce94ab8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,6 @@ +[MASTER] +disable = missing-module-docstring, missing-class-docstring, missing-function-docstring, too-few-public-methods + +[SIMILARITIES] +ignore-imports=yes +min-similarity-lines=6 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8b9e18c --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +check_and_test: FORCE + mypy stone_age --strict + mypy test --strict + python3 -m unittest + +lint: FORCE + pylint stone_age/ + pylint test/ + +format: FORCE + autopep8 -i stone_age/*.py + autopep8 -i stone_age/player_board/*.py + autopep8 -i stone_age/game_board/*.py + autopep8 -i stone_age/game_phase_controller/*.py + autopep8 -i test/*.py + autopep8 -i test/test_integration/*.py + autopep8 -i test/player_board/*.py + autopep8 -i test/game_board/*.py + autopep8 -i test/game_phase_controller/*.py + autopep8 -i test/player_board/test_integration/*.py + autopep8 -i test/game_board/test_integration/*.py + autopep8 -i test/game_phase_controller/test_integration/*.py +FORCE: ; diff --git a/stone_age/__init__.py b/stone_age/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/factories.py b/stone_age/factories.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_board/__init__.py b/stone_age/game_board/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_board/factories.py b/stone_age/game_board/factories.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_board/interfaces.py b/stone_age/game_board/interfaces.py new file mode 100644 index 0000000..d84d8dd --- /dev/null +++ b/stone_age/game_board/interfaces.py @@ -0,0 +1 @@ +# pylint: disable=unused-argument, duplicate-code diff --git a/stone_age/game_board/simple_types.py b/stone_age/game_board/simple_types.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_phase_controller/__init__.py b/stone_age/game_phase_controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_phase_controller/factories.py b/stone_age/game_phase_controller/factories.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/game_phase_controller/game_phase_controller.py b/stone_age/game_phase_controller/game_phase_controller.py new file mode 100644 index 0000000..f0c3fc7 --- /dev/null +++ b/stone_age/game_phase_controller/game_phase_controller.py @@ -0,0 +1,274 @@ +from __future__ import annotations +from typing import Iterable, Mapping, Optional, Any +import json + +from stone_age.interfaces import InterfaceGamePhaseController +from stone_age.simple_types import PlayerOrder, Location, Effect, ActionResult, HasAction +from stone_age.game_phase_controller.interfaces import InterfaceGamePhaseState +from stone_age.game_phase_controller.simple_types import GamePhase + + +class GamePhaseController(InterfaceGamePhaseController): + _dispatchers: Mapping[GamePhase, InterfaceGamePhaseState] + _round_starting_player: PlayerOrder + _current_player: PlayerOrder + _current_player_taking_reward: Optional[PlayerOrder] + _game_phase: GamePhase + + def __init__(self, dispatchers: Mapping[GamePhase, InterfaceGamePhaseState], + starting_player: PlayerOrder): + self._round_starting_player = starting_player + self._current_player = starting_player + self._current_player_taking_reward = None + self._dispatchers = dict(dispatchers) + self._game_phase = GamePhase.PLACE_FIGURES + + def _check_players_turn(self, player: PlayerOrder) -> bool: + match self._game_phase: + case GamePhase.PLACE_FIGURES | GamePhase.MAKE_ACTION | GamePhase.WAITING_FOR_TOOL_USE: + return player == self._current_player + case GamePhase.ALL_PLAYERS_TAKE_A_REWARD: + assert self._current_player_taking_reward is not None + return player == self._current_player_taking_reward + case GamePhase.FEED_TRIBE: + return True + case GamePhase.NEW_ROUND: + return False + case _: # GAME_END + assert False + + def _progress_state_after_succesfull_action(self) -> None: + match self._game_phase: + case GamePhase.PLACE_FIGURES | GamePhase.FEED_TRIBE: + self._current_player = self._current_player.forward() + return + case GamePhase.MAKE_ACTION | GamePhase.WAITING_FOR_TOOL_USE: + return + case GamePhase.ALL_PLAYERS_TAKE_A_REWARD: + assert self._current_player_taking_reward is not None + self._current_player_taking_reward = self._current_player_taking_reward.forward() + return + case GamePhase.NEW_ROUND: + self._game_phase = GamePhase.PLACE_FIGURES + self._round_starting_player = self._round_starting_player.forward() + self._current_player = self._round_starting_player + case _: # GAME_END + assert False + + def _progress_state_after_no_action_possible(self) -> None: + match self._game_phase: + case GamePhase.PLACE_FIGURES | GamePhase.FEED_TRIBE | GamePhase.MAKE_ACTION: + self._current_player = self._current_player.forward() + return + case GamePhase.ALL_PLAYERS_TAKE_A_REWARD | GamePhase.WAITING_FOR_TOOL_USE: + self._current_player_taking_reward = None + self._game_phase = GamePhase.MAKE_ACTION + return + case GamePhase.NEW_ROUND: + self._game_phase = GamePhase.GAME_END + return + case _: # GAME_END + assert False + + def _progress_state_after_no_action_possible_by_any_player(self) -> None: + match self._game_phase: + case GamePhase.PLACE_FIGURES: + self._current_player = self._round_starting_player + self._game_phase = GamePhase.MAKE_ACTION + return + case GamePhase.MAKE_ACTION: + self._current_player = self._round_starting_player + self._game_phase = GamePhase.FEED_TRIBE + return + case GamePhase.FEED_TRIBE: + self._current_player = self._round_starting_player + self._game_phase = GamePhase.NEW_ROUND + case _: # NEW_ROUND, WAITING_FOR_TOOL_USE, ALL_PLAYERS_TAKE_A_REWARD, GAME_END + assert False + + def _progress_state_tool_use(self) -> None: + match self._game_phase: + case GamePhase.MAKE_ACTION: + self._game_phase = GamePhase.WAITING_FOR_TOOL_USE + return + case _: + assert False + + def _progress_state_all_players_take_a_reward(self) -> None: + match self._game_phase: + case GamePhase.MAKE_ACTION: + self._game_phase = GamePhase.ALL_PLAYERS_TAKE_A_REWARD + self._current_player_taking_reward = self._current_player + return + case _: + assert False + + def _try_to_do_further_actions(self) -> None: + first_unsuccesful_player: Optional[PlayerOrder] = None + while True: + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + player: PlayerOrder = self._current_player_taking_reward or self._current_player + if self._game_phase != GamePhase.GAME_END and first_unsuccesful_player == player: + self._progress_state_after_no_action_possible_by_any_player() + first_unsuccesful_player = None + continue + action_result: HasAction = dispatcher.try_to_make_automatic_action( + player) + match action_result: + case HasAction.WAITING_FOR_PLAYER_ACTION: + first_unsuccesful_player = None + return + case HasAction.AUTOMATIC_ACTION_DONE: + first_unsuccesful_player = None + self._progress_state_after_succesfull_action() + case HasAction.NO_ACTION_POSSIBLE: + if first_unsuccesful_player is None: + first_unsuccesful_player = player + self._progress_state_after_no_action_possible() + continue + case _: + assert False + + def place_figures(self, player: PlayerOrder, location: Location, figures_count: int) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.place_figures( + player, location, figures_count) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def make_action(self, player: PlayerOrder, location: Location, + input_resources: Iterable[Effect], + output_resources: Iterable[Effect]) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.make_action( + player, location, input_resources, output_resources) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case ActionResult.ACTION_DONE_WAIT_FOR_TOOL_USE: + self._progress_state_tool_use() + self._try_to_do_further_actions() + return True + case ActionResult.ACTION_DONE_ALL_PLAYERS_TAKE_A_REWARD: + self._progress_state_all_players_take_a_reward() + self._try_to_do_further_actions() + return True + case _: + assert False + + def skip_action(self, player: PlayerOrder, location: Location) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.skip_action(player, location) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def use_tools(self, player: PlayerOrder, tool_index: int) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.use_tools(player, tool_index) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def no_more_tools_this_throw(self, player: PlayerOrder) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.no_more_tools_this_throw( + player) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_no_action_possible() + self._try_to_do_further_actions() + return True + case _: + assert False + + def feed_tribe(self, player: PlayerOrder, resources: Iterable[Effect]) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.feed_tribe(player, resources) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def do_not_feed_this_turn(self, player: PlayerOrder) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.do_not_feed_this_turn(player) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def make_all_players_take_a_reward_choice(self, player: PlayerOrder, reward: Effect) -> bool: + if not self._check_players_turn(player): + return False + dispatcher: InterfaceGamePhaseState = self._dispatchers[self._game_phase] + action_result: ActionResult = dispatcher.make_all_players_take_a_reward_choice( + player, reward) + match action_result: + case ActionResult.FAILURE: + return False + case ActionResult.ACTION_DONE: + self._progress_state_after_succesfull_action() + self._try_to_do_further_actions() + return True + case _: + assert False + + def state(self) -> str: + state: Any = { + "game phase": str(self._game_phase), + "round starting player": self._round_starting_player.order, + "current_player": self._current_player.order, + "player taking a reward": "None" if self._current_player_taking_reward is None + else self._current_player_taking_reward.order + } + return json.dumps(state) diff --git a/stone_age/game_phase_controller/interfaces.py b/stone_age/game_phase_controller/interfaces.py new file mode 100644 index 0000000..6b7cf8a --- /dev/null +++ b/stone_age/game_phase_controller/interfaces.py @@ -0,0 +1,38 @@ +# pylint: disable=unused-argument, duplicate-code +from __future__ import annotations +from typing import Iterable +from stone_age.simple_types import PlayerOrder, Location, Effect +from stone_age.simple_types import HasAction, ActionResult + + +class InterfaceGamePhaseState: + def place_figures(self, player: PlayerOrder, location: Location, + figures_count: int) -> ActionResult: + assert False + + def make_action(self, player: PlayerOrder, location: Location, + input_resources: Iterable[Effect], + output_resources: Iterable[Effect]) -> ActionResult: + assert False + + def skip_action(self, player: PlayerOrder, location: Location) -> ActionResult: + assert False + + def use_tools(self, player: PlayerOrder, tool_index: int) -> ActionResult: + assert False + + def no_more_tools_this_throw(self, player: PlayerOrder) -> ActionResult: + assert False + + def feed_tribe(self, player: PlayerOrder, resources: Iterable[Effect]) -> ActionResult: + assert False + + def do_not_feed_this_turn(self, player: PlayerOrder) -> ActionResult: + assert False + + def make_all_players_take_a_reward_choice(self, player: PlayerOrder, + reward: Effect) -> ActionResult: + assert False + + def try_to_make_automatic_action(self, player: PlayerOrder) -> HasAction: + assert False diff --git a/stone_age/game_phase_controller/simple_types.py b/stone_age/game_phase_controller/simple_types.py new file mode 100644 index 0000000..35c7981 --- /dev/null +++ b/stone_age/game_phase_controller/simple_types.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class GamePhase(Enum): + PLACE_FIGURES = 1 + MAKE_ACTION = 2 + FEED_TRIBE = 3 + NEW_ROUND = 4 + WAITING_FOR_TOOL_USE = 5 + ALL_PLAYERS_TAKE_A_REWARD = 6 + GAME_END = 7 diff --git a/stone_age/interfaces.py b/stone_age/interfaces.py new file mode 100644 index 0000000..ac1a500 --- /dev/null +++ b/stone_age/interfaces.py @@ -0,0 +1,35 @@ +# pylint: disable=unused-argument, duplicate-code +from __future__ import annotations +from typing import Iterable +from stone_age.simple_types import PlayerOrder, Location, Effect + + +class InterfaceGamePhaseController: + def place_figures(self, player: PlayerOrder, location: Location, figures_count: int) -> bool: + assert False + + def make_action(self, player: PlayerOrder, location: Location, + input_resources: Iterable[Effect], + output_resources: Iterable[Effect]) -> bool: + assert False + + def skip_action(self, player: PlayerOrder, location: Location) -> bool: + assert False + + def use_tools(self, player: PlayerOrder, tool_index: int) -> bool: + assert False + + def no_more_tools_this_throw(self, player: PlayerOrder) -> bool: + assert False + + def feed_tribe(self, player: PlayerOrder, resources: Iterable[Effect]) -> bool: + assert False + + def do_not_feed_this_turn(self, player: PlayerOrder) -> bool: + assert False + + def make_all_players_take_a_reward_choice(self, player: PlayerOrder, reward: Effect) -> bool: + assert False + + def state(self) -> str: + assert False diff --git a/stone_age/player_board/__init__.py b/stone_age/player_board/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/player_board/factories.py b/stone_age/player_board/factories.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/player_board/interfaces.py b/stone_age/player_board/interfaces.py new file mode 100644 index 0000000..d84d8dd --- /dev/null +++ b/stone_age/player_board/interfaces.py @@ -0,0 +1 @@ +# pylint: disable=unused-argument, duplicate-code diff --git a/stone_age/player_board/simple_types.py b/stone_age/player_board/simple_types.py new file mode 100644 index 0000000..e69de29 diff --git a/stone_age/simple_types.py b/stone_age/simple_types.py new file mode 100644 index 0000000..daceac2 --- /dev/null +++ b/stone_age/simple_types.py @@ -0,0 +1,98 @@ +from __future__ import annotations +from enum import Enum + + +class PlayerOrder: + _order: int + _players: int + + def __init__(self, order: int, players: int): + self._order = order + self._players = players + + @property + def order(self) -> int: + return self._order + + @property + def players(self) -> int: + return self._players + + def forward(self) -> PlayerOrder: + forward: int = (self._order+1) % self._players + return PlayerOrder(forward, self._players) + + def __str__(self) -> str: + return str(self._order) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PlayerOrder): + return NotImplemented + assert self._players == other.players + return self._order == other.order + + +class Location(Enum): + TOOL_MAKER = 1 + HUT = 2 + FIELD = 3 + HUNTING_GROUNDS = 4 + FOREST = 5 + CLAY_MOUND = 6 + QUARY = 7 + RIVER = 8 + CIVILISATION_CARD1 = 11 + CIVILISATION_CARD2 = 12 + CIVILISATION_CARD3 = 13 + CIVILISATION_CARD4 = 14 + BUILDING_TILE1 = 21 + BUILDING_TILE2 = 22 + BUILDING_TILE3 = 23 + BUILDING_TILE4 = 24 + + +class Effect(Enum): + FOOD = 1 + WOOD = 2 + CLAY = 3 + STONE = 4 + GOLD = 5 + TOOL = 6 + FIELD = 7 + BUILDING = 8 + ONE_TIME_TOOL2 = 12 + ONE_TIME_TOOL3 = 13 + ONE_TIME_TOOL4 = 14 + + @staticmethod + def is_resource(effect: Effect) -> bool: + resources = (Effect.WOOD, Effect.CLAY, Effect.STONE, Effect.GOLD) + return effect in resources + + @staticmethod + def is_resource_or_food(effect: Effect) -> bool: + return Effect.is_resource(effect) or effect == Effect.FOOD + + @staticmethod + def points(effect: Effect) -> int: + points_table = { + Effect.FOOD: 2, + Effect.WOOD: 3, + Effect.CLAY: 4, + Effect.STONE: 5, + Effect.GOLD: 6, + } + return points_table.get(effect, 0) + + +class ActionResult(Enum): + FAILURE = 1 + ACTION_DONE = 2 + ACTION_DONE_WAIT_FOR_TOOL_USE = 3 + ACTION_DONE_ALL_PLAYERS_TAKE_A_REWARD = 4 + + +class HasAction(Enum): + WAITING_FOR_PLAYER_ACTION = 1 + AUTOMATIC_ACTION_DONE = 2 + NO_ACTION_POSSIBLE = 3 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/game_board/__init__.py b/test/game_board/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/game_board/test_integration/__init__.py b/test/game_board/test_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/game_phase_controller/__init__.py b/test/game_phase_controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/game_phase_controller/test_game_phase_contoller.py b/test/game_phase_controller/test_game_phase_contoller.py new file mode 100644 index 0000000..a8f29fd --- /dev/null +++ b/test/game_phase_controller/test_game_phase_contoller.py @@ -0,0 +1,179 @@ +# pylint: disable=too-many-instance-attributes +from typing import Iterable, Any +import unittest +import json +from stone_age.game_phase_controller.game_phase_controller import GamePhaseController +from stone_age.game_phase_controller.interfaces import InterfaceGamePhaseState +from stone_age.game_phase_controller.simple_types import GamePhase +from stone_age.simple_types import ActionResult, HasAction, PlayerOrder, Location, Effect + + +class StateMock(InterfaceGamePhaseState): + expected_action_results: list[ActionResult] + expected_has_action: list[HasAction] + + def __init__(self) -> None: + self.expected_action_results = [] + self.expected_has_action = [] + + def place_figures(self, player: PlayerOrder, location: Location, + figures_count: int) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def make_action(self, player: PlayerOrder, location: Location, + input_resources: Iterable[Effect], + output_resources: Iterable[Effect]) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def skip_action(self, player: PlayerOrder, location: Location) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def use_tools(self, player: PlayerOrder, tool_index: int) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def no_more_tools_this_throw(self, player: PlayerOrder) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def feed_tribe(self, player: PlayerOrder, resources: Iterable[Effect]) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def do_not_feed_this_turn(self, player: PlayerOrder) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def make_all_players_take_a_reward_choice(self, player: PlayerOrder, + reward: Effect) -> ActionResult: + assert self.expected_action_results + return self.expected_action_results.pop(0) + + def try_to_make_automatic_action(self, player: PlayerOrder) -> HasAction: + assert self.expected_has_action + return self.expected_has_action.pop(0) + + +class TestGamePhaseController(unittest.TestCase): + def setUp(self) -> None: + self.place_figures_state: StateMock = StateMock() + self.make_action_state: StateMock = StateMock() + self.feed_tribe_state: StateMock = StateMock() + self.new_round_state: StateMock = StateMock() + self.waiting_for_tool_use_state: StateMock = StateMock() + self.all_players_take_a_reward_state: StateMock = StateMock() + self.game_end_state: StateMock = StateMock() + + dispatchers = { + GamePhase.PLACE_FIGURES: self.place_figures_state, + GamePhase.MAKE_ACTION: self.make_action_state, + GamePhase.FEED_TRIBE: self.feed_tribe_state, + GamePhase.NEW_ROUND: self.new_round_state, + GamePhase.WAITING_FOR_TOOL_USE: self.waiting_for_tool_use_state, + GamePhase.ALL_PLAYERS_TAKE_A_REWARD: self.all_players_take_a_reward_state, + GamePhase.GAME_END: self.game_end_state, + } + + self.controller = GamePhaseController( + dispatchers, PlayerOrder(0, 2)) + + def state_string(self) -> str: + state: Any = json.loads(self.controller.state()) + return str(state["game phase"])+","+str(state["round starting player"])+"/" \ + + str(state["current_player"])+"/" + \ + str(state["player taking a reward"]) + + def test_players_swap_as_expected(self) -> None: + self.assertEqual(self.state_string(), + "GamePhase.PLACE_FIGURES,0/0/None") + + # incorrect player + res = self.controller.place_figures( + PlayerOrder(1, 2), Location.BUILDING_TILE1, 1) + self.assertFalse(res) + self.assertEqual(self.state_string(), + "GamePhase.PLACE_FIGURES,0/0/None") + + # correct player succesfully places figure + self.place_figures_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.place_figures_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.place_figures( + PlayerOrder(0, 2), Location.BUILDING_TILE1, 1) + self.assertTrue(res) + self.assertEqual(self.state_string(), + "GamePhase.PLACE_FIGURES,0/1/None") + + # correct player succesfully places figure, next player has no action, but another one has + self.place_figures_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.place_figures_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.place_figures_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.place_figures( + PlayerOrder(1, 2), Location.BUILDING_TILE1, 1) + self.assertTrue(res) + self.assertEqual(self.state_string(), + "GamePhase.PLACE_FIGURES,0/1/None") + + # correct player succesfully places figure, but nobody has an action + self.place_figures_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.place_figures_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.place_figures_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.make_action_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.place_figures( + PlayerOrder(1, 2), Location.BUILDING_TILE1, 1) + self.assertTrue(res) + self.assertEqual(self.state_string(), "GamePhase.MAKE_ACTION,0/0/None") + + # action evaluation does not swap who is on turn + self.make_action_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.make_action_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.make_action( + PlayerOrder(0, 2), Location.BUILDING_TILE1, [], []) + self.assertTrue(res) + self.assertEqual(self.state_string(), "GamePhase.MAKE_ACTION,0/0/None") + + # move to feed tribe phase + self.make_action_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.make_action_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.make_action_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.feed_tribe_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.make_action( + PlayerOrder(0, 2), Location.BUILDING_TILE1, [], []) + self.assertTrue(res) + self.assertEqual(self.state_string(), "GamePhase.FEED_TRIBE,0/0/None") + + # order in Feed trib phase is arbitrary, move to new turm and game ends + self.feed_tribe_state.expected_action_results.append( + ActionResult.ACTION_DONE) + self.feed_tribe_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.feed_tribe_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) + self.new_round_state.expected_has_action.append( + HasAction.NO_ACTION_POSSIBLE) # indicates game end + self.game_end_state.expected_has_action.append( + HasAction.WAITING_FOR_PLAYER_ACTION) + res = self.controller.feed_tribe(PlayerOrder(1, 2), []) + self.assertTrue(res) + self.assertEqual(self.state_string(), "GamePhase.GAME_END,0/0/None") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/game_phase_controller/test_integration/__init__.py b/test/game_phase_controller/test_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/player_board/__init__.py b/test/player_board/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/player_board/test_integration/__init__.py b/test/player_board/test_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_integration/__init__.py b/test/test_integration/__init__.py new file mode 100644 index 0000000..e69de29