-
Notifications
You must be signed in to change notification settings - Fork 1
[Small] Feature/game repo #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ed82df6
Added game repo and game manager
RobbieKiwi 5d62e23
Added game repo and game manager
RobbieKiwi 8e8d68c
mino changes
RobbieKiwi 0f0d47e
some refactoring
RobbieKiwi bc952ff
formatting
RobbieKiwi 809ab89
Changes based on pr comments
RobbieKiwi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| from typing import Protocol, runtime_checkable | ||
|
|
||
| from src.app.game_repo.base import BaseGameStateRepo | ||
| from src.models.game_state import GameState | ||
| from src.models.ids import GameId | ||
| from src.models.message import GameToPlayerMessage, PlayerToGameMessage, ToGameMessage, InternalMessage, FromGameMessage | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class CanReceiveGameToPlayerMessages(Protocol): | ||
| # A generic stub for the front end implementation | ||
| def handle_player_messages(self, msgs: list[GameToPlayerMessage]) -> None: ... | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class CanReceiveToGameMessage(Protocol): | ||
| # A generic stub for the game engine implementation | ||
| @classmethod | ||
| def handle_message(cls, game_state: GameState, msg: ToGameMessage) -> tuple[GameState, list[FromGameMessage]]: ... | ||
|
|
||
|
|
||
| class GameManager: | ||
| def __init__( | ||
| self, | ||
| game_repo: BaseGameStateRepo, | ||
| game_engine: CanReceiveToGameMessage, | ||
| front_end: CanReceiveGameToPlayerMessages, | ||
| ) -> None: | ||
| assert isinstance(game_repo, BaseGameStateRepo) | ||
| assert isinstance(game_engine, CanReceiveToGameMessage) | ||
| assert isinstance(front_end, CanReceiveGameToPlayerMessages) | ||
| self.game_repo = game_repo | ||
| self.game_engine = game_engine | ||
| self.front_end = front_end | ||
|
|
||
| def handle_player_message(self, game_id: GameId, msg: PlayerToGameMessage) -> None: | ||
| # TODO Make this atomic | ||
| game_state = self.game_repo.get_game_state(game_id) | ||
| updated_game_state = self.handle_message(game_state=game_state, msg=msg) | ||
| self.game_repo.update_game_state(updated_game_state) | ||
|
|
||
| def handle_message(self, game_state: GameState, msg: ToGameMessage) -> GameState: | ||
| game_state, messages = self.game_engine.handle_message(game_state=game_state, msg=msg) | ||
|
|
||
| msgs_to_players = [e for e in messages if isinstance(e, GameToPlayerMessage)] | ||
| self.front_end.handle_player_messages(msgs=msgs_to_players) | ||
|
|
||
| msgs_to_self = [e for e in messages if isinstance(e, InternalMessage)] | ||
| if not len(msgs_to_self): | ||
| return game_state | ||
|
|
||
| assert len(msgs_to_self) == 1, "There should be at most one internal message generated by the engine." | ||
| return self.handle_message(game_state=game_state, msg=msgs_to_self[0]) |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| from abc import ABC, abstractmethod | ||
|
|
||
| from src.models.game_state import GameState | ||
| from src.models.ids import GameId | ||
|
|
||
|
|
||
| class BaseGameStateRepo(ABC): | ||
| @abstractmethod | ||
| def generate_game_id(self) -> GameId: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def add_game_state(self, game: GameState) -> None: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def update_game_state(self, game: GameState) -> None: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def get_game_state(self, game_id: GameId) -> GameState: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def list_game_ids(self) -> list[GameId]: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def delete_game_state(self, game_id: GameId, missing_ok: bool = True) -> None: | ||
| pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| from pathlib import Path | ||
|
|
||
| from src.app.game_repo.base import BaseGameStateRepo | ||
| from src.directories import game_cache_dir | ||
| from src.models.game_state import GameState | ||
| from src.models.ids import GameId | ||
| from src.tools.serialization import serialize, deserialize | ||
|
|
||
|
|
||
| class FileGameStateRepo(BaseGameStateRepo): | ||
| def __init__(self, cache_dir: Path = game_cache_dir) -> None: | ||
| self.cache_dir = cache_dir | ||
|
|
||
| def generate_game_id(self) -> GameId: | ||
| game_ids = self.list_game_ids() | ||
| return GameId(max(game_ids).as_int() + 1 if game_ids else 0) | ||
|
|
||
| def add_game_state(self, game: GameState) -> None: | ||
| path = self.game_id_to_file_path(game.game_id) | ||
| if path.exists(): # Todo make these checks threadsafe | ||
| raise FileExistsError(f"Game with ID {game.game_id} already exists.") | ||
| with open(path, "w") as file: | ||
| file.write(serialize(game)) | ||
|
|
||
| def update_game_state(self, game: GameState) -> None: | ||
| path = self.game_id_to_file_path(game.game_id) | ||
| if not path.exists(): | ||
| raise FileNotFoundError(f"Game with ID {game.game_id} does not exist.") | ||
| with open(path, "w") as file: | ||
| file.write(serialize(game)) | ||
|
|
||
| def get_game_state(self, game_id: GameId) -> GameState: | ||
| path = self.game_id_to_file_path(game_id) | ||
| if not path.exists(): | ||
| raise FileNotFoundError(f"Game with ID {game_id} does not exist.") | ||
| with open(path, "r") as file: | ||
| data = file.read() | ||
| return deserialize(x=data, cls=GameState) | ||
|
|
||
| def list_game_ids(self) -> list[GameId]: | ||
| game_files = self.cache_dir.glob("game_*.json") | ||
| return [self.file_path_to_game_id(file_path) for file_path in game_files if file_path.is_file()] | ||
|
|
||
| def delete_game_state(self, game_id: GameId, missing_ok: bool = True) -> None: | ||
| path = self.game_id_to_file_path(game_id) | ||
| path.unlink(missing_ok=missing_ok) | ||
|
|
||
| def game_id_to_file_path(self, game_id: GameId) -> Path: | ||
| """Get the file path for a specific game ID.""" | ||
| return self.cache_dir / f"game_{game_id}.json" | ||
|
|
||
| @staticmethod | ||
| def file_path_to_game_id(file_path: Path) -> GameId: | ||
| """Extract the game ID from a file path.""" | ||
| if not file_path.name.startswith("game_") or not file_path.name.endswith(".json"): | ||
| raise ValueError(f"Invalid game file name: {file_path.name}") | ||
| game_id_str = file_path.name[len("game_") : -len(".json")] | ||
| return GameId(int(game_id_str)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from pathlib import Path | ||
|
|
||
| root_dir = Path(__file__).parent | ||
| game_cache_dir = root_dir / "game_cache" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,102 +1,104 @@ | ||
| from src.models.event import ( | ||
| Event, | ||
| from src.models.game_state import GameState | ||
| from src.models.message import ( | ||
| UpdateBidRequest, | ||
| BuyAssetRequest, | ||
| EndTurn, | ||
| UpdateBidResponse, | ||
| BuyAssetResponse, NewPhase | ||
| BuyAssetResponse, | ||
| NewPhase, | ||
| ToGameMessage, | ||
| FromGameMessage, | ||
| ) | ||
| from src.models.game_state import GameState | ||
|
|
||
|
|
||
| class Engine: | ||
| @classmethod | ||
| def handle_event(cls, game_state: GameState, event: Event) -> tuple[GameState, list[Event]]: | ||
| def handle_message(cls, game_state: GameState, msg: ToGameMessage) -> tuple[GameState, list[FromGameMessage]]: | ||
| """ | ||
| Events happen every time a player takes an action or a timer runs out. | ||
| Every time an event occurs, the engine is informed and it can then: | ||
| Messages can come from players or from the game itself | ||
| Every time a message occurs, the engine is informed and it can then: | ||
| -Update the game state | ||
| -Send messages back to the players OR to itself to trigger a new phase | ||
| -Send messages back to the players OR to itself | ||
| :param game_state: The current state of the game | ||
| :param event: The triggering event | ||
| :return: The new game state and a list of events to be sent | ||
| :param msg: The triggering message | ||
| :return: The new game state and a list of messages to be sent | ||
| """ | ||
| # Handle the event based on its type | ||
| if isinstance(event, NewPhase): | ||
| return cls.handle_new_phase_event(game_state, event) | ||
| elif isinstance(event, UpdateBidRequest): | ||
| return cls.handle_update_bid_event(game_state, event) | ||
| elif isinstance(event, BuyAssetRequest): | ||
| return cls.handle_buy_asset_event(game_state, event) | ||
| elif isinstance(event, EndTurn): | ||
| return cls.handle_end_turn_event(game_state, event) | ||
| # Handle the message based on its type | ||
| if isinstance(msg, NewPhase): | ||
| return cls.handle_new_phase_message(game_state, msg) | ||
| elif isinstance(msg, UpdateBidRequest): | ||
| return cls.handle_update_bid_message(game_state, msg) | ||
| elif isinstance(msg, BuyAssetRequest): | ||
| return cls.handle_buy_asset_message(game_state, msg) | ||
| elif isinstance(msg, EndTurn): | ||
| return cls.handle_end_turn_message(game_state, msg) | ||
| else: | ||
| raise NotImplementedError(f"Event type {type(event)} not implemented.") | ||
| raise NotImplementedError(f"message type {type(msg)} not implemented.") | ||
|
|
||
| @classmethod | ||
| def handle_new_phase_event( | ||
| cls, | ||
| game_state: GameState, | ||
| event: NewPhase, | ||
| ) -> tuple[GameState, list[Event]]: | ||
| def handle_new_phase_message( | ||
| cls, | ||
| game_state: GameState, | ||
| msg: NewPhase, | ||
| ) -> tuple[GameState, list[FromGameMessage]]: | ||
| """ | ||
| Handle a new phase event. | ||
| Handle a new phase message. | ||
| :param game_state: The current state of the game | ||
| :param event: The triggering event | ||
| :return: The new game state and a list of events to be sent to the player interface | ||
| :param msg: The triggering message | ||
| :return: The new game state and a list of messages to be sent to the player interface | ||
| """ | ||
| # TODO Do something depending on what phase we are in | ||
| # TODO if we are in the da_auction phase, we need to run the market coupling algorithm | ||
| raise NotImplementedError() | ||
|
|
||
| @classmethod | ||
| def handle_update_bid_event( | ||
| cls, | ||
| game_state: GameState, | ||
| event: UpdateBidRequest, | ||
| def handle_update_bid_message( | ||
| cls, | ||
| game_state: GameState, | ||
| msg: UpdateBidRequest, | ||
| ) -> tuple[GameState, list[UpdateBidResponse]]: | ||
| """ | ||
| Handle an update bid event. | ||
| Handle an update bid message. | ||
| :param game_state: The current state of the game | ||
| :param event: The triggering event | ||
| :return: The new game state and a list of events to be sent to the player interface | ||
| :param msg: The triggering message | ||
| :return: The new game state and a list of messages to be sent to the player interface | ||
| """ | ||
| # TODO Check if the bid is valid (including if the player can afford it). | ||
| # TODO Update bids in the game state | ||
| # TODO Return one UpdateBidResponseEvent for the player who made the bid | ||
| # TODO Return one UpdateBidResponse for the player who made the bid | ||
| raise NotImplementedError() | ||
|
|
||
| @classmethod | ||
| def handle_buy_asset_event( | ||
| cls, | ||
| game_state: GameState, | ||
| event: BuyAssetRequest, | ||
| def handle_buy_asset_message( | ||
| cls, | ||
| game_state: GameState, | ||
| msg: BuyAssetRequest, | ||
| ) -> tuple[GameState, list[BuyAssetResponse]]: | ||
| """ | ||
| Handle a buy asset event. | ||
| Handle a buy asset message. | ||
| :param game_state: The current state of the game | ||
| :param event: The triggering event | ||
| :return: The new game state and a list of events to be sent to the player interface | ||
| :param msg: The triggering message | ||
| :return: The new game state and a list of messages to be sent to the player interface | ||
| """ | ||
| # TODO Check if the request is valid (including if the asset is for sale and the player can afford it). | ||
| # TODO Update asset ownership | ||
| # TODO Return a BuyAssetResponse | ||
| raise NotImplementedError() | ||
|
|
||
| @classmethod | ||
| def handle_end_turn_event( | ||
| cls, | ||
| game_state: GameState, | ||
| event: EndTurn, | ||
| def handle_end_turn_message( | ||
| cls, | ||
| game_state: GameState, | ||
| msg: EndTurn, | ||
| ) -> tuple[GameState, list[NewPhase]]: | ||
| """ | ||
| Handle an end turn event. | ||
| Handle an end turn message. | ||
| :param game_state: The current state of the game | ||
| :param event: The triggering event | ||
| :return: The new game state and a list of events to be sent to the player interface | ||
| :param msg: The triggering message | ||
| :return: The new game state and a list of messages to be sent to the player interface | ||
| """ | ||
| # TODO Update the player to indicate that their turn has ended | ||
| # TODO If this phase requires players to play one by one (Do we need such a phase?) Then cycle to the next player | ||
| # TODO Check if all players have ended their turns and we need to move on to the next phase | ||
| # TODO If necessary, return an event to signal yourself to go to a different phase | ||
| # TODO If necessary, return an message to signal yourself to go to a different phase | ||
| raise NotImplementedError() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.