Coverage for zombie_nomnom/cli.py: 100%
86 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"""
2Module containing the code to run the cli for zombie dice and expose the first implemenation of the game.
4If you want to just play a game using click's interface for getting users inputs from the cli
5you may use the `run_game` function.
7```python
8from zombie_nomnom import ZombieDieGame
9from zombie_nomnom.cli import run_game
12# Starts a full game from setup to full play by play.
13run_game()
15existing_game = ZombieDieGame(players=["Me", "You", "Mr. McGee"])
17# Run the cli for an already running/existing game.
18run_game(existing_game)
20```
22If you would like to just use some of the functions we have to render different objects
23you can use our print functions such as: `render_players`, `render_turn`, `render_winner`
25```python
26from zombie_nomnom import ZombieDieGame
27from zombie_nomnom.cli import render_players, render_turn, render_winner
29existing_game = ZombieDieGame(players=["Billy", "Zabka"])
31# Prints out the details of the players of the game and their scores.
32render_players(existing_game)
34# Prints out the given round information object.
35render_turn(existing_game.round)
37# Prints out the highest scoring player
38# defaults to the player that went first in the case of a tie.
39render_winner(existing_game)
41```
43This module is primarly used by app and should not be used by other parts of our library.
45"""
47import json
48import os
49from typing import Any, Callable, TypeVar
50import click
52from .engine import DrawDice, Player, RoundState, Score, ZombieDieGame
53from .engine.serialization import format_to_json_dict, parse_game_json_dict
55draw_command = DrawDice()
56"""Command used to draw the dice required for a turn."""
58score_command = Score()
59"""Command used to score a players hand during the game."""
62def draw_dice(game: ZombieDieGame):
63 """Applys the DrawDice command to the game instance and
64 renders the result to the console.
66 **Parameters**
67 - game (`zombie_nomnom.ZombieDieGame`): instance of the game to apply the draw command too.
68 """
69 click.echo("Drawing dice...")
70 turn = game.process_command(draw_command)
71 if not turn.ended:
72 click.echo(_format_turn_info(turn))
73 else:
74 click.echo(f"Ohh no!! {turn.player.name} Has Died(T_T) R.I.P")
77def score_hand(game: ZombieDieGame):
78 """Applys the score command to a game instance and
79 prints out result to the console.
81 **Parameters**
82 - game (`zombie_nomnom.ZombieDieGame`): game instance you want to score with.
83 """
84 click.echo("Scoring hand...")
85 turn = game.process_command(score_command)
86 click.echo(_format_turn_info(turn))
89def exit_game(game: ZombieDieGame):
90 """
91 Exits the game by marking the game as over for players
92 who wish to finish the current game they are playing
93 and prints out to the console.
95 **Parameters**
96 - game (`zombie_nomnom.ZombieDieGame`): game instance you want to end.
97 """
98 click.echo("Ending game...")
99 game.game_over = True
102def save_game(game: ZombieDieGame):
103 click.echo("Saving game...")
104 filepath = click.prompt(
105 "Enter savefile name to save to",
106 type=click.Path(
107 dir_okay=False,
108 writable=True,
109 ),
110 )
112 if os.path.exists(filepath):
113 if not click.confirm("Overwrite existing file?", abort=False):
114 return
116 with open(filepath, "w") as fp:
117 json.dump(format_to_json_dict(game), fp)
118 click.echo(f"Saved game to {filepath} succesfully!!")
121_actions: dict[str, Callable[[ZombieDieGame], None]] = {
122 "Exit": exit_game,
123 "Save Game": save_game,
124 "Score hand": score_hand,
125 "Draw dice": draw_dice,
126}
129def run_game(game: ZombieDieGame | None = None):
130 """
131 Main entrypoint for prompting and running the game,
132 either an existing instance or creates a new one if not given.
133 Will allow users to get prompted and play the game as well as run setup if no game
134 is given.
136 **Parameters**
137 - game (`zombie_nomnom.ZombieDieGame` | `None`, optional): game instance that we want to run. Defaults to None.
139 **Returns**
140 - `zombie_nomnom.ZombieDieGame`: The instance of the game that has been played.
141 """
142 game = game or setup_game()
143 while not game.game_over:
144 # prime game with initial turn.
145 render_players(game)
146 play_turn(game)
147 render_winner(game)
148 return game
151def render_winner(game: ZombieDieGame):
152 """Prints out the current winner of the game instance to the console.
154 **Parmeters**
155 - game (`zombie_nomnom.ZombieDieGame`): instance that we are looking for the winner on.
156 """
157 formatted_player = _format_player(game.winner)
158 click.echo(f"{formatted_player} Has Won!!")
161def play_turn(game: ZombieDieGame):
162 """Prompts the user on the console to select action for the turn and prints out the current turn information.
164 **Parameters**
165 - game (`zombie_nomnom.ZombieDieGame`): game we want to do a turn action on.
166 """
167 render_turn(game.round)
168 select_dict_item(_actions)(game)
171def _format_turn_info(turn: RoundState):
172 player = turn.player
173 bag = turn.bag
175 return f"{player.name}, Hand: Brains({len(player.brains)}), Feet({len(player.rerolls)}), Shots({len(player.shots)}), Dice Remaining: {len(bag)}"
178def render_turn(turn: RoundState):
179 """Prints turn info to the console for a given RoundState.
181 **Parameters**
182 - turn (`zombie_nomnom.RoundState`): RoundState we are printing.
183 """
184 click.echo(f"Currently Playing {_format_turn_info(turn)}")
187def _format_player(player: Player):
188 """
189 Formats a player object into a string for our rendering fuctions.
191 **Parameters**
192 - player (`zombie_nomnom.Player`): player we want to format as string
194 **Returns**
195 - `str`: Stringified version of player
196 """
197 return f"{player.name} ({player.total_brains})"
200def render_players(game: ZombieDieGame):
201 """Prints out the players currently playing in
202 game instance. Will put them all on a single line.
204 **Parameters**
205 - game (`zombie_nomnom.ZombieDieGame`): game instance that we are getting players from.
206 """
207 players_listed = ", ".join(_format_player(player) for player in game.players)
208 click.echo(f"Players: {players_listed}")
211class StrippedStr(click.ParamType):
212 """Custom `str` parameters that will take in the input from the cli
213 and trim any trailing or leading spaces so that I can focus on just the value itself.
214 """
216 def convert(
217 self, value: Any, param: click.Parameter | None, ctx: click.Context | None
218 ) -> str:
219 """Converts a value from clicks input function into a stripped `str`.
220 If given an object will turn the object to a `str` using the `__str__` and then trim the output.
222 **Parameters**
223 - value (`Any`): Value that was taken by clicks input functions.
224 - param (`click.Parameter` | `None`): Optional Parameter form click.
225 - ctx (`click.Context` | `None`): Optional context form click.
227 **Returns**
228 - `str`: str value that has been trimmed.
229 """
230 if isinstance(value, str):
231 return value.strip()
232 else:
233 return str(value).strip()
236def new_game() -> ZombieDieGame:
237 names = prompt_list(
238 "Enter Player Name",
239 _type=StrippedStr(),
240 confirmation_prompt="Add Another Player?",
241 )
242 # TODO(Milo): Figure out a bunch of game types to play that we can use as templates for the die.
243 return ZombieDieGame(
244 players=[Player(name=name) for name in names],
245 )
248def load_game() -> ZombieDieGame:
249 filepath = click.prompt(
250 "Enter savefile name to load",
251 type=click.Path(
252 dir_okay=False,
253 exists=True,
254 readable=True,
255 ),
256 )
258 with open(filepath, "r") as fp:
259 data = json.load(fp)
261 return parse_game_json_dict(data)
264def setup_game() -> ZombieDieGame:
265 """Runs the setup game cli prompts for users to enter the players in the game.
266 This will prompt you for each player in the game then create and return the
267 game instance you have with those players.
269 **Returns**
270 - `zombie_nomnom.ZombieDieGame`: The instance of the game you setup.
271 """
272 callback = select_dict_item(
273 {
274 "Exit": lambda: exit(0),
275 "New Game": new_game,
276 "Load Game": load_game,
277 }
278 )
279 return callback()
282TVar = TypeVar("TVar")
285def select_dict_item(value: dict[str, TVar]) -> TVar:
286 """Prompts the user to select an item from a dictionary and
287 then return the value stored at that key. The selection is
288 based on an array of options that is orded the same as the
289 way they are stored in the keys.
291 **Parameters**
292 - value (`dict[str, TVar]`): dictionary with values the user will select from.
294 **Returns**
295 - `TVar`: The value that was selected by the user.
296 """
297 menu_items = list(value)
298 menu = "\n".join(
299 f"{index}) {item}" for index, item in reversed(list(enumerate(menu_items)))
300 )
301 click.echo(menu)
302 selected_index = click.prompt(
303 f"Select Item (0-{len(menu_items) - 1})",
304 type=click.IntRange(0, len(menu_items) - 1),
305 )
306 return value[menu_items[selected_index]]
309def prompt_list(
310 prompt: str,
311 _type: type,
312 confirmation_prompt: str = "Add Another?",
313) -> list:
314 """Prompts the user to input a list of items using the click library.
315 Allows you to define the type of information you want in your list and then return that to you.
317 **Parameters**
318 - prompt (`str`): Prompt to display to the user.
319 - _type (`type`): The type that you wish to get back, can also be a `click.ParamType`
320 - confirmation_prompt (`str`, optional): Optional prompt for when the user is asked to add another item. Defaults to "Add Another?".
322 **Returns**
323 - `list`: Collection of items that the user has given.
324 """
325 inputs = []
326 inputs.append(click.prompt(prompt, type=_type))
328 while click.confirm(confirmation_prompt):
329 inputs.append(click.prompt(prompt, type=_type))
330 return inputs