Coverage for zombie_nomnom/engine/serialization.py: 100%
64 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 01:14 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 01:14 +0000
1"""This module contains the code that relates to how we can turn our game instances into a dictionary as well as
2how we can store commands and load them back dynamically
3i.e. if you make custom commands how to load those commands back.
5```python
6from zombie_nomnom import ZombieDieGame
7from zombie_nomnom.engine.serialization import format_to_json_dict
9game = ZombieDieGame(players=["Player Uno"])
11game_dict = format_to_json_dict(game)
13# now you can write it as a yaml or json using whatever you like.
14import json
15with open('save_file.dat', 'w') as fp:
16 json.dump(game_dict, fp) # write it out to the file!!
18# at a later date...
19from zombie_nomnom.engine.serialization import parse_game_json_dict
20with open('save_file.dat', 'r') as fp:
21 game_dict = json.load(fp)
23# now we can keep playing like nothing happened!!
24game = parse_game_json_dict(game_dict)
25```
27This allows you to control how you want to seralize the data and we store the format
28in the TypedDict definitions within the module if you need to have some more information on how it looks.
29This means that whatever way you wanna store these models you can and we can load it as long as the dict
30matches our defined structure.
31"""
33from enum import Enum
34import importlib
35from typing import Any, TypedDict
37from zombie_nomnom.engine.commands import Command
38from zombie_nomnom.engine.game import ZombieDieGame
39from zombie_nomnom.engine.models import DieRecipe, Player, RoundState
40from zombie_nomnom.models.bag import DieBag
43class DieDict(TypedDict):
44 sides: list[str]
45 current_face: str | None
48class PlayerDict(TypedDict):
49 id: str
50 name: str
51 total_brains: int
52 hand: list[DieDict]
55class DieBagDict(TypedDict):
56 dice: list[DieDict]
57 drawn_dice: list[DieDict] | None
60class RoundStateDict(TypedDict):
61 diebag: DieBagDict
64class CommandDict(TypedDict):
65 cls: str
66 args: list[Any]
67 kwargs: dict[str, Any]
70class DieRecipeDict(TypedDict):
71 faces: list[str]
72 amount: int
75class ZombieDieGameDict(TypedDict):
76 players: list[PlayerDict]
77 commands: list[tuple[CommandDict, RoundStateDict]]
78 current_player: int | None
79 first_winning_player: int | None
80 round: RoundStateDict
81 game_over: bool
82 score_threshold: int
83 bag_function: str | list[DieRecipeDict]
86# We may want this to be removed later idk yet?
87class KnownFunctions(str, Enum):
88 STANDARD = "standard"
91def format_command(command: Command) -> CommandDict:
92 cmd_type = type(command)
93 module = cmd_type.__module__
94 qual_name = cmd_type.__qualname__
95 return {
96 "cls": f"{module}.{qual_name}",
97 "args": [],
98 # only works if the field on the class matches the param in __init__.py
99 "kwargs": command.__dict__,
100 }
103def parse_command_dict(command: CommandDict) -> Command:
104 [*module_path, cls_name] = command.get("cls").split(".")
105 module_name = ".".join(module_path)
106 module = importlib.import_module(module_name)
107 cls = getattr(module, cls_name)
108 return cls(*command.get("args"), **command.get("kwargs"))
111def format_to_json_dict(game: ZombieDieGame) -> ZombieDieGameDict:
112 """Will default any game to be using the standard bag unless given a recipe to create dice.
114 **Parameters**
115 - game (`ZombieDieGame`): The game to serialize
117 **Returns**
118 - `ZombieDieGameDict`: The serialized game as a dict that is json serializable
119 """
120 return {
121 "players": [player.model_dump(mode="json") for player in game.players],
122 "bag_function": (
123 KnownFunctions.STANDARD
124 if not game.bag_recipes
125 else [recipe.model_dump(mode="json") for recipe in game.bag_recipes]
126 ),
127 "commands": [
128 (format_command(command), state.model_dump(mode="json"))
129 for command, state in game.commands
130 ],
131 "current_player": game.current_player,
132 "first_winning_player": game.first_winning_player,
133 "game_over": game.game_over,
134 "round": game.round.model_dump(mode="json"),
135 "score_threshold": game.score_threshold,
136 }
139UNTRANSFORMED_KEYS = {
140 "score_threshold",
141 "current_player",
142 "first_winning_player",
143 "game_over",
144}
147def parse_game_json_dict(game_data: ZombieDieGameDict) -> ZombieDieGame:
148 parameters = {
149 key: value for key, value in game_data.items() if key in UNTRANSFORMED_KEYS
150 }
151 # ones we just need a simple model_validate_on
152 parameters["commands"] = [
153 (parse_command_dict(command), RoundState.model_validate(state))
154 for command, state in game_data["commands"]
155 ]
156 parameters["players"] = [
157 Player.model_validate(player) for player in game_data["players"]
158 ]
159 parameters["round"] = RoundState.model_validate(game_data.get("round"))
161 # special fields to set.
162 bag_func = game_data["bag_function"]
163 if isinstance(bag_func, str) and bag_func != KnownFunctions.STANDARD:
164 # We only allow us to use standard_bag when we load a dict.
165 raise ValueError(
166 f"Unable to understand the bag_function referenced: {bag_func}"
167 )
168 # default function for a game is standard anyway so this will just work lol.
169 parameters["bag_function"] = None
170 if not isinstance(bag_func, str):
171 # basically we know these are recipes so load them.
172 parameters["bag_recipes"] = [
173 DieRecipe.model_validate(raw_recipe) for raw_recipe in bag_func
174 ]
176 return ZombieDieGame(**parameters)