byte-vortex commited on
Commit
a9bf127
·
verified ·
1 Parent(s): dd7b94a

Deploy Myco from CI

Browse files
Files changed (2) hide show
  1. ui/copy_renderers.py +592 -0
  2. ui/scene_renderers.py +5 -11
ui/copy_renderers.py ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ POISONOUS_MUSHROOM_NAMES = {
12
+ "Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle",
13
+ }
14
+
15
+ # ── Inline CSS + keyframes injected once into the scene ──────────────────────
16
+ SCENE_STYLES = """
17
+ <style>
18
+ /* ── Base scene ── */
19
+ .forest-scene {
20
+ position: relative; isolation: isolate;
21
+ overflow: hidden; min-height: 20rem;
22
+ padding: 0.75rem; border-radius: 28px;
23
+ background:
24
+ radial-gradient(circle at 50% 72%, rgba(255,239,173,.26), transparent 8rem),
25
+ radial-gradient(circle at 18% 24%, rgba(134,225,153,.22), transparent 10rem),
26
+ linear-gradient(180deg,#123626 0%,#245c3c 54%,#523b20 100%);
27
+ border: 1px solid rgba(225,255,209,.25);
28
+ box-shadow: 0 18px 48px rgba(8,30,20,.26);
29
+ color: #f9ffe9;
30
+ }
31
+
32
+ /* ── HUD bar ── */
33
+ .scene-hud {
34
+ position: relative; z-index: 3;
35
+ display: flex; flex-wrap: wrap; gap: .4rem;
36
+ padding: .5rem .75rem; border-radius: 999px;
37
+ background: rgba(13,38,28,.72); backdrop-filter: blur(6px);
38
+ font-size: .8rem; font-weight: 800;
39
+ }
40
+
41
+ /* ── Stage ── */
42
+ .scene-stage {
43
+ position: relative; height: 13rem;
44
+ margin: .6rem 0;
45
+ background:
46
+ linear-gradient(transparent 62%, rgba(106,79,33,.55) 63%),
47
+ radial-gradient(ellipse at bottom, rgba(190,255,180,.2), transparent 60%);
48
+ border-radius: 22px; overflow: hidden;
49
+ }
50
+ .scene-stage span { position: absolute; line-height:1; }
51
+
52
+ /* ── Trees / grass ── */
53
+ .tree-left { left:6%; bottom:18%; font-size:5rem; filter:drop-shadow(0 8px 12px rgba(0,0,0,.28)); }
54
+ .tree-right { right:7%; bottom:20%; font-size:4.75rem; filter:drop-shadow(0 8px 12px rgba(0,0,0,.28)); }
55
+ .grass-one { left:26%; bottom:14%; font-size:2.4rem; }
56
+ .grass-two { right:28%;bottom:16%; font-size:2.2rem; }
57
+
58
+ /* ── Sparkles ── */
59
+ .sparkle-one { left:34%; top:18%; font-size:1.8rem; animation:twinkle 1.8s ease-in-out infinite; }
60
+ .sparkle-two { right:33%; top:24%; font-size:1.6rem; animation:twinkle 2.2s ease-in-out infinite reverse; }
61
+ @keyframes twinkle {
62
+ 0%,100%{opacity:.3;transform:scale(.85);}
63
+ 50%{opacity:1;transform:scale(1.18);}
64
+ }
65
+
66
+ /* ── Myco player — position grid ── */
67
+ .myco-player {
68
+ font-size:4rem; z-index:2;
69
+ transition: left .32s cubic-bezier(.34,1.56,.64,1), bottom .32s cubic-bezier(.34,1.56,.64,1);
70
+ animation: myco-bob 2.4s ease-in-out infinite;
71
+ filter:drop-shadow(0 6px 14px rgba(255,92,92,.28));
72
+ }
73
+ @keyframes myco-bob {
74
+ 0%,100%{transform:translateY(0);}
75
+ 50%{transform:translateY(-.45rem);}
76
+ }
77
+ .pos-0-0 .myco-player{left:14%;bottom:62%;}
78
+ .pos-1-0 .myco-player{left:41%;bottom:62%;}
79
+ .pos-2-0 .myco-player{left:67%;bottom:62%;}
80
+ .pos-0-1 .myco-player{left:14%;bottom:36%;}
81
+ .pos-1-1 .myco-player{left:41%;bottom:36%;}
82
+ .pos-2-1 .myco-player{left:67%;bottom:36%;}
83
+ .pos-0-2 .myco-player{left:14%;bottom:13%;}
84
+ .pos-1-2 .myco-player{left:41%;bottom:13%;}
85
+ .pos-2-2 .myco-player{left:67%;bottom:13%;}
86
+
87
+ /* ── Mushroom sprite ── */
88
+ .mushroom-sprite {
89
+ right:33%; bottom:22%; font-size:3.6rem; z-index:2;
90
+ filter:drop-shadow(0 6px 14px rgba(0,0,0,.3));
91
+ }
92
+
93
+ /* ── STATE: waiting (no mushroom) ── */
94
+ .state-waiting .mushroom-sprite { opacity:0; }
95
+
96
+ /* ── STATE: discovered — mushroom pops up, Myco runs toward it ── */
97
+ .state-discovered .mushroom-sprite {
98
+ animation: shroom-popup 0.7s cubic-bezier(.34,1.56,.64,1) both;
99
+ }
100
+ .state-discovered .myco-player {
101
+ animation: myco-bob 2.4s ease-in-out infinite,
102
+ myco-run-in 0.6s cubic-bezier(.34,1.56,.64,1) both;
103
+ }
104
+ @keyframes shroom-popup {
105
+ 0% { opacity:0; transform:translateY(3rem) scale(.2); }
106
+ 60% { opacity:1; transform:translateY(-.4rem) scale(1.18); }
107
+ 100% { opacity:1; transform:translateY(0) scale(1); }
108
+ }
109
+ @keyframes myco-run-in {
110
+ 0% { transform:translateX(-3rem) scaleX(-1); }
111
+ 100% { transform:translateX(0) scaleX(1); }
112
+ }
113
+
114
+ /* ── STATE: picked — mushroom flies up and disappears ── */
115
+ .state-picked .mushroom-sprite {
116
+ animation: shroom-catch 0.55s cubic-bezier(.34,1.56,.64,1) both;
117
+ }
118
+ .state-picked .myco-player {
119
+ animation: myco-bob 2.4s ease-in-out infinite,
120
+ myco-catch-jump 0.55s ease both;
121
+ }
122
+ @keyframes shroom-catch {
123
+ 0% { opacity:1; transform:translateY(0) scale(1); }
124
+ 40% { opacity:1; transform:translateY(-2.5rem) scale(1.3) rotate(15deg); }
125
+ 100% { opacity:0; transform:translateY(-5rem) scale(.2) rotate(40deg); }
126
+ }
127
+ @keyframes myco-catch-jump {
128
+ 0%,100%{ transform:translateY(0); }
129
+ 40% { transform:translateY(-1.4rem) scale(1.1); }
130
+ }
131
+
132
+ /* ── STATE: danger (poison, not yet picked) ── */
133
+ .state-danger .mushroom-sprite {
134
+ animation: shroom-popup 0.7s cubic-bezier(.34,1.56,.64,1) both,
135
+ poison-pulse 1.2s ease-in-out infinite 0.7s;
136
+ filter: drop-shadow(0 0 18px rgba(255,62,91,.9));
137
+ }
138
+ .state-danger .myco-player {
139
+ animation: myco-bob 2.4s ease-in-out infinite,
140
+ myco-recoil 0.5s ease both;
141
+ }
142
+ @keyframes poison-pulse {
143
+ 0%,100%{ filter:drop-shadow(0 0 10px rgba(255,62,91,.6)); }
144
+ 50% { filter:drop-shadow(0 0 28px rgba(255,62,91,1)); }
145
+ }
146
+ @keyframes myco-recoil {
147
+ 0% { transform:translateX(0); }
148
+ 30% { transform:translateX(-1.2rem); }
149
+ 60% { transform:translateX(.4rem); }
150
+ 100%{ transform:translateX(0); }
151
+ }
152
+
153
+ /* ── STATE: game over ── */
154
+ .state-game-over .forest-scene,
155
+ .state-game-over {
156
+ animation: screen-shake .45s ease both;
157
+ background:
158
+ radial-gradient(circle at 50% 72%, rgba(255,62,91,.35), transparent 8rem),
159
+ linear-gradient(180deg,#3b0a0a 0%,#6b1a1a 54%,#2a0a0a 100%) !important;
160
+ }
161
+ .state-game-over .mushroom-sprite {
162
+ animation: shroom-explode .6s ease both;
163
+ filter: drop-shadow(0 0 24px rgba(255,62,91,1));
164
+ }
165
+ .state-game-over .myco-player {
166
+ animation: myco-fall .6s ease both;
167
+ }
168
+ @keyframes screen-shake {
169
+ 0%,100%{ transform:translateX(0); }
170
+ 20% { transform:translateX(-8px); }
171
+ 40% { transform:translateX(8px); }
172
+ 60% { transform:translateX(-5px); }
173
+ 80% { transform:translateX(5px); }
174
+ }
175
+ @keyframes shroom-explode {
176
+ 0% { transform:scale(1) rotate(0); opacity:1; }
177
+ 50% { transform:scale(1.8) rotate(20deg); opacity:.8; }
178
+ 100%{ transform:scale(0) rotate(90deg); opacity:0; }
179
+ }
180
+ @keyframes myco-fall {
181
+ 0% { transform:rotate(0) translateY(0); opacity:1; }
182
+ 100%{ transform:rotate(-90deg) translateY(2rem); opacity:.4; }
183
+ }
184
+
185
+ /* ── Rarity glows ── */
186
+ .rarity-rare .mushroom-sprite { filter:drop-shadow(0 0 18px rgba(138,220,255,.9)); }
187
+ .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; }
188
+ @keyframes legendary-pulse {
189
+ 0%,100%{ filter:drop-shadow(0 0 18px rgba(255,215,0,.7)); transform:scale(1); }
190
+ 50% { filter:drop-shadow(0 0 36px rgba(255,215,0,1)); transform:scale(1.1); }
191
+ }
192
+
193
+ /* ── Mini map grid ── */
194
+ .forest-map {
195
+ display:grid; grid-template-columns:repeat(3,1fr);
196
+ gap:.3rem; margin:.4rem auto .6rem;
197
+ max-width:9rem; padding:.4rem;
198
+ border:1px solid rgba(255,255,255,.18); border-radius:14px;
199
+ background:rgba(13,38,28,.58);
200
+ }
201
+ .map-tile {
202
+ display:grid; min-height:1.7rem; place-items:center;
203
+ border-radius:8px; background:rgba(255,255,255,.1); font-weight:900;
204
+ }
205
+ .map-tile.active { background:rgba(255,224,138,.92); color:#3d2c00!important; }
206
+
207
+ /* ── Caption ── */
208
+ .scene-caption {
209
+ display:flex; justify-content:space-between; gap:.6rem;
210
+ padding:.55rem .8rem; border-radius:14px;
211
+ background:rgba(13,38,28,.68); backdrop-filter:blur(6px);
212
+ align-items:center;
213
+ }
214
+ .scene-caption span { color:#d9f4d4; }
215
+
216
+ /* ── Score burst ── */
217
+ .score-burst {
218
+ margin:.65rem auto 0; max-width:520px;
219
+ padding:.5rem .85rem; border-radius:999px;
220
+ background:rgba(255,224,112,.18); border:1px solid rgba(255,244,190,.36);
221
+ color:#fff7d6; font-weight:900; text-align:center;
222
+ animation:score-pop .56s ease both;
223
+ }
224
+ @keyframes score-pop {
225
+ 0%{transform:scale(.92);opacity:0;}
226
+ 45%{transform:scale(1.06);opacity:1;}
227
+ 100%{transform:scale(1);opacity:1;}
228
+ }
229
+ .state-game-over .score-burst {
230
+ background:rgba(130,0,30,.45); border-color:rgba(255,120,140,.55);
231
+ }
232
+
233
+ /* ── Story moment ── */
234
+ .story-moment {
235
+ margin:.75rem auto 0; max-width:760px;
236
+ padding:.8rem 1rem; border:1px solid rgba(255,234,166,.3);
237
+ border-radius:18px; background:rgba(25,18,45,.72);
238
+ color:#fff7d6; box-shadow:0 0 24px rgba(165,111,255,.22);
239
+ }
240
+ .story-moment strong,.story-moment span { display:block; }
241
+ .story-moment span { margin-top:.2rem; color:#f6e7b2; }
242
+
243
+ /* ── Suppress Gradio white flash ── */
244
+ .generating .forest-scene, .pending .forest-scene { opacity:.88!important; }
245
+ .progress-level,.progress-level-inner { display:none!important; }
246
+ div.gradio-html { background:transparent!important; }
247
+ .wrap.default.pending,.wrap.default.generating { background:transparent!important; }
248
+ </style>
249
+ """
250
+
251
+ def hero_markdown() -> str:
252
+ return """
253
+ <div class="myco-hero">
254
+ <div class="myco-mascot">🍄</div>
255
+ <div>
256
+ <p class="eyebrow">Tiny AI forest game</p>
257
+ <h1>Myco</h1>
258
+ <p>Follow an AI mushroom companion through a glowing forest. Every discovery starts with Myco's intuition.</p>
259
+ </div>
260
+ </div>
261
+ """
262
+
263
+ def home_forest_markdown() -> str:
264
+ return """
265
+ <div class="home-forest-postcard" aria-label="Animated Myco forest postcard">
266
+ <div class="home-sky">
267
+ <span class="home-milky-way" title="Milky Way">🌌</span>
268
+ <span class="home-star star-one">✨</span>
269
+ <span class="home-star star-two">✦</span>
270
+ <span class="home-star star-three">✧</span>
271
+ </div>
272
+ <div class="home-forest-floor">
273
+ <span class="home-tree left">🌲</span>
274
+ <span class="home-grass grass-left">🌿</span>
275
+ <span class="home-myco" title="Myco">🍄</span>
276
+ <span class="home-brown-mushroom" title="Brown mushroom">🍄‍🟫</span>
277
+ <span class="home-grass grass-right">🍃</span>
278
+ <span class="home-tree right">🌳</span>
279
+ <span class="home-moon">🌖</span>
280
+ </div>
281
+ <p><strong>The Milky Way is awake.</strong> Myco hears a brown mushroom whispering your first clue.</p>
282
+ <span class="home-whisper">"Tap Play. Let's find what the forest is hiding."</span>
283
+ </div>
284
+ """
285
+
286
+ def play_hook_markdown() -> str:
287
+ return """
288
+ <div class="play-hook">
289
+ <p class="eyebrow">Mission</p>
290
+ <h2>Goal: follow clues until Myco remembers the Impossible Mushroom.</h2>
291
+ <div class="hook-steps">
292
+ <span>1 · Move through the forest</span>
293
+ <span>2 · Search Clearing — mushroom pops up!</span>
294
+ <span>3 · Myco reacts · Pick, Study, or Follow Whisper</span>
295
+ </div>
296
+ <p>Every clue advances one mystery. Follow whispers to reveal why the MycoDex is regrowing a forest that vanished.</p>
297
+ </div>
298
+ """
299
+
300
+ def forest_scene(
301
+ current: MushroomState | None,
302
+ collection: CollectionState | None,
303
+ position: tuple[int, int] | None = None,
304
+ ) -> str:
305
+ count = len(_safe_collection(collection))
306
+ area = current_area(count)
307
+ pos_x, pos_y = _safe_position(position)
308
+ pos_cls = f"pos-{pos_x}-{pos_y}"
309
+
310
+ if current is None:
311
+ mushroom_name = "Hidden mushroom"
312
+ rarity = "common"
313
+ sprite = "✨"
314
+ status = "The moss is listening. Click Search Clearing to wake the forest."
315
+ omen = "Myco is waiting to sense the first spore."
316
+ event_label = "🌌 Dreaming Forest"
317
+ encounter = "The next clearing may reveal a memory, creature, or poison omen."
318
+ mystery_label = "The Wrong Memory"
319
+ score_total = "0"
320
+ health = "3"
321
+ reward_text = "Search a clearing to score spores."
322
+ state_cls = "state-waiting"
323
+ else:
324
+ mushroom_name = current.get("name", "Mystery mushroom")
325
+ rarity = current.get("rarity", "Common").lower()
326
+ sprite = "🍄‍🟫"
327
+ game_over = current.get("game_over") == "Yes"
328
+ studied = current.get("studied") == "Yes"
329
+ event_label = f"{current.get('event_emoji','✨')} {current.get('event_title','Strange Omen')}"
330
+ encounter = current.get("encounter", "The forest waits for Myco's reaction.")
331
+ mystery_label = current.get("mystery_title", "The Wrong Memory")
332
+ score_total = current.get("score_total", "0")
333
+ health = current.get("health", "3")
334
+ reward_text = current.get("reward_text", "Decide whether to risk the pickup.")
335
+ state_cls = _scene_state_class(current)
336
+ status = f"A {rarity} {mushroom_name} breaks through the moss!"
337
+ if game_over:
338
+ status = f"💀 GAME OVER: {mushroom_name} was poisonous!"
339
+ omen = "New field note unlocked." if studied else "Ask Myco what the forest is hiding."
340
+ if current.get("picked") == "Yes":
341
+ omen = "✅ Picked! Move to the next clearing."
342
+ if game_over:
343
+ omen = "Press Search Clearing to start a new run."
344
+
345
+ # Unique key forces browser to re-run CSS animations on every render
346
+ import time
347
+ anim_key = str(int(time.time() * 1000))[-6:]
348
+
349
+ return f'''{SCENE_STYLES}
350
+ <div class="forest-scene rarity-{escape(rarity)} {pos_cls} {state_cls}" data-key="{anim_key}">
351
+ <div class="scene-hud">
352
+ <span>🧭 {escape(area.name)}</span>
353
+ <span>🎮 ({pos_x+1},{pos_y+1})</span>
354
+ <span>📖 {count} found</span>
355
+ <span>⭐ {escape(score_total)}</span>
356
+ <span>❤️ {escape(health)}/3</span>
357
+ <span>{escape(event_label)}</span>
358
+ <span>🧩 {escape(mystery_label)}</span>
359
+ </div>
360
+
361
+ <div class="scene-stage" aria-label="Magical forest scene">
362
+ <span class="tree tree-left">🌲</span>
363
+ <span class="tree tree-right">🌳</span>
364
+ <span class="grass grass-one">🌿</span>
365
+ <span class="grass grass-two">🍃</span>
366
+ <span class="myco-player" title="Myco">🍄</span>
367
+ <span class="mushroom-sprite" title="{escape(mushroom_name)}">{sprite if current else "✨"}</span>
368
+ <span class="sparkle sparkle-one">✨</span>
369
+ <span class="sparkle sparkle-two">✦</span>
370
+ </div>
371
+
372
+ <div class="forest-map" aria-label="Forest grid">
373
+ {_movement_tiles(pos_x, pos_y)}
374
+ </div>
375
+
376
+ <div class="scene-caption">
377
+ <strong>{escape(status)}</strong>
378
+ <span>{escape(omen)}</span>
379
+ </div>
380
+ <div class="score-burst" aria-live="polite">{escape(reward_text)}</div>
381
+ <div class="story-moment">
382
+ <strong>{escape(event_label)}</strong>
383
+ <span>{escape(encounter)}</span>
384
+ </div>
385
+ </div>
386
+ '''
387
+
388
+ def _safe_position(position):
389
+ if position is None:
390
+ return (1, 1)
391
+ x, y = position
392
+ return (max(0, min(2, int(x))), max(0, min(2, int(y))))
393
+
394
+ def _movement_tiles(active_x, active_y):
395
+ tiles = []
396
+ for y in range(3):
397
+ for x in range(3):
398
+ active = " active" if (x, y) == (active_x, active_y) else ""
399
+ icon = "🍄" if active else "·"
400
+ tiles.append(f'<span class="map-tile{active}">{icon}</span>')
401
+ return "".join(tiles)
402
+
403
+ def _scene_state_class(current):
404
+ if current.get("game_over") == "Yes":
405
+ return "state-game-over"
406
+ if current.get("picked") == "Yes":
407
+ return "state-picked"
408
+ if current.get("poison") == "Yes":
409
+ return "state-danger"
410
+ return "state-discovered"
411
+
412
+ def game_status_markdown(
413
+ current: MushroomState | None,
414
+ collection: CollectionState | None,
415
+ position: tuple[int, int] | None = None,
416
+ ) -> str:
417
+ count = len(_safe_collection(collection))
418
+ pos_x, pos_y = _safe_position(position)
419
+ active_mystery = next_mystery(count)
420
+ mystery_line = _mystery_goal_line(active_mystery, count)
421
+
422
+ if current is None:
423
+ objective = "Goal: solve **one mystery** — why does Myco remember a vanished forest?"
424
+ discovery = "No active discovery yet. Move Myco, then Search Clearing."
425
+ ai_job = "Myco's job: notice clues you would miss alone."
426
+ event = "**Event:** waiting for the first omen"
427
+ clue_line = mystery_line
428
+ thread_line= "**Mystery thread:** The Wrong Memory — search the first clearing."
429
+ bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
430
+ score_line = "**Score:** 0 spores · **Health:** 3/3"
431
+ else:
432
+ name = current.get("name", "Mystery mushroom")
433
+ studied = current.get("studied") == "Yes"
434
+ next_step= "Collect it to preserve the clue" if studied else "Ask Myco, then Study or Follow Whisper"
435
+ if current.get("game_over") == "Yes":
436
+ objective = "**💀 GAME OVER** — press **Search Clearing** to start a new run."
437
+ elif current.get("picked") == "Yes":
438
+ objective = "Current objective: **move to the next clearing and search again**."
439
+ else:
440
+ objective = f"Current objective: **{next_step}**."
441
+ bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
442
+ score_line = (
443
+ f"**Score:** {current.get('score_total','0')} spores · "
444
+ f"**Health:** {current.get('health','3')}/3 · "
445
+ f"**Last:** {current.get('reward_text','—')}"
446
+ )
447
+ discovery = f"Active discovery: **{name}** · Rarity: **{current.get('rarity','Unknown')}**"
448
+ event = f"**Event:** {current.get('event_emoji','✨')} {current.get('event_title','Strange Omen')}"
449
+ clue_line = f"**Clue:** {current.get('clue','Myco is still listening.')}"
450
+ thread_line = (
451
+ f"**Mystery thread:** {current.get('mystery_title','The Wrong Memory')} — "
452
+ f"Next: {current.get('mystery_next','follow the whisper.')}"
453
+ )
454
+ ai_job = "Myco's job: interpret clues, warn you away from danger, make discoveries feel alive."
455
+
456
+ return "\n".join((
457
+ "### 🎮 Quest Log",
458
+ f"**MycoDex:** {count} discoveries · **Map:** ({pos_x+1},{pos_y+1})",
459
+ discovery, objective, score_line, bloom_line,
460
+ event, clue_line, thread_line, mystery_line, ai_job,
461
+ ))
462
+
463
+ def _mystery_bloom_meter(count):
464
+ filled = max(0, min(5, count))
465
+ petals = "".join("🌕" if i < filled else "🌑" for i in range(5))
466
+ if filled >= 5:
467
+ return f"{petals} Impossible Bloom visible"
468
+ return f"{petals} {5-filled} clue-whispers remain"
469
+
470
+ def mushroom_card(
471
+ mushroom: Mushroom | None,
472
+ clue: str | None = None,
473
+ secret: str | None = None,
474
+ state: MushroomState | None = None,
475
+ ) -> str:
476
+ if mushroom is None:
477
+ return "### 🌲 Forest Clearing\nClick **Search Clearing** — watch Myco run toward the mushroom!"
478
+ s = state or {}
479
+ poison = mushroom.name in POISONOUS_MUSHROOM_NAMES
480
+ poison_warning = "\n> ⚠️ **Myco senses danger — do NOT pick this without studying first!**" if poison else ""
481
+ return "\n".join((
482
+ f"### 🍄‍🟫 {mushroom.name}",
483
+ f"**{mushroom.rarity} discovery** · found near **{mushroom.habitat}**",
484
+ f"> {mushroom.lore}",
485
+ poison_warning,
486
+ "",
487
+ f"* **Edible:** {mushroom.edible}",
488
+ f"* **Magic:** {mushroom.magic}",
489
+ f"* **Danger:** {mushroom.danger}",
490
+ f"* **Value:** {_mushroom_value_label(mushroom.rarity)}",
491
+ f"* **Poison risk:** {_poison_risk_label(mushroom.name)}",
492
+ f"* **Event:** {s.get('event_emoji','✨')} {s.get('event_title','Strange Omen')}",
493
+ f"* **Mystery:** {s.get('mystery_title','The Wrong Memory')}",
494
+ f"* **Clue:** {clue or 'Study with Myco to reveal the clue.'}",
495
+ f"* **Secret:** {secret or 'Collect it to see how the MycoDex reacts.'}",
496
+ ))
497
+
498
+ def _mushroom_value_label(rarity):
499
+ if rarity == "Legendary": return "100 spores"
500
+ if rarity == "Rare": return "35 spores + special discovery"
501
+ return "10 spores"
502
+
503
+ def _poison_risk_label(name):
504
+ return "⚠️ POISONOUS — Game Over if picked!" if name in POISONOUS_MUSHROOM_NAMES else "No obvious poison"
505
+
506
+ def world_map_markdown(collection: CollectionState | None) -> str:
507
+ count = len(_safe_collection(collection))
508
+ cards = []
509
+ for area in AREA_UNLOCKS:
510
+ unlocked = count >= area.required_discoveries
511
+ state = "unlocked" if unlocked else "locked"
512
+ badge = "✅ Unlocked" if unlocked else f"🔒 {area.required_discoveries - count} away"
513
+ cards.append("\n".join((
514
+ f'<div class="map-card {state}">',
515
+ ' <div class="map-card-top">',
516
+ f' <strong>{escape(area.name)}</strong><span>{badge}</span>',
517
+ ' </div>',
518
+ f' <p>{escape(area.description)}</p>',
519
+ '</div>',
520
+ )))
521
+ secrets = tuple(_secret_badge(t.name, count >= t.required_discoveries) for t in revealed_mysteries(count))
522
+ if not secrets:
523
+ secrets = (_secret_badge("First Spore", False),)
524
+ return "\n".join((
525
+ '<div class="world-map-panel">',
526
+ ' <div class="world-map-header"><p class="eyebrow">World Map</p>',
527
+ ' <h2>Forest Atlas</h2>',
528
+ f' <p>{count} discoveries logged.</p></div>',
529
+ f' <div class="achievement-row">{"".join(secrets)}</div>',
530
+ f' <div class="map-grid">{"".join(cards)}</div>',
531
+ '</div>',
532
+ ))
533
+
534
+ def _secret_badge(label, unlocked):
535
+ state = "earned" if unlocked else "pending"
536
+ icon = "🔎" if unlocked else "◇"
537
+ return f'<span class="achievement {state}">{icon} {escape(label)}</span>'
538
+
539
+ def dex_markdown(collection: CollectionState | None) -> str:
540
+ current_collection = _safe_collection(collection)
541
+ if not current_collection:
542
+ return EMPTY_DEX
543
+ rows = ["### 📖 MycoDex", ""]
544
+ for entry in current_collection:
545
+ rows.append(
546
+ f"* 🍄 **{entry['name']}** — {entry['rarity']} — {entry['habitat']} "
547
+ f"— clue: {entry.get('clue','not studied yet')}"
548
+ )
549
+ return "\n".join(rows)
550
+
551
+ def _safe_collection(collection):
552
+ return list(collection or [])
553
+
554
+ def progress_markdown(collection: CollectionState | None) -> str:
555
+ count = len(_safe_collection(collection))
556
+ active_area = current_area(count)
557
+ upcoming_area = next_area(count)
558
+ unlocked_names= ", ".join(a.name for a in unlocked_areas(count))
559
+ rows = [
560
+ "### ✨ Forest Progress",
561
+ f"**MycoDex discoveries:** {count}",
562
+ f"**Current area:** {active_area.name} — {active_area.description}",
563
+ f"**Unlocked:** {unlocked_names}",
564
+ ]
565
+ upcoming_mystery = next_mystery(count)
566
+ if upcoming_area:
567
+ rows.append(f"**Next area:** {upcoming_area.required_discoveries - count} more to unlock **{upcoming_area.name}**.")
568
+ else:
569
+ rows.append(f"**All areas visible:** You reached **{AREA_UNLOCKS[-1].name}**.")
570
+ if upcoming_mystery:
571
+ rows.append(f"**Next secret:** {upcoming_mystery.required_discoveries - count} more to reveal **{upcoming_mystery.name}**.")
572
+ else:
573
+ rows.append("**All current secrets found.** Myco thinks the Elder is watching kindly.")
574
+ return "\n\n".join(rows)
575
+
576
+ def _mystery_goal_line(active_mystery, count):
577
+ if active_mystery is None:
578
+ return "**Mystery:** Every known clue is awake. Keep exploring for rare surprises."
579
+ remaining = active_mystery.required_discoveries - count
580
+ return f"**Mystery:** {remaining} clue discoveries until **{active_mystery.name}** — {active_mystery.clue}"
581
+
582
+ def game_intro_markdown() -> str:
583
+ return """
584
+ <div style="text-align:center;margin-bottom:.75rem;">
585
+ <h3>🌲 The Deep Forest</h3>
586
+ <p style="color:#666;font-size:.9em;">
587
+ You step into the glowing undergrowth. Myco hops off your shoulder, its cap faintly pulsing.<br>
588
+ Click <strong>Search Clearing</strong> — watch Myco run toward whatever pops up.
589
+ </p>
590
+ </div>
591
+ """
592
+
ui/scene_renderers.py CHANGED
@@ -1,8 +1,3 @@
1
- """
2
- Myco Engine: Mushroom Emergence System.
3
- Provides core generative rendering for Myco sprites.
4
- """
5
-
6
  import hashlib
