-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame_controller.py
More file actions
371 lines (294 loc) · 14.2 KB
/
Copy pathgame_controller.py
File metadata and controls
371 lines (294 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
'''
=======================================================================
SEMANTIC VERIFICATION ENGINE (Ref implementation: Harry Potter Trivia)
=======================================================================
-----------------------------------------------------------------------
CLI MVP -> Game Controller Module
-----------------------------------------------------------------------
Game orchestrator responsible for executing a single gameplay run for a
player, including introduction, session execution, and end-game flow.
Acts as the interface between:
- main application loop (multi-session orchestration)
- view layer (user interaction)
- evaluation router (tiered hybrid semantic / structured evaluation system)
Core responsibilities:
- Handle player introduction and initialization
- Execute a full game session via run_game(session)
- Manage per-turn execution via _handle_turn()
- Route answers through evaluation_router for structured evaluation
- Update player state based on evaluation results
- Aggregate turn-level results into SessionReport
- Coordinate end-of-session flow and exit messaging
Game flow (high-level):
1. Introduction and player setup
2. Session execution (question loop)
- Display question
- Collect input
- Evaluate answer via routed evaluators
- Update player state
- Return TurnResult
3. Session finalization
- Aggregate SessionReport
- Handle quit / failure / completion states
- Delegate persistence to main layer
TODO:
system_signals (from startup) reserved for future runtime UX handling
(e.g. degraded/free-tier latency warnings, temporary LLM disablement)
'''
import time
from game_app.player import Player
from game_app.constants import NUM_QUESTIONS_PER_SESSION, GameStatus, UserWantsToQuit
from game_app.types import Question, SessionReport, Session
from engine.dto import TurnResult
from engine.evaluators.router import evaluation_router
## GAMEPLAY CONTROLLER: manage flow between the game states.
class GameController():
"""
Main game controller for the Trivia game.
- Manage the game flow and state.
- Interact with Player and Trivia modules.
- To manage the state (like the current Player object, the Trivia session object,
maybe the current question index, game over status).
- Other methods could handle specific parts like _get_player_details(), _play_round(),
_display_results().r adding a new player,
"""
# Track current state of the game.
def __init__(self, system_signals, view):
self.player = None # set during introduction phase
self.player_initialized = False # flipped to True after Introduction. Player is invariant after that
self.system_startup_signals = system_signals or {}
self.view = view # Persistent component
## Game Introduction
def _handle_introduction(self, total_questions: int):
"""
Setup the game introduction to run the Introduction class functions.
This will display:
1. Game title with ASCII art
2. Displays game dedication message
3. Displays the player greetings
4. Retrieves the player details and initializes the Player
5. Explains the game play that leads to the start of the game.
UX: Breaks the flow into distinct 'Screens'.
"""
# --- SCREEN 1: THE HOOK (Title & Greeting) ---
# 1. game title (view already clears the screen here)
self.view.print_ascii_art(font_style='ogre')
# 2. greeting
self.view.print_greeting()
# 3. pause to let player read before next input
self.view.console.print("\n[dim italic]Press Enter to enter the Great Hall...[/]\n")
self.view.console.input()
# --- SCREEN 2: IDENTITY (Name & House) ---
self.view.console.clear() # wipe screen
# 4. get name
player_name = self.view.get_player_name()
# 5. get house
player_house = self.view.get_player_house(player_name)
# create player object
self.player = Player(player_name, player_house)
# --- SCREEN 3: THE REVEAL (Welcome Panel) ---
self.view.console.clear() #wipe screen
# 6. welcome panel
self.view.print_personalized_player_welcome(self.player)
# pause before next screen
self.view.console.print("\n[dim italic]Press Enter to learn the rules...[/]")
self.view.console.input()
# --- SCREEN 4: THE RULES ---
self.view.console.clear() # clear screen
# 7. explain game play
# (This method has its own pauses, so it works well on a fresh screen)
self.view.explain_gameplay(total_questions)
# final clear so the first question pops on a black background
self.view.console.clear()
def start_game(self) -> bool:
"""
Handles the one-time player setup and introduction.
This is so the introduction is only required once if the
player wants to play multiple rounds.
Returns True if the setup was successful, False otherwise.
"""
# configuration
total_questions = NUM_QUESTIONS_PER_SESSION
# interactive introduction
self._handle_introduction(total_questions)
assert self.player is not None, "Player must be set during initialization"
self.player_initialized = True
return True
## Run Sessions
def run_game(self, session: Session) -> SessionReport:
"""
Orchestrates a single, complete game session from introduction to end-game.
This method is the main driver for a round of trivia. It calls internal
helper methods to handle the player introduction, data setup, the
turn-by-turn gameplay loop, and the final summary.
At the conclusion of the session, it prompts the user if they wish to
play again and communicates their choice via its return value.
TODO: consolidate report building into a helper to avoid repetition
Returns:
session_report : returns status report for session action in Main.
"""
# Guard clause: player initialized by controller at startup as invariant.
if self.player is None:
raise RuntimeError(
"GameController state invalid: player not initialized before run_game()")
# Guard clause: startup orchestration guarantees runtime question availability.
if not session.questions:
raise RuntimeError(
"Session initialized without questions. "
"Runtime invariant violated.")
# initialize
start_time = time.time()
questions_answered = 0
all_turn_results = []
player = self.player
session_report = SessionReport(
game_id = session.game_id,
total_questions = session.session_size,
player_name = player.name,
house = player.house
)
# Gameplay single session loop
try:
# Reset the player's stats for the new round before the questions begin.
player.reset_stats()
for idx, question in enumerate(session.questions):
# single question presentation and evaluation
turn_result = self._handle_turn(question=question, question_idx=idx)
all_turn_results.append(turn_result)
questions_answered +=1
# game loss path
if not player.has_chances_left():
# populate report
session_report.game_status = GameStatus.LOST
session_report.score = player.score
session_report.duration_sec = time.time() - start_time
session_report.questions_answered = questions_answered
session_report.all_turn_results = all_turn_results
# end game
self.view.display_error("Uh oh! You've run out of chances! 🥺")
self._render_session_summary(session_report)
return session_report
# populate report
session_report.game_status = GameStatus.COMPLETED
session_report.score = player.score
session_report.duration_sec = time.time() - start_time
session_report.questions_answered = questions_answered
session_report.all_turn_results = all_turn_results
# game win: session completed
self._render_session_summary(session_report)
return session_report
# custom exception to quit if player enters 'quit' at any input and end game.
except UserWantsToQuit:
session_report.game_status = GameStatus.QUIT
session_report.duration_sec = time.time() - start_time
session_report.score = None # score ommited for quit in MVP
session_report.questions_answered = questions_answered
session_report.all_turn_results = all_turn_results
return session_report
# handle a single turn with the Question object
def _handle_turn(self, question: Question, question_idx: int) -> TurnResult:
"""
Executes a single question turn.
Flow:
1. Display question to player via View
2. Collect player input
3. Route question + answer through evaluation_router
4. Build TurnResult containing:
- question metadata (id, type, answer type)
- evaluation output
5. Update Player state based on evaluation result
6. Provide feedback via View
7. Return TurnResult for session aggregation
Returns:
TurnResult: full record of the executed turn including evaluation outcome
"""
assert self.player is not None
player = self.player
hints_revealed = 0
console_lines = []
# 1. ask the question
# UX: pass current score to the view for the header
self.view.display_question_screen(question, question_idx, console_lines, player.score, player.get_chances)
#2. input loop — command routing
player_answer = None
while player_answer is None:
user_input = self.view.get_player_answer()
match user_input.lower():
case "hint":
if hints_revealed < 3: # silently ignore if already at max 3
hints_revealed += 1
console_lines = self.view.build_hint_lines(question, hints_revealed)
self.view.display_question_screen(question, question_idx,
console_lines, player.score, player.get_chances)
case "reference":
console_lines = self.view.build_reference_lines(question, hints_revealed)
self.view.display_question_screen(question, question_idx,
console_lines, player.score, player.get_chances)
case "quit":
raise UserWantsToQuit()
case _:
player_answer = user_input
# 3. Render evaluating state
console_lines = self.view.build_evaluating_lines(question, hints_revealed)
self.view.display_question_screen(question, question_idx, console_lines, player.score, player.get_chances)
# 4. Evaluate — spinner runs while router is working
with self.view.console.status("", spinner="dots"):
result = evaluation_router(player_answer, question)
# 5. build evaluation report for controller
turn_report = TurnResult(
question_idx = question_idx,
question_master_id = question.master_id,
question_type = question.question_type,
answer_type = question.answer_type,
correct_answer = question.answer,
player_answer = player_answer,
evaluation = result)
is_answer_correct = turn_report.evaluation.is_correct
# 6. update player score and state
if is_answer_correct is True:
player.add_score()
else:
player.lose_chance()
# 7. Render result
console_lines = self.view.build_result_lines(question, hints_revealed,
turn_report, player.get_chances)
self.view.display_question_screen(question, question_idx, console_lines, player.score, player.get_chances)
# 8. Pause before next question
self.view.wait_for_continue()
return turn_report
## End Game
# End game sequence
def _render_session_summary(self, session_report: SessionReport) -> None:
"""Orchestrates the session end sequence by calling the View."""
assert self.player is not None, "Player must be set during initialization"
total_questions = session_report.total_questions
# 0. display game-over message
self.view.display_game_over()
# 1. get player final score and display it
final_score = self.player.score
self.view.display_final_score(final_score, total_questions)
# 2. Get player rank and display rank and roast! :D
final_rank = self.player.find_player_wizard_rank(total_questions)
self.view.display_player_rank(final_rank, self.player)
self.view.display_final_housepoints(final_score, self.player)
return
# wrapper for main to display final goodbye
def display_goodbye(self, session_report: SessionReport):
"""
Orchestrates the display of a final goodbye message to the player.
This method is intended to be called by the main application runner
after the primary game loop has exited. It routes exit messages
based on session and game status.
"""
player_name = session_report.player_name
game_status = session_report.game_status
match game_status:
case GameStatus.COMPLETED | GameStatus.LOST:
# customized message with player name
self.view.display_goodbye(player_name)
case GameStatus.FAILED:
# A generic goodbye if there was no player
self.view.display_generic_goodbye()
case GameStatus.QUIT:
self.view.display_quit_message()
return