byte-vortex commited on
Commit
21f3232
·
verified ·
1 Parent(s): 1d0f23b

Delete ui/old--renderers.py

Browse files
Files changed (1) hide show
  1. ui/old--renderers.py +0 -686
ui/old--renderers.py DELETED
@@ -1,686 +0,0 @@
1
- """Markdown renderers for the Myco Gradio interface."""
2
-
3
- from html import escape
4
- from game.progression import (
5
- AREA_UNLOCKS, current_area, next_area, next_mystery,
6
- revealed_mysteries, unlocked_areas,
7
- )
8
- from game.state import CollectionState, EMPTY_DEX, MushroomState
9
- from models.mushroom import Mushroom
10
-
11
- import json
12
- import ast
13
-
14
- POISONOUS_MUSHROOM_NAMES = {
15
- "Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle",
16
- }
17
-
18
- SCENE_STYLES = """
19
- <style>
20
- .forest-scene {
21
- position: relative; isolation: isolate;
22
- overflow: hidden; min-height: 20rem;
23
- padding: 0.75rem; border-radius: 28px;
24
- background:
25
- radial-gradient(circle at 50% 72%, rgba(255,239,173,.26), transparent 8rem),
26
- radial-gradient(circle at 18% 24%, rgba(134,225,153,.22), transparent 10rem),
27
- linear-gradient(180deg,#123626 0%,#245c3c 54%,#523b20 100%);
28
- border: 1px solid rgba(225,255,209,.25);
29
- box-shadow: 0 18px 48px rgba(8,30,20,.26);
30
- color: #f9ffe9;
31
- }
32
- .scene-hud {
33
- position: relative; z-index: 3;
34
- display: flex; flex-wrap: wrap; gap: .4rem;
35
- padding: .5rem .75rem; border-radius: 999px;
36
- background: rgba(13,38,28,.72); backdrop-filter: blur(6px);
37
- font-size: .8rem; font-weight: 800;
38
- }
39
- .scene-stage {
40
- position: relative; height: 13rem;
41
- margin: .6rem 0;
42
- background:
43
- linear-gradient(transparent 62%, rgba(106,79,33,.55) 63%),
44
- radial-gradient(ellipse at bottom, rgba(190,255,180,.2), transparent 60%);
45
- border-radius: 22px; overflow: hidden;
46
- }
47
- .scene-stage span { position: absolute; line-height:1; }
48
- .tree-left { left:6%; bottom:18%; font-size:5rem; filter:drop-shadow(0 8px 12px rgba(0,0,0,.28)); }
49
- .tree-right { right:7%; bottom:20%; font-size:4.75rem; filter:drop-shadow(0 8px 12px rgba(0,0,0,.28)); }
50
- .grass-one { left:26%; bottom:14%; font-size:2.4rem; }
51
- .grass-two { right:28%;bottom:16%; font-size:2.2rem; }
52
- .sparkle-one { left:34%; top:18%; font-size:1.8rem; animation:twinkle 1.8s ease-in-out infinite; }
53
- .sparkle-two { right:33%; top:24%; font-size:1.6rem; animation:twinkle 2.2s ease-in-out infinite reverse; }
54
- @keyframes twinkle {
55
- 0%,100%{opacity:.3;transform:scale(.85);}
56
- 50%{opacity:1;transform:scale(1.18);}
57
- }
58
- .myco-player {
59
- font-size:4rem; z-index:2;
60
- transition: left .32s cubic-bezier(.34,1.56,.64,1), bottom .32s cubic-bezier(.34,1.56,.64,1);
61
- animation: myco-bob 2.4s ease-in-out infinite;
62
- filter:drop-shadow(0 6px 14px rgba(255,92,92,.28));
63
- }
64
- @keyframes myco-bob {
65
- 0%,100%{transform:translateY(0);}
66
- 50%{transform:translateY(-.45rem);}
67
- }
68
- .pos-0-0 .myco-player{left:14%;bottom:62%;}
69
- .pos-1-0 .myco-player{left:41%;bottom:62%;}
70
- .pos-2-0 .myco-player{left:67%;bottom:62%;}
71
- .pos-0-1 .myco-player{left:14%;bottom:36%;}
72
- .pos-1-1 .myco-player{left:41%;bottom:36%;}
73
- .pos-2-1 .myco-player{left:67%;bottom:36%;}
74
- .pos-0-2 .myco-player{left:14%;bottom:13%;}
75
- .pos-1-2 .myco-player{left:41%;bottom:13%;}
76
- .pos-2-2 .myco-player{left:67%;bottom:13%;}
77
- .mushroom-sprite {
78
- right:33%; bottom:22%; font-size:3.6rem; z-index:2;
79
- filter:drop-shadow(0 6px 14px rgba(0,0,0,.3));
80
- }
81
- .state-waiting .mushroom-sprite { opacity:0; }
82
- .state-discovered .mushroom-sprite {
83
- animation: shroom-popup 0.7s cubic-bezier(.34,1.56,.64,1) both;
84
- }
85
- .state-discovered .myco-player {
86
- animation: myco-bob 2.4s ease-in-out infinite,
87
- myco-run-in 0.6s cubic-bezier(.34,1.56,.64,1) both;
88
- }
89
- @keyframes shroom-popup {
90
- 0% { opacity:0; transform:translateY(3rem) scale(.2); }
91
- 60% { opacity:1; transform:translateY(-.4rem) scale(1.18); }
92
- 100% { opacity:1; transform:translateY(0) scale(1); }
93
- }
94
- @keyframes myco-run-in {
95
- 0% { transform:translateX(-3rem) scaleX(-1); }
96
- 100% { transform:translateX(0) scaleX(1); }
97
- }
98
- .state-picked .mushroom-sprite {
99
- animation: shroom-catch 0.55s cubic-bezier(.34,1.56,.64,1) both;
100
- }
101
- .state-picked .myco-player {
102
- animation: myco-bob 2.4s ease-in-out infinite, myco-catch-jump 0.55s ease both;
103
- }
104
- @keyframes shroom-catch {
105
- 0% { opacity:1; transform:translateY(0) scale(1); }
106
- 40% { opacity:1; transform:translateY(-2.5rem) scale(1.3) rotate(15deg); }
107
- 100% { opacity:0; transform:translateY(-5rem) scale(.2) rotate(40deg); }
108
- }
109
- @keyframes myco-catch-jump {
110
- 0%,100%{ transform:translateY(0); }
111
- 40% { transform:translateY(-1.4rem) scale(1.1); }
112
- }
113
- .state-danger .mushroom-sprite {
114
- animation: shroom-popup 0.7s cubic-bezier(.34,1.56,.64,1) both,
115
- poison-pulse 1.2s ease-in-out infinite 0.7s;
116
- filter: drop-shadow(0 0 18px rgba(255,62,91,.9));
117
- }
118
- .state-danger .myco-player {
119
- animation: myco-bob 2.4s ease-in-out infinite, myco-recoil 0.5s ease both;
120
- }
121
- @keyframes poison-pulse {
122
- 0%,100%{ filter:drop-shadow(0 0 10px rgba(255,62,91,.6)); }
123
- 50% { filter:drop-shadow(0 0 28px rgba(255,62,91,1)); }
124
- }
125
- @keyframes myco-recoil {
126
- 0% { transform:translateX(0); }
127
- 30% { transform:translateX(-1.2rem); }
128
- 60% { transform:translateX(.4rem); }
129
- 100%{ transform:translateX(0); }
130
- }
131
- .state-game-over .forest-scene,
132
- .state-game-over {
133
- animation: screen-shake .45s ease both;
134
- background:
135
- radial-gradient(circle at 50% 72%, rgba(255,62,91,.35), transparent 8rem),
136
- linear-gradient(180deg,#3b0a0a 0%,#6b1a1a 54%,#2a0a0a 100%) !important;
137
- }
138
- .state-game-over .mushroom-sprite {
139
- animation: shroom-explode .6s ease both;
140
- filter: drop-shadow(0 0 24px rgba(255,62,91,1));
141
- }
142
- .state-game-over .myco-player { animation: myco-fall .6s ease both; }
143
- @keyframes screen-shake {
144
- 0%,100%{ transform:translateX(0); }
145
- 20% { transform:translateX(-8px); }
146
- 40% { transform:translateX(8px); }
147
- 60% { transform:translateX(-5px); }
148
- 80% { transform:translateX(5px); }
149
- }
150
- @keyframes shroom-explode {
151
- 0% { transform:scale(1) rotate(0); opacity:1; }
152
- 50% { transform:scale(1.8) rotate(20deg); opacity:.8; }
153
- 100%{ transform:scale(0) rotate(90deg); opacity:0; }
154
- }
155
- @keyframes myco-fall {
156
- 0% { transform:rotate(0) translateY(0); opacity:1; }
157
- 100%{ transform:rotate(-90deg) translateY(2rem); opacity:.4; }
158
- }
159
- .rarity-rare .mushroom-sprite { filter:drop-shadow(0 0 18px rgba(138,220,255,.9)); }
160
- .rarity-legendary .mushroom-sprite { filter:drop-shadow(0 0 28px rgba(255,215,0,1)); animation:legendary-pulse 1s ease-in-out infinite 0.7s; }
161
- @keyframes legendary-pulse {
162
- 0%,100%{ filter:drop-shadow(0 0 18px rgba(255,215,0,.7)); transform:scale(1); }
163
- 50% { filter:drop-shadow(0 0 36px rgba(255,215,0,1)); transform:scale(1.1); }
164
- }
165
- .forest-map {
166
- display:grid; grid-template-columns:repeat(3,1fr);
167
- gap:.3rem; margin:.4rem auto .6rem;
168
- max-width:9rem; padding:.4rem;
169
- border:1px solid rgba(255,255,255,.18); border-radius:14px;
170
- background:rgba(13,38,28,.58);
171
- }
172
- .map-tile {
173
- display:grid; min-height:1.7rem; place-items:center;
174
- border-radius:8px; background:rgba(255,255,255,.1); font-weight:900;
175
- }
176
- .map-tile.active { background:rgba(255,224,138,.92); color:#3d2c00!important; }
177
- .scene-caption {
178
- display:flex; justify-content:space-between; gap:.6rem;
179
- padding:.55rem .8rem; border-radius:14px;
180
- background:rgba(13,38,28,.68); backdrop-filter:blur(6px);
181
- align-items:center;
182
- }
183
- .scene-caption span { color:#d9f4d4; }
184
- .score-burst {
185
- margin:.65rem auto 0; max-width:520px;
186
- padding:.5rem .85rem; border-radius:999px;
187
- background:rgba(255,224,112,.18); border:1px solid rgba(255,244,190,.36);
188
- color:#fff7d6; font-weight:900; text-align:center;
189
- animation:score-pop .56s ease both;
190
- }
191
- @keyframes score-pop {
192
- 0%{transform:scale(.92);opacity:0;}
193
- 45%{transform:scale(1.06);opacity:1;}
194
- 100%{transform:scale(1);opacity:1;}
195
- }
196
- .state-game-over .score-burst {
197
- background:rgba(130,0,30,.45); border-color:rgba(255,120,140,.55);
198
- }
199
- .story-moment {
200
- margin:.75rem auto 0; max-width:760px;
201
- padding:.8rem 1rem; border:1px solid rgba(255,234,166,.3);
202
- border-radius:18px; background:rgba(25,18,45,.72);
203
- color:#fff7d6; box-shadow:0 0 24px rgba(165,111,255,.22);
204
- }
205
- .story-moment strong,.story-moment span { display:block; }
206
- .story-moment span { margin-top:.2rem; color:#f6e7b2; }
207
- .generating .forest-scene, .pending .forest-scene { opacity:.88!important; }
208
- .progress-level,.progress-level-inner { display:none!important; }
209
- div.gradio-html { background:transparent!important; }
210
- .wrap.default.pending,.wrap.default.generating { background:transparent!important; }
211
-
212
- /* AI activity indicator */
213
- .ai-tick-badge {
214
- display:inline-block; padding:.15rem .55rem; border-radius:999px;
215
- background:rgba(80,220,120,.22); border:1px solid rgba(80,220,120,.4);
216
- color:#90ffb8; font-size:.72rem; font-weight:800;
217
- animation: ai-pulse 2s ease-in-out infinite;
218
- }
219
- @keyframes ai-pulse {
220
- 0%,100%{ opacity:.6; } 50%{ opacity:1; }
221
- }
222
- </style>
223
- """
224
-
225
-
226
- # ---------------------------------------------------------------------------
227
- # Static UI fragments
228
- # ---------------------------------------------------------------------------
229
-
230
- def hero_markdown() -> str:
231
- return """
232
- <div class="myco-hero">
233
- <div class="myco-mascot">🍄</div>
234
- <div>
235
- <p class="eyebrow">Tiny AI forest game</p>
236
- <h1>Myco</h1>
237
- <p>Follow an AI mushroom companion through a glowing forest. Every discovery starts with Myco's intuition.</p>
238
- </div>
239
- </div>
240
- """
241
-
242
- def home_forest_markdown() -> str:
243
- return """
244
- <div class="home-forest-postcard" aria-label="Animated Myco forest postcard">
245
- <style>
246
- .home-myco-wrapper, .home-brown-wrapper { position:relative; display:inline-block; }
247
- .home-spore {
248
- position:absolute; left:50%; top:20%; width:4px; height:4px;
249
- background:#a8ffb2; border-radius:50%; filter:blur(0.5px);
250
- box-shadow:0 0 6px #52ff6b,0 0 10px #fff;
251
- opacity:0; pointer-events:none;
252
- animation:floatHomeSpore var(--duration) ease-out infinite;
253
- animation-delay:var(--delay);
254
- }
255
- @keyframes floatHomeSpore {
256
- 0% { transform:translate(0,0) scale(1); opacity:0; }
257
- 15% { opacity:1; }
258
- 80% { opacity:0.7; }
259
- 100% { transform:translate(var(--x),var(--y)) scale(0.2); opacity:0; }
260
- }
261
- </style>
262
- <div class="home-sky">
263
- <span class="home-milky-way">🌌</span>
264
- <span class="home-star star-one">✨</span>
265
- <span class="home-star star-two">✦</span>
266
- <span class="home-star star-three">✧</span>
267
- </div>
268
- <div class="home-forest-floor">
269
- <span class="home-tree left">🌲</span>
270
- <span class="home-grass grass-left">🌿</span>
271
- <span class="home-myco-wrapper">
272
- <span class="home-myco">🍄</span>
273
- <span class="home-spore" style="--x:-25px;--y:-45px;--duration:3.5s;--delay:0s;"></span>
274
- <span class="home-spore" style="--x:20px;--y:-35px;--duration:4.2s;--delay:0.8s;"></span>
275
- <span class="home-spore" style="--x:-5px;--y:-60px;--duration:3s;--delay:1.5s;"></span>
276
- </span>
277
- <span class="home-brown-wrapper">
278
- <span class="home-brown-mushroom">🍄‍🟫</span>
279
- <span class="home-spore" style="--x:-15px;--y:-40px;--duration:3.8s;--delay:0.4s;background:#ffeaa7;box-shadow:0 0 6px #fdcb6e,0 0 10px #fff;"></span>
280
- <span class="home-spore" style="--x:25px;--y:-50px;--duration:4.5s;--delay:1.2s;background:#ffeaa7;box-shadow:0 0 6px #fdcb6e,0 0 10px #fff;"></span>
281
- </span>
282
- <span class="home-grass grass-right">🍃</span>
283
- <span class="home-tree right">🌳</span>
284
- <span class="home-moon">🌖</span>
285
- </div>
286
- <p><strong>The Milky Way is awake.</strong> Myco hears a brown mushroom whispering your first clue.</p>
287
- <span class="home-whisper">"Tap Play. Let's find what the forest is hiding."</span>
288
- </div>
289
- """
290
-
291
- def play_hook_markdown() -> str:
292
- return """
293
- <div class="play-hook">
294
- <p class="eyebrow">Mission</p>
295
- <h2>Goal: follow clues until Myco remembers the Impossible Mushroom.</h2>
296
- <div class="hook-steps">
297
- <span>1 · Move through the forest</span>
298
- <span>2 · Search Clearing — mushroom pops up!</span>
299
- <span>3 · Myco reacts · Pick, Study, or Follow Whisper</span>
300
- </div>
301
- <p>Every clue advances one mystery. Follow whispers to reveal why the MycoDex is regrowing a forest that vanished.</p>
302
- </div>
303
- """
304
-
305
- def game_intro_markdown() -> str:
306
- return """
307
- <div style="text-align:center;margin-bottom:.75rem;">
308
- <h3>🌲 The Deep Forest</h3>
309
- <p style="color:#359635;font-size:.9em;">
310
- You step into the glowing undergrowth. Myco hops off your shoulder, its cap faintly pulsing.<br>
311
- Click <strong>Search Clearing</strong> — watch Myco run toward whatever pops up.
312
- <span class="ai-tick-badge">🤖 Myco AI roaming</span>
313
- </p>
314
- </div>
315
- """
316
-
317
-
318
- # ---------------------------------------------------------------------------
319
- # Forest scene — main animated panel
320
- # ---------------------------------------------------------------------------
321
-
322
- def forest_scene(
323
- current: "MushroomState | None",
324
- collection: "CollectionState | None",
325
- position: "tuple[int,int] | None" = None,
326
- ) -> str:
327
- import time
328
- count = len(_safe_collection(collection))
329
- area = current_area(count)
330
- pos_x, pos_y = _safe_position(position)
331
- pos_cls = f"pos-{pos_x}-{pos_y}"
332
-
333
- # Safe defaults
334
- mushroom_name = "Hidden mushroom"
335
- rarity = "common"
336
- sprite = "✨"
337
- status = "The moss is listening. Click Search Clearing to wake the forest."
338
- omen = "Myco is waiting to sense the first spore."
339
- event_label = "🌌 Dreaming Forest"
340
- encounter = "The next clearing may reveal a memory, creature, or poison omen."
341
- mystery_label = "The Wrong Memory"
342
- score_total = "0"
343
- health = "3"
344
- reward_text = "Search a clearing to score spores."
345
- state_cls = "state-waiting"
346
-
347
- if current is not None:
348
- mushroom_name = current.get("name", "Mystery cluster")
349
- rarity = current.get("rarity", "Common").lower()
350
- game_over = current.get("game_over") == "Yes"
351
-
352
- event_label = f"{current.get('event_emoji','✨')} {current.get('event_title','Strange Omen')}"
353
- encounter = current.get("encounter", "Go pick them up!")
354
- mystery_label = current.get("mystery_title", "The Wrong Memory")
355
- score_total = current.get("score_total", "0")
356
- health = current.get("health", "3")
357
- reward_text = current.get("reward_text", "Dodge the poison, collect the rest!")
358
- state_cls = _scene_state_class(current)
359
-
360
- if game_over:
361
- status = "💀 GAME OVER: You stepped on a poisonous mushroom!"
362
- omen = "Press Search Clearing to start a new run."
363
- elif current.get("picked") == "Yes":
364
- status = "✅ Area clear! All safe mushrooms collected."
365
- omen = "Search the next clearing!"
366
- else:
367
- status = "Mushrooms sprouted! Move Myco over them to collect."
368
- omen = "🍄 Watch your step!"
369
-
370
- anim_key = str(int(time.time() * 1000))[-6:]
371
-
372
- return f'''{SCENE_STYLES}
373
- <div class="forest-scene rarity-{escape(rarity)} {pos_cls} {state_cls}" data-key="{anim_key}">
374
- <div class="scene-hud">
375
- <span>🧭 {escape(area.name)}</span>
376
- <span>🎮 ({pos_x+1},{pos_y+1})</span>
377
- <span>📖 {count} found</span>
378
- <span>⭐ {escape(str(score_total))}</span>
379
- <span>❤️ {escape(str(health))}/3</span>
380
- <span>{escape(event_label)}</span>
381
- <span>🧩 {escape(mystery_label)}</span>
382
- </div>
383
-
384
- <div class="scene-stage" aria-label="Magical forest scene">
385
- <span class="tree tree-left">🌲</span>
386
- <span class="tree tree-right">🌲</span>
387
- <span class="grass grass-one">🌿</span>
388
- <span class="grass grass-two">🍃</span>
389
- <span class="myco-player" title="Myco">🍄</span>
390
- <span class="mushroom-sprite" title="{escape(mushroom_name)}">{escape(sprite)}</span>
391
- <span class="sparkle sparkle-one">✨</span>
392
- <span class="sparkle sparkle-two">✦</span>
393
- <span class="wood-log">🪵</span>
394
- </div>
395
-
396
- <div class="forest-map" aria-label="Forest grid">
397
- {_movement_tiles(pos_x, pos_y, current)}
398
- </div>
399
-
400
- <div class="scene-caption">
401
- <strong>{escape(str(status))}</strong>
402
- <span>{escape(str(omen))}</span>
403
- </div>
404
- <div class="score-burst" aria-live="polite">{escape(str(reward_text))}</div>
405
- <div class="story-moment">
406
- <strong>{escape(str(event_label))}</strong>
407
- <span>{escape(str(encounter))}</span>
408
- </div>
409
- </div>'''
410
-
411
-
412
- # ---------------------------------------------------------------------------
413
- # Helpers
414
- # ---------------------------------------------------------------------------
415
-
416
- def _safe_position(position):
417
- if position is None:
418
- return (1, 1)
419
- try:
420
- x, y = position
421
- return (max(0, min(2, int(x))), max(0, min(2, int(y))))
422
- except Exception:
423
- return (1, 1)
424
-
425
-
426
- def _movement_tiles(active_x, active_y, current=None):
427
- """Draws the grid, safely decoding strings to ensure mushrooms always show up!"""
428
- mush_coords = {}
429
-
430
- # --- DEFENSIVE PARSING ---
431
- # If Gradio passed the state as a string, turn it back into a dictionary
432
- if isinstance(current, str):
433
- try:
434
- current = json.loads(current)
435
- except:
436
- try:
437
- current = ast.literal_eval(current)
438
- except:
439
- current = {} # Default to empty if parsing fails
440
-
441
- # Map out where the mushrooms are
442
- if current and isinstance(current, dict) and "active_mushrooms" in current:
443
- for m in current["active_mushrooms"]:
444
- # Ensure we are using ints for coordinates, just in case
445
- if m.get("picked") == "No" or m.get("picked") is False:
446
- mx, my = int(m["mush_x"]), int(m["mush_y"])
447
- mush_coords[(mx, my)] = m
448
-
449
- tiles = []
450
- for y in range(3):
451
- for x in range(3):
452
- # Ensure player coordinates are ints
453
- is_player = (int(x), int(y)) == (int(active_x), int(active_y))
454
- is_mush = (int(x), int(y)) in mush_coords
455
-
456
- active_class = " active" if is_player else ""
457
-
458
- if is_player:
459
- icon = "🍄" # Myco
460
- elif is_mush:
461
- # If it's a mushroom, show poison skull or magic sparkles
462
- m_data = mush_coords[(int(x), int(y))]
463
- icon = "☠️" if m_data.get("poison") == "Yes" else "✨"
464
- else:
465
- icon = "·" # Empty grass
466
-
467
- tiles.append(f'<span class="map-tile{active_class}">{icon}</span>')
468
-
469
- return "".join(tiles)
470
-
471
-
472
- def _scene_state_class(current):
473
- if not current:
474
- return "state-waiting"
475
- if current.get("game_over") == "Yes":
476
- return "state-game-over"
477
- if current.get("picked") == "Yes":
478
- return "state-picked"
479
- if current.get("poison") == "Yes":
480
- return "state-danger"
481
- return "state-discovered"
482
-
483
- def _safe_collection(collection):
484
- return list(collection or [])
485
-
486
-
487
- # ---------------------------------------------------------------------------
488
- # Game status panel
489
- # ---------------------------------------------------------------------------
490
-
491
- def game_status_markdown(
492
- current: "MushroomState | None",
493
- collection: "CollectionState | None",
494
- position: "tuple[int,int] | None" = None,
495
- ) -> str:
496
- count = len(_safe_collection(collection))
497
- pos_x, pos_y = _safe_position(position)
498
- active_mystery = next_mystery(count)
499
- mystery_line = _mystery_goal_line(active_mystery, count)
500
-
501
- if current is None:
502
- objective = "Goal: solve **one mystery** — why does Myco remember a vanished forest?"
503
- discovery = "No active discovery yet. Move Myco, then Search Clearing."
504
- ai_job = "Myco AI is roaming — watch it search, study, and collect automatically."
505
- event = "**Event:** waiting for the first omen"
506
- clue_line = mystery_line
507
- thread_line = "**Mystery thread:** The Wrong Memory — search the first clearing."
508
- bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
509
- score_line = "**Score:** 0 spores · **Health:** 3/3"
510
- else:
511
- name = current.get("name", "Mystery mushroom")
512
- studied = current.get("studied") == "Yes"
513
- game_over = current.get("game_over") == "Yes"
514
- picked = current.get("picked") == "Yes"
515
-
516
- if game_over:
517
- objective = "**💀 GAME OVER** — press **Search Clearing** to start a new run."
518
- elif picked:
519
- objective = "Current objective: **move to the next clearing and search again**."
520
- else:
521
- next_step = "Collect it to preserve the clue" if studied else "Study it, then Collect or Pick"
522
- objective = f"Current objective: **{next_step}**."
523
-
524
- bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
525
- score_line = (
526
- f"**Score:** {current.get('score_total','0')} spores · "
527
- f"**Health:** {current.get('health','3')}/3 · "
528
- f"**Last:** {current.get('reward_text','—')}"
529
- )
530
- discovery = f"Active discovery: **{name}** · Rarity: **{current.get('rarity','Unknown')}**"
531
- event = f"**Event:** {current.get('event_emoji','✨')} {current.get('event_title','Strange Omen')}"
532
- clue_line = f"**Clue:** {current.get('clue','Myco is still listening.')}"
533
- thread_line = (
534
- f"**Mystery thread:** {current.get('mystery_title','The Wrong Memory')} — "
535
- f"Next: {current.get('mystery_next','follow the whisper.')}"
536
- )
537
- ai_job = "Myco AI is watching — it will study and collect automatically while you explore."
538
-
539
- return "\n".join((
540
- "### 🎮 Quest Log",
541
- f"**MycoDex:** {count} discoveries · **Map:** ({pos_x+1},{pos_y+1})",
542
- discovery, objective, score_line, bloom_line,
543
- event, clue_line, thread_line, mystery_line, ai_job,
544
- ))
545
-
546
-
547
- # ---------------------------------------------------------------------------
548
- # Mushroom card
549
- # ---------------------------------------------------------------------------
550
-
551
- def mushroom_card(
552
- mushroom: "Mushroom | None",
553
- clue: "str | None" = None,
554
- secret: "str | None" = None,
555
- state: "MushroomState | None" = None,
556
- ) -> str:
557
- if mushroom is None:
558
- return "### 🌲 Forest Clearing\nClick **Search Clearing** — watch Myco run toward the mushroom!"
559
- s = state or {}
560
- poison = mushroom.name in POISONOUS_MUSHROOM_NAMES
561
- warning = "\n> ⚠️ **Myco senses danger — do NOT pick this without studying first!**" if poison else ""
562
- return "\n".join((
563
- f"### 🍄‍🟫 {mushroom.name}",
564
- f"**{mushroom.rarity} discovery** · found near **{mushroom.habitat}**",
565
- f"> {mushroom.lore}",
566
- warning,
567
- "",
568
- f"* **Edible:** {mushroom.edible}",
569
- f"* **Magic:** {mushroom.magic}",
570
- f"* **Danger:** {mushroom.danger}",
571
- f"* **Value:** {_mushroom_value_label(mushroom.rarity)}",
572
- f"* **Poison risk:** {_poison_risk_label(mushroom.name)}",
573
- f"* **Event:** {s.get('event_emoji','✨')} {s.get('event_title','Strange Omen')}",
574
- f"* **Mystery:** {s.get('mystery_title','The Wrong Memory')}",
575
- f"* **Clue:** {clue or 'Study with Myco to reveal the clue.'}",
576
- f"* **Secret:** {secret or 'Collect it to see how the MycoDex reacts.'}",
577
- f"* **Status:** {'⚠️ POISONOUS' if poison else '✅ Appears safe'}",
578
- ))
579
-
580
- def _mushroom_value_label(rarity):
581
- return {"Legendary": "100 spores", "Rare": "35 spores"}.get(rarity, "10 spores")
582
-
583
- def _poison_risk_label(name):
584
- return "⚠️ POISONOUS — Game Over if picked!" if name in POISONOUS_MUSHROOM_NAMES else "No obvious poison"
585
-
586
-
587
- # ---------------------------------------------------------------------------
588
- # World map
589
- # ---------------------------------------------------------------------------
590
-
591
- def world_map_markdown(collection: "CollectionState | None") -> str:
592
- count = len(_safe_collection(collection))
593
- cards = []
594
- for area in AREA_UNLOCKS:
595
- unlocked = count >= area.required_discoveries
596
- state = "unlocked" if unlocked else "locked"
597
- badge = "✅ Unlocked" if unlocked else f"🔒 {area.required_discoveries - count} away"
598
- cards.append("\n".join((
599
- f'<div class="map-card {state}">',
600
- ' <div class="map-card-top">',
601
- f' <strong>{escape(area.name)}</strong><span>{badge}</span>',
602
- ' </div>',
603
- f' <p>{escape(area.description)}</p>',
604
- '</div>',
605
- )))
606
- secrets = tuple(_secret_badge(t.name, count >= t.required_discoveries) for t in revealed_mysteries(count))
607
- if not secrets:
608
- secrets = (_secret_badge("First Spore", False),)
609
- return "\n".join((
610
- '<div class="world-map-panel">',
611
- ' <div class="world-map-header"><p class="eyebrow">World Map</p>',
612
- ' <h2>Forest Atlas</h2>',
613
- f' <p>{count} discoveries logged.</p></div>',
614
- f' <div class="achievement-row">{"".join(secrets)}</div>',
615
- f' <div class="map-grid">{"".join(cards)}</div>',
616
- '</div>',
617
- ))
618
-
619
- def _secret_badge(label, unlocked):
620
- state = "earned" if unlocked else "pending"
621
- icon = "🔎" if unlocked else "◇"
622
- return f'<span class="achievement {state}">{icon} {escape(label)}</span>'
623
-
624
-
625
- # ---------------------------------------------------------------------------
626
- # MycoDex
627
- # ---------------------------------------------------------------------------
628
-
629
- def dex_markdown(collection: "CollectionState | None") -> str:
630
- current_collection = _safe_collection(collection)
631
- if not current_collection:
632
- return EMPTY_DEX
633
- rows = ["### 📖 MycoDex", ""]
634
- for entry in current_collection:
635
- poison_flag = " ☠️" if entry.get("poison") == "Yes" else ""
636
- rows.append(
637
- f"* 🍄 **{entry['name']}**{poison_flag} — {entry['rarity']} — "
638
- f"{entry['habitat']} — clue: {entry.get('clue','not studied yet')}"
639
- )
640
- return "\n".join(rows)
641
-
642
-
643
- # ---------------------------------------------------------------------------
644
- # Progress
645
- # ---------------------------------------------------------------------------
646
-
647
- def progress_markdown(collection: "CollectionState | None") -> str:
648
- count = len(_safe_collection(collection))
649
- active_area = current_area(count)
650
- upcoming_area = next_area(count)
651
- unlocked_names = ", ".join(a.name for a in unlocked_areas(count))
652
- rows = [
653
- "### ✨ Forest Progress",
654
- f"**MycoDex discoveries:** {count}",
655
- f"**Current area:** {active_area.name} — {active_area.description}",
656
- f"**Unlocked:** {unlocked_names}",
657
- ]
658
- upcoming_mystery = next_mystery(count)
659
- if upcoming_area:
660
- rows.append(f"**Next area:** {upcoming_area.required_discoveries - count} more to unlock **{upcoming_area.name}**.")
661
- else:
662
- rows.append(f"**All areas visible.** You reached **{AREA_UNLOCKS[-1].name}**.")
663
- if upcoming_mystery:
664
- rows.append(f"**Next secret:** {upcoming_mystery.required_discoveries - count} more to reveal **{upcoming_mystery.name}**.")
665
- else:
666
- rows.append("**All current secrets found.** Myco thinks the Elder is watching kindly.")
667
- return "\n\n".join(rows)
668
-
669
-
670
- # ---------------------------------------------------------------------------
671
- # Helpers
672
- # ---------------------------------------------------------------------------
673
-
674
- def _mystery_goal_line(active_mystery, count):
675
- if active_mystery is None:
676
- return "**Mystery:** Every known clue is awake. Keep exploring for rare surprises."
677
- remaining = active_mystery.required_discoveries - count
678
- return f"**Mystery:** {remaining} clue discoveries until **{active_mystery.name}** — {active_mystery.clue}"
679
-
680
- def _mystery_bloom_meter(count):
681
- filled = max(0, min(5, count))
682
- petals = "".join("🌕" if i < filled else "🌑" for i in range(5))
683
- if filled >= 5:
684
- return f"{petals} Impossible Bloom visible"
685
- return f"{petals} {5 - filled} clue-whispers remain"
686
-