File size: 21,235 Bytes
e75654e
 
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2703c4f
 
 
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2703c4f
7346308
 
10de4ed
 
7346308
10de4ed
 
7346308
10de4ed
7346308
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
7346308
 
 
10de4ed
2703c4f
7346308
10de4ed
 
 
 
 
 
 
 
 
 
 
2703c4f
10de4ed
 
 
2703c4f
 
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7346308
 
2703c4f
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2703c4f
10de4ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7346308
10de4ed
 
 
7346308
10de4ed
 
 
 
 
 
 
 
 
 
 
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
def update_game(choice, game_state_json):
    """Update game based on player choice"""
    try: # Wrap major logic in try-except
        if not choice: # Handle empty choice if dropdown value is cleared
            # Return updates that don't change anything
            return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()

        # Parse game state from JSON
        game_state_dict = json.loads(game_state_json)
        game_state = GameState.from_dict(game_state_dict)

        # Get current page data
        current_page = game_state.current_page
        current_page_str = str(current_page)

        if current_page_str not in game_data:
             # Handle invalid page number
             error_content = f"<p>Error: Reached invalid page number {current_page}. Resetting game.</p>"
             svg_error = create_svg_illustration("default")
             new_game_state = GameState()
             return (svg_error, error_content, gr.Dropdown(choices=["Restart"], label="Error", value=None), new_game_state.to_json(),
                     generate_stats_display(new_game_state), generate_inventory_display(new_game_state), "Error - Resetting")

        page_data = game_data[current_page_str]

        # Find the selected option
        selected_option = None
        current_options_list = page_data.get("options", [])
        for opt in current_options_list:
            # Ensure 'text' key exists in option dictionary
            if isinstance(opt, dict) and opt.get("text") == choice:
                selected_option = opt
                break

        # Check alternative option if it was displayed and chosen
        alt_opt_data = page_data.get("alternativeOption")
        if not selected_option and isinstance(alt_opt_data, dict) and alt_opt_data.get("text") == choice:
             # Check if the alternative option should have been available
             # This logic needs refinement: check if primary options were filtered out *and* alt condition met
             alt_show_if = alt_opt_data.get("showIf")
             current_inventory = game_state.inventory
             primary_options_available = False
             if current_options_list:
                  for opt in current_options_list:
                       is_available = True
                       req_item = opt.get("requireItem")
                       if req_item and req_item not in current_inventory:
                           is_available = False
                       req_any = opt.get("requireAnyItem")
                       if is_available and req_any and not any(i in current_inventory for i in req_any):
                           is_available = False
                       if is_available:
                           primary_options_available = True
                           break # Found at least one available primary option

             # Select alternative only if no primary options were available AND showIf condition is met
             if not primary_options_available and (not alt_show_if or any(item in current_inventory for item in alt_show_if)):
                  selected_option = alt_opt_data


        if selected_option is None:
            # Handle case where choice might be "Restart" or invalid
            if choice == "Restart":
                 return initialize_game()
            else:
                # Stay on the same page if the choice wasn't found
                options = [opt.get("text", "Invalid Option") for opt in current_options_list]
                if alt_opt_data and alt_opt_data.get("text"): # Add alternative option text if it exists
                    # Check showIf condition for alternative option display
                    alt_show_if = alt_opt_data.get("showIf")
                    current_inventory = game_state.inventory
                    if not alt_show_if or any(item in current_inventory for item in alt_show_if):
                         # Check if primary options were filtered out
                         primary_options_available = False
                         if current_options_list:
                             # Re-check primary options availability
                             for opt in current_options_list:
                                 is_available = True
                                 req_item = opt.get("requireItem")
                                 if req_item and req_item not in current_inventory: is_available = False
                                 req_any = opt.get("requireAnyItem")
                                 if is_available and req_any and not any(i in current_inventory for i in req_any): is_available = False
                                 if is_available:
                                     primary_options_available = True
                                     break
                         if not primary_options_available:
                             options.append(alt_opt_data["text"])

                if not options: options = ["Restart"]

                svg_content = create_svg_illustration(page_data.get("illustration", "default"))
                content = f"<h2>{page_data.get('title', 'Error')}</h2>" + page_data.get("content", "") + f"<p><i>Debug: Choice '{choice}' not matched to any available option.</i></p>"
                stats_display = generate_stats_display(game_state)
                inventory_display = generate_inventory_display(game_state)
                story_path_display = f"You remain on page {current_page}: {page_data.get('title', 'Error')}"
                return svg_content, content, gr.Dropdown(choices=options, value=None), game_state.to_json(), stats_display, inventory_display, story_path_display

        # --- Option requirement checks ---
        item_required = selected_option.get("requireItem")
        any_item_required = selected_option.get("requireAnyItem")
        current_inventory = game_state.inventory # Cache inventory

        # Check specific item requirement
        if item_required and item_required not in current_inventory:
            content = f"<h2>{page_data['title']}</h2>" + page_data["content"]
            content += f"<p style='color: red;'>You need the <strong>{item_required}</strong> for this option, but you don't have it.</p>"
            options_texts = [opt.get("text") for opt in page_data.get("options", []) if opt.get("text")]
            # Check if alt option should be added back
            if alt_opt_data and alt_opt_data.get("text"):
                alt_show_if = alt_opt_data.get("showIf")
                if not alt_show_if or any(item in current_inventory for item in alt_show_if):
                     primary_available_check = any(
                         opt.get("text") and (not opt.get("requireItem") or opt.get("requireItem") in current_inventory) and \
                         (not opt.get("requireAnyItem") or any(i in current_inventory for i in opt.get("requireAnyItem")))
                         for opt in page_data.get("options", [])
                     )
                     if not primary_available_check: options_texts.append(alt_opt_data["text"])

            if not options_texts: options_texts = ["Restart"]
            svg_content = create_svg_illustration(page_data.get("illustration", "default"))
            stats_display = generate_stats_display(game_state)
            inventory_display = generate_inventory_display(game_state)
            story_path_display = f"You remain on page {current_page}: {page_data['title']}"
            return svg_content, content, gr.Dropdown(choices=list(set(options_texts)), value=None), game_state.to_json(), stats_display, inventory_display, story_path_display # Use set to remove duplicates

        # Check 'any item' requirement
        if any_item_required and not any(item in current_inventory for item in any_item_required):
            item_list = ", ".join(f"'{item}'" for item in any_item_required)
            content = f"<h2>{page_data['title']}</h2>" + page_data["content"]
            content += f"<p style='color: red;'>You need one of the following items for this option: <strong>{item_list}</strong>, but you don't have any.</p>"
            options_texts = [opt.get("text") for opt in page_data.get("options", []) if opt.get("text")]
             # Check if alt option should be added back (similar logic as above)
            if alt_opt_data and alt_opt_data.get("text"):
                alt_show_if = alt_opt_data.get("showIf")
                if not alt_show_if or any(item in current_inventory for item in alt_show_if):
                     primary_available_check = any(
                         opt.get("text") and (not opt.get("requireItem") or opt.get("requireItem") in current_inventory) and \
                         (not opt.get("requireAnyItem") or any(i in current_inventory for i in opt.get("requireAnyItem")))
                         for opt in page_data.get("options", [])
                     )
                     if not primary_available_check: options_texts.append(alt_opt_data["text"])

            if not options_texts: options_texts = ["Restart"]
            svg_content = create_svg_illustration(page_data.get("illustration", "default"))
            stats_display = generate_stats_display(game_state)
            inventory_display = generate_inventory_display(game_state)
            story_path_display = f"You remain on page {current_page}: {page_data['title']}"
            return svg_content, content, gr.Dropdown(choices=list(set(options_texts)), value=None), game_state.to_json(), stats_display, inventory_display, story_path_display


        # --- Process valid choice ---
        item_to_add = selected_option.get("addItem")
        if item_to_add and item_to_add not in game_state.inventory:
            game_state.inventory.append(item_to_add)
            item_data = items_data.get(item_to_add, {})
            if item_data.get("type") == "consumable" and item_data.get("useOnAdd"):
                 if "hpRestore" in item_data:
                      hp_before = game_state.current_hp
                      game_state.current_hp = min(game_state.max_hp, game_state.current_hp + item_data["hpRestore"])
                      # Maybe add feedback about using the item? Add later if needed.
                 game_state.inventory.remove(item_to_add) # Remove consumable after use

        # Move to the next page (initial move before battle/challenge checks)
        next_page = selected_option["next"]
        next_page_str = str(next_page)

        if next_page_str not in game_data:
             error_content = f"<p>Error: Option leads to invalid page number {next_page}. Resetting game.</p>"
             svg_error = create_svg_illustration("default")
             new_game_state = GameState()
             return (svg_error, error_content, gr.Dropdown(choices=["Restart"], label="Error", value=None), new_game_state.to_json(),
                     generate_stats_display(new_game_state), generate_inventory_display(new_game_state), "Error - Resetting")

        # --- Battle Check ---
        battle_occurred = False
        battle_message = ""
        battle_result = True

        # Check random battle flag on the *current* page data (page_data before challenge potentially changes it)
        if page_data.get("randomBattle", False) and random.random() < 0.2:
            battle_result, battle_log = simulate_battle(game_state)
            battle_occurred = True
            if not battle_result:
                content = "<h2>Game Over</h2><p>You have been defeated in battle!</p>" + battle_log
                stats_display = generate_stats_display(game_state)
                inventory_display = generate_inventory_display(game_state)
                return create_svg_illustration("game-over"), content, gr.Dropdown(choices=["Restart"], label="Game Over", value=None), game_state.to_json(), stats_display, inventory_display, "Game Over - You were defeated in battle."
            else:
                 battle_message = f"<div style='border: 1px solid green; padding: 10px; margin-top: 10px; background-color: #e8f5e9;'><strong>Battle Won!</strong>{battle_log}</div>"

        # --- Set current page to the determined next page (before challenge check) ---
        game_state.current_page = next_page
        if next_page not in game_state.visited_pages:
            game_state.visited_pages.append(next_page)

        # Update journey progress
        game_state.journey_progress += 5
        if game_state.journey_progress > 100: game_state.journey_progress = 100

        # Get data for the potentially final destination page (next_page_str)
        page_data = game_data[next_page_str] # Now refers to the data of the page we are landing on

        # --- HP Loss / Stat Increase on Landing Page ---
        stat_increase = page_data.get("statIncrease")
        if stat_increase:
            stat = stat_increase.get("stat")
            amount = stat_increase.get("amount")
            if stat and amount and stat in game_state.stats:
                 game_state.stats[stat] += amount

        hp_loss = page_data.get("hpLoss")
        if hp_loss:
            game_state.current_hp -= hp_loss
            if game_state.current_hp <= 0:
                game_state.current_hp = 0
                content = "<h2>Game Over</h2><p>You have died from your wounds!</p>"
                stats_display = generate_stats_display(game_state)
                inventory_display = generate_inventory_display(game_state)
                return create_svg_illustration("game-over"), content, gr.Dropdown(choices=["Restart"], label="Game Over", value=None), game_state.to_json(), stats_display, inventory_display, "Game Over - You died from your wounds."


        # --- Challenge Check on Landing Page ---
        challenge_log = ""
        challenge_occurred = False
        challenge = page_data.get("challenge")
        if challenge:
            challenge_occurred = True
            req_stat = challenge.get("stat")
            req_difficulty = challenge.get("difficulty")
            req_success_page = challenge.get("success")
            req_failure_page = challenge.get("failure")

            if not all([req_stat, req_difficulty, req_success_page is not None, req_failure_page is not None]): # Check pages can be 0
                 challenge_log = "<div style='color: orange;'>Challenge data incomplete.</div>"
                 challenge = None # Skip challenge processing
            else:
                success, roll, total = perform_challenge(game_state, challenge)
                challenge_log = f"<div style='border: 1px solid #ccc; padding: 10px; margin-top: 10px; background-color: #eee;'>"
                challenge_log += f"<strong>Challenge: {challenge.get('title', 'Skill Check')}</strong><br>"
                challenge_log += f"Target Stat: {req_stat}, Difficulty: {req_difficulty}<br>"
                stat_val_before_roll = total - roll # Stat value used in the roll
                challenge_log += f"You rolled a {roll} + ({stat_val_before_roll} {req_stat}) = {total}<br>"

                if success:
                    challenge_log += "<strong style='color: green;'>Success!</strong></div>"
                    next_page = req_success_page
                else:
                    # *** THIS IS THE CORRECTED LINE (around line 208 of app.py) ***
                    challenge_log += "<strong style='color: red;'>Failure!</strong></div>"
                    # **************************************************************
                    next_page = req_failure_page

                # Update game state again based on challenge outcome
                game_state.current_page = next_page
                next_page_str = str(next_page)

                if next_page_str not in game_data:
                    error_content = f"<p>Error: Challenge outcome leads to invalid page {next_page}. Resetting.</p>"
                    svg_error = create_svg_illustration("default")
                    new_game_state = GameState()
                    return (svg_error, error_content, gr.Dropdown(choices=["Restart"], label="Error", value=None), new_game_state.to_json(),
                            generate_stats_display(new_game_state), generate_inventory_display(new_game_state), "Error - Resetting")

                if next_page not in game_state.visited_pages:
                    game_state.visited_pages.append(next_page)
                # Load the *final* destination page data after challenge resolution
                page_data = game_data[next_page_str]


        # --- Prepare Final Output ---
        final_page_title = page_data.get('title', 'Untitled')
        svg_content = create_svg_illustration(page_data.get("illustration", "default"))

        # Handle game over on the final destination page
        if page_data.get("gameOver", False):
            content = f"<h2>{final_page_title}</h2>"
            content += page_data.get("content", "")
            if battle_occurred and battle_result: content += battle_message
            if challenge_occurred: content += challenge_log
            if "ending" in page_data:
                content += f"<div style='text-align: center; margin-top: 20px; font-weight: bold; color: #c00;'>THE END</div>"
                content += f"<p style='font-style: italic;'>{page_data['ending']}</p>"

            stats_display = generate_stats_display(game_state)
            inventory_display = generate_inventory_display(game_state)
            return svg_content, content, gr.Dropdown(choices=["Restart"], label="Game Over", value=None), game_state.to_json(), stats_display, inventory_display, "Game Over - " + final_page_title

        # Build content for the final destination page (if not game over)
        content = f"<h2>{final_page_title}</h2>"
        content += page_data.get("content", "<p>No content.</p>")
        if battle_occurred and battle_result: content += battle_message
        if challenge_occurred: content += challenge_log

        # Build options for the final destination page
        options_texts = []
        current_inventory = game_state.inventory # Cache inventory again
        final_page_options = page_data.get("options", [])
        if final_page_options:
            for opt in final_page_options:
                option_available = True
                if isinstance(opt, dict): # Ensure opt is a dictionary
                    req_item = opt.get("requireItem")
                    if req_item and req_item not in current_inventory:
                         option_available = False
                    req_any_item = opt.get("requireAnyItem")
                    if option_available and req_any_item:
                        if not any(item in current_inventory for item in req_any_item):
                             option_available = False
                    if option_available and opt.get("text"):
                        options_texts.append(opt["text"])
                # else: skip invalid option format

        # Handle alternative option if necessary (only if no primary options were available/visible)
        alt_opt_data = page_data.get("alternativeOption")
        if isinstance(alt_opt_data, dict) and alt_opt_data.get("text") and not options_texts: # Check if text exists and no primary options shown
            alt_show_if = alt_opt_data.get("showIf")
            if not alt_show_if or any(item in current_inventory for item in alt_show_if):
                options_texts.append(alt_opt_data["text"])


        if not options_texts: # If still no options (dead end?)
            options_texts = ["Restart"]
            content += "<p><i>There are no further actions you can take from here.</i></p>"

        # Update story path display text
        story_path = f"You are on page {next_page}: {final_page_title}"
        if game_state.journey_progress >= 80: story_path += " (Nearing the conclusion)"
        elif game_state.journey_progress >= 50: story_path += " (Middle of your journey)"
        elif game_state.journey_progress >= 25: story_path += " (Adventure beginning)"

        # Generate final displays
        stats_display = generate_stats_display(game_state)
        inventory_display = generate_inventory_display(game_state)

        # Return final state
        return (svg_content, content, gr.Dropdown(choices=list(set(options_texts)), label="What will you do?", value=None),
                game_state.to_json(), stats_display, inventory_display, story_path)

    except Exception as e:
         # Generic error handling for the entire update function
         print(f"Error during update_game: {e}")
         import traceback
         traceback.print_exc() # Print detailed traceback to console (if possible in Gradio Lite)
         error_content = f"<p>A critical error occurred: {e}. Resetting game state.</p>"
         svg_error = create_svg_illustration("default")
         new_game_state = GameState() # Reset state
         return (svg_error, error_content, gr.Dropdown(choices=["Restart"], label="Critical Error", value=None), new_game_state.to_json(),
                 generate_stats_display(new_game_state), generate_inventory_display(new_game_state), "Critical Error - Reset")