7
 
8
  # ── 8-BIT MATRIX BLUEPRINTS ──
@@ -12,7 +7,7 @@ MATRIX_FLAT = [[0,0,1,1,1,1,1,1,0,0],[0,1,3,2,2,2,3,2,1,0],[1,2,2,2,3,2,2,2,2,1]
12
  MATRIX_STAR = [[0,0,0,0,1,1,0,0,0,0],[0,1,0,1,2,2,1,0,1,0],[1,2,1,2,2,2,2,1,2,1],[0,1,2,3,2,2,3,2,1,0],[0,0,1,1,4,4,1,1,0,0],[0,0,0,1,4,4,1,0,0,0],[0,0,0,1,4,4,1,0,0,0],[0,0,0,1,1,1,1,0,0,0]]
13
 
14
  def generate_mushroom_sprite(name: str) -> str:
15
- """Generates the pixel mushroom CSS visual."""
16
  hash_val = int(hashlib.md5(name.encode('utf-8')).hexdigest(), 16)
17
  shapes = [MATRIX_DOME, MATRIX_SKULL, MATRIX_FLAT, MATRIX_STAR]
18
  matrix = shapes[hash_val % len(shapes)]
@@ -22,18 +17,17 @@ def generate_mushroom_sprite(name: str) -> str:
22
 
23
  color_map = {1: "#121214", 2: c_cap, 3: c_spot, 4: c_stem}
24
 
25
- px_scale = 8
26
  shadows = [f"{c*px_scale}px {r*px_scale}px 0 {color_map[v]}"
27
  for r, row in enumerate(matrix) for c, v in enumerate(row) if v in color_map]
28
 
29
  return f"""
30
- <div style="width:{px_scale}px; height:{px_scale}px; box-shadow: {', '.join(shadows)}; margin: 50px auto; animation: emerge 1s ease-out;"></div>
31
- <style>@keyframes emerge {{ 0% {{ transform: scale(0) rotate(-45deg); opacity: 0; }} 100% {{ transform: scale(1) rotate(0deg); opacity: 1; }} }}</style>
32
  """
33
 
34
  def render_mushroom_stage(current, collection, position):
35
- """Primary UI call that uses the sprite generator."""
36
  if not current:
37
- return "<div style='text-align:center; padding:50px;'>Searching for growth...</div>"
38
  return generate_mushroom_sprite(current.get("name", "Unknown"))
39
 
 
 
 
 
 
 
1
  import hashlib
2
 
3
  # ── 8-BIT MATRIX BLUEPRINTS ──
 
7
  MATRIX_STAR = [[0,0,0,0,1,1,0,0,0,0],[0,1,0,1,2,2,1,0,1,0],[1,2,1,2,2,2,2,1,2,1],[0,1,2,3,2,2,3,2,1,0],[0,0,1,1,4,4,1,1,0,0],[0,0,0,1,4,4,1,0,0,0],[0,0,0,1,4,4,1,0,0,0],[0,0,0,1,1,1,1,0,0,0]]
8
 
9
  def generate_mushroom_sprite(name: str) -> str:
10
+ """Generates the procedural pixel mushroom visual."""
11
  hash_val = int(hashlib.md5(name.encode('utf-8')).hexdigest(), 16)
12
  shapes = [MATRIX_DOME, MATRIX_SKULL, MATRIX_FLAT, MATRIX_STAR]
13
  matrix = shapes[hash_val % len(shapes)]
 
17
 
18
  color_map = {1: "#121214", 2: c_cap, 3: c_spot, 4: c_stem}
19
 
20
+ px_scale = 5
21
  shadows = [f"{c*px_scale}px {r*px_scale}px 0 {color_map[v]}"
22
  for r, row in enumerate(matrix) for c, v in enumerate(row) if v in color_map]
23
 
24
  return f"""
25
+ <div style="width:{px_scale}px; height:{px_scale}px; box-shadow: {', '.join(shadows)}; margin: 0 auto; transform: scale(1.4);"></div>
 
26
  """
27
 
28
  def render_mushroom_stage(current, collection, position):
29
+ """Generates the mushroom sprite if an object exists."""
30
  if not current:
31
+ return ""
32
  return generate_mushroom_sprite(current.get("name", "Unknown"))
33