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

1""" 

2Module containing the code to run the cli for zombie dice and expose the first implemenation of the game. 

3 

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. 

6 

7```python 

8from zombie_nomnom import ZombieDieGame 

9from zombie_nomnom.cli import run_game 

10 

11 

12# Starts a full game from setup to full play by play. 

13run_game()  

14 

15existing_game = ZombieDieGame(players=["Me", "You", "Mr. McGee"]) 

16 

17# Run the cli for an already running/existing game. 

18run_game(existing_game) 

19 

20``` 

21 

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` 

24 

25```python 

26from zombie_nomnom import ZombieDieGame 

27from zombie_nomnom.cli import render_players, render_turn, render_winner 

28 

29existing_game = ZombieDieGame(players=["Billy", "Zabka"]) 

30 

31# Prints out the details of the players of the game and their scores. 

32render_players(existing_game) 

33 

34# Prints out the given round information object. 

35render_turn(existing_game.round) 

36 

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) 

40 

41``` 

42 

43This module is primarly used by app and should not be used by other parts of our library. 

44 

45""" 

46 

47import json 

48import os 

49from typing import Any, Callable, TypeVar 

50import click 

51 

52from .engine import DrawDice, Player, RoundState, Score, ZombieDieGame 

53from .engine.serialization import format_to_json_dict, parse_game_json_dict 

54 

55draw_command = DrawDice() 

56"""Command used to draw the dice required for a turn.""" 

57 

58score_command = Score() 

59"""Command used to score a players hand during the game.""" 

60 

61 

62def draw_dice(game: ZombieDieGame): 

63 """Applys the DrawDice command to the game instance and 

64 renders the result to the console. 

65 

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

75 

76 

77def score_hand(game: ZombieDieGame): 

78 """Applys the score command to a game instance and 

79 prints out result to the console. 

80 

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

87 

88 

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. 

94 

95 **Parameters** 

96 - game (`zombie_nomnom.ZombieDieGame`): game instance you want to end. 

97 """ 

98 click.echo("Ending game...") 

99 game.game_over = True 

100 

101 

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 ) 

111 

112 if os.path.exists(filepath): 

113 if not click.confirm("Overwrite existing file?", abort=False): 

114 return 

115 

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

119 

120 

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} 

127 

128 

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. 

135 

136 **Parameters** 

137 - game (`zombie_nomnom.ZombieDieGame` | `None`, optional): game instance that we want to run. Defaults to None. 

138 

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 

149 

150 

151def render_winner(game: ZombieDieGame): 

152 """Prints out the current winner of the game instance to the console. 

153 

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

159 

160 

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. 

163 

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) 

169 

170 

171def _format_turn_info(turn: RoundState): 

172 player = turn.player 

173 bag = turn.bag 

174 

175 return f"{player.name}, Hand: Brains({len(player.brains)}), Feet({len(player.rerolls)}), Shots({len(player.shots)}), Dice Remaining: {len(bag)}" 

176 

177 

178def render_turn(turn: RoundState): 

179 """Prints turn info to the console for a given RoundState. 

180 

181 **Parameters** 

182 - turn (`zombie_nomnom.RoundState`): RoundState we are printing. 

183 """ 

184 click.echo(f"Currently Playing {_format_turn_info(turn)}") 

185 

186 

187def _format_player(player: Player): 

188 """ 

189 Formats a player object into a string for our rendering fuctions. 

190 

191 **Parameters** 

192 - player (`zombie_nomnom.Player`): player we want to format as string 

193 

194 **Returns** 

195 - `str`: Stringified version of player 

196 """ 

197 return f"{player.name} ({player.total_brains})" 

198 

199 

200def render_players(game: ZombieDieGame): 

201 """Prints out the players currently playing in 

202 game instance. Will put them all on a single line. 

203 

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

209 

210 

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

215 

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. 

221 

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. 

226 

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

234 

235 

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 ) 

246 

247 

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 ) 

257 

258 with open(filepath, "r") as fp: 

259 data = json.load(fp) 

260 

261 return parse_game_json_dict(data) 

262 

263 

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. 

268 

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

280 

281 

282TVar = TypeVar("TVar") 

283 

284 

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. 

290 

291 **Parameters** 

292 - value (`dict[str, TVar]`): dictionary with values the user will select from. 

293 

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

307 

308 

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. 

316 

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

321 

322 **Returns** 

323 - `list`: Collection of items that the user has given. 

324 """ 

325 inputs = [] 

326 inputs.append(click.prompt(prompt, type=_type)) 

327 

328 while click.confirm(confirmation_prompt): 

329 inputs.append(click.prompt(prompt, type=_type)) 

330 return inputs