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

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. 

4 

5```python 

6from zombie_nomnom import ZombieDieGame 

7from zombie_nomnom.engine.serialization import format_to_json_dict 

8 

9game = ZombieDieGame(players=["Player Uno"]) 

10 

11game_dict = format_to_json_dict(game) 

12 

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!! 

17 

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) 

22 

23# now we can keep playing like nothing happened!! 

24game = parse_game_json_dict(game_dict) 

25``` 

26 

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""" 

32 

33from enum import Enum 

34import importlib 

35from typing import Any, TypedDict 

36 

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 

41 

42 

43class DieDict(TypedDict): 

44 sides: list[str] 

45 current_face: str | None 

46 

47 

48class PlayerDict(TypedDict): 

49 id: str 

50 name: str 

51 total_brains: int 

52 hand: list[DieDict] 

53 

54 

55class DieBagDict(TypedDict): 

56 dice: list[DieDict] 

57 drawn_dice: list[DieDict] | None 

58 

59 

60class RoundStateDict(TypedDict): 

61 diebag: DieBagDict 

62 

63 

64class CommandDict(TypedDict): 

65 cls: str 

66 args: list[Any] 

67 kwargs: dict[str, Any] 

68 

69 

70class DieRecipeDict(TypedDict): 

71 faces: list[str] 

72 amount: int 

73 

74 

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] 

84 

85 

86# We may want this to be removed later idk yet? 

87class KnownFunctions(str, Enum): 

88 STANDARD = "standard" 

89 

90 

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 } 

101 

102 

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")) 

109 

110 

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. 

113 

114 **Parameters** 

115 - game (`ZombieDieGame`): The game to serialize 

116 

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 } 

137 

138 

139UNTRANSFORMED_KEYS = { 

140 "score_threshold", 

141 "current_player", 

142 "first_winning_player", 

143 "game_over", 

144} 

145 

146 

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")) 

160 

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 ] 

175 

176 return ZombieDieGame(**parameters)