Spaces:
Sleeping
Sleeping
Delete ui/2-renderers.py
Browse files- ui/2-renderers.py +0 -525
ui/2-renderers.py
DELETED
|
@@ -1,525 +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 |
-
|
| 15 |
-
POISONOUS_MUSHROOM_NAMES = {
|
| 16 |
-
"Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle",
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
# ---------------------------------------------------------------------------
|
| 20 |
-
# Static UI fragments
|
| 21 |
-
# ---------------------------------------------------------------------------
|
| 22 |
-
|
| 23 |
-
def hero_markdown() -> str:
|
| 24 |
-
return """
|
| 25 |
-
<div class="myco-hero">
|
| 26 |
-
<div class="myco-mascot">🍄</div>
|
| 27 |
-
<div>
|
| 28 |
-
<p class="eyebrow">Tiny AI mushroom companion</p>
|
| 29 |
-
<h1>Myco</h1>
|
| 30 |
-
<p>Follow an AI mushroom companion through a glowing forest. Every discovery starts with Myco's intuition.</p>
|
| 31 |
-
</div>
|
| 32 |
-
</div>
|
| 33 |
-
"""
|
| 34 |
-
|
| 35 |
-
def bubble(content):
|
| 36 |
-
return f"""
|
| 37 |
-
<div style="
|
| 38 |
-
font-size: 12px;
|
| 39 |
-
padding: 12px;
|
| 40 |
-
border-radius: 28px;
|
| 41 |
-
background: linear-gradient(135deg, #1e1e1e 0%, #0f2027 50%, #203a43 100%);
|
| 42 |
-
color: #b6ff9a;
|
| 43 |
-
font-family: monospace;
|
| 44 |
-
">
|
| 45 |
-
{content} by Myco AI 🍄
|
| 46 |
-
</div>
|
| 47 |
-
"""
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def home_forest_markdown() -> str:
|
| 51 |
-
return """
|
| 52 |
-
<div class="home-forest-postcard" aria-label="Animated Myco forest postcard">
|
| 53 |
-
<style>
|
| 54 |
-
.home-myco-wrapper, .home-brown-wrapper { position:relative; display:inline-block; }
|
| 55 |
-
.home-spore {
|
| 56 |
-
position:absolute; left:50%; top:20%; width:4px; height:4px;
|
| 57 |
-
background:#a8ffb2; border-radius:50%; filter:blur(0.5px);
|
| 58 |
-
box-shadow:0 0 6px #52ff6b,0 0 10px #fff;
|
| 59 |
-
opacity:0; pointer-events:none;
|
| 60 |
-
animation:floatHomeSpore var(--duration) ease-out infinite;
|
| 61 |
-
animation-delay:var(--delay);
|
| 62 |
-
}
|
| 63 |
-
@keyframes floatHomeSpore {
|
| 64 |
-
0% { transform:translate(0,0) scale(1); opacity:0; }
|
| 65 |
-
15% { opacity:1; }
|
| 66 |
-
80% { opacity:0.7; }
|
| 67 |
-
100% { transform:translate(var(--x),var(--y)) scale(0.2); opacity:0; }
|
| 68 |
-
}
|
| 69 |
-
</style>
|
| 70 |
-
<div class="home-sky">
|
| 71 |
-
<span class="home-milky-way">🌌</span>
|
| 72 |
-
<span class="home-star star-one">✨</span>
|
| 73 |
-
<span class="home-star star-two">✦</span>
|
| 74 |
-
<span class="home-star star-three">✧</span>
|
| 75 |
-
</div>
|
| 76 |
-
<div class="home-forest-floor">
|
| 77 |
-
<span class="home-tree left">🌲</span>
|
| 78 |
-
<span class="home-grass grass-left">🌿</span>
|
| 79 |
-
<span class="home-myco-wrapper">
|
| 80 |
-
<span class="home-myco">🍄</span>
|
| 81 |
-
<span class="home-spore" style="--x:-25px;--y:-45px;--duration:3.5s;--delay:0s;"></span>
|
| 82 |
-
<span class="home-spore" style="--x:20px;--y:-35px;--duration:4.2s;--delay:0.8s;"></span>
|
| 83 |
-
<span class="home-spore" style="--x:-5px;--y:-60px;--duration:3s;--delay:1.5s;"></span>
|
| 84 |
-
</span>
|
| 85 |
-
<span class="home-brown-wrapper">
|
| 86 |
-
<span class="home-brown-mushroom">🍄🟫</span>
|
| 87 |
-
<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>
|
| 88 |
-
<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>
|
| 89 |
-
</span>
|
| 90 |
-
<span class="home-grass grass-right">🍃</span>
|
| 91 |
-
<span class="home-tree right">🌳</span>
|
| 92 |
-
<span class="home-moon">🌖</span>
|
| 93 |
-
</div>
|
| 94 |
-
<p><strong>The Milky Way is awake.</strong> Myco hears a brown mushroom whispering your first clue.</p>
|
| 95 |
-
<span class="home-whisper">"Tap Play. Let's find what the forest is hiding."</span>
|
| 96 |
-
</div>
|
| 97 |
-
"""
|
| 98 |
-
|
| 99 |
-
def play_hook_markdown() -> str:
|
| 100 |
-
return """
|
| 101 |
-
<div class="play-hook">
|
| 102 |
-
<p class="eyebrow">Mission</p>
|
| 103 |
-
<h2>Goal: follow clues until Myco remembers the Impossible Mushroom.</h2>
|
| 104 |
-
<div class="hook-steps">
|
| 105 |
-
<span>1 · Move through the forest</span>
|
| 106 |
-
<span>2 · Search Clearing — mushroom pops up!</span>
|
| 107 |
-
<span>3 · Myco reacts · Pick, Study, or Follow Whisper</span>
|
| 108 |
-
</div>
|
| 109 |
-
<p>Every clue advances one mystery.</p>
|
| 110 |
-
</div>
|
| 111 |
-
"""
|
| 112 |
-
|
| 113 |
-
def game_intro_markdown() -> str:
|
| 114 |
-
return """
|
| 115 |
-
<div style="text-align:center;margin-bottom:.75rem;">
|
| 116 |
-
<h3>🌲 The Deep Forest</h3>
|
| 117 |
-
<p style="color:#359635;font-size:.9em;">
|
| 118 |
-
You step into the glowing undergrowth. Myco hops off your shoulder, its cap faintly pulsing.<br>
|
| 119 |
-
Click <strong>Search Clearing</strong> — watch Myco run toward whatever pops up.
|
| 120 |
-
<span class="ai-tick-badge">🤖 Myco AI roaming</span>
|
| 121 |
-
</p>
|
| 122 |
-
</div>
|
| 123 |
-
"""
|
| 124 |
-
|
| 125 |
-
# ---------------------------------------------------------------------------
|
| 126 |
-
# Forest scene — accepts optional active_mushrooms list
|
| 127 |
-
# ---------------------------------------------------------------------------
|
| 128 |
-
|
| 129 |
-
def forest_scene(
|
| 130 |
-
current: "MushroomState | None",
|
| 131 |
-
collection: "CollectionState | None",
|
| 132 |
-
position: "tuple[int,int] | None" = None,
|
| 133 |
-
active_mushrooms: "list | None" = None,
|
| 134 |
-
) -> str:
|
| 135 |
-
import time
|
| 136 |
-
count = len(_safe_collection(collection))
|
| 137 |
-
area = current_area(count)
|
| 138 |
-
pos_x, pos_y = _safe_position(position)
|
| 139 |
-
pos_cls = f"pos-{pos_x}-{pos_y}"
|
| 140 |
-
|
| 141 |
-
# Catch active_mushrooms if packed inside the current dictionary
|
| 142 |
-
if current is not None and not active_mushrooms:
|
| 143 |
-
active_mushrooms = current.get("active_mushrooms", [])
|
| 144 |
-
|
| 145 |
-
# Safe defaults
|
| 146 |
-
mushroom_name = "Hidden mushroom"
|
| 147 |
-
rarity = "common"
|
| 148 |
-
sprite = "✨"
|
| 149 |
-
status = "The moss is listening. Click Search Clearing to wake the forest."
|
| 150 |
-
omen = "Myco is waiting to sense the first spore."
|
| 151 |
-
event_label = "🌌 Dreaming Forest"
|
| 152 |
-
encounter = "The next clearing may reveal a memory, creature, or poison omen."
|
| 153 |
-
mystery_label = "The Wrong Memory"
|
| 154 |
-
score_total = "0"
|
| 155 |
-
health = "3"
|
| 156 |
-
reward_text = "Search a clearing to score spores."
|
| 157 |
-
state_cls = "state-waiting"
|
| 158 |
-
|
| 159 |
-
if current is not None:
|
| 160 |
-
mushroom_name = current.get("name") or "Mystery cluster"
|
| 161 |
-
|
| 162 |
-
# SAFEGUARD: Prevent NoneType.lower() crash
|
| 163 |
-
raw_rarity = current.get("rarity")
|
| 164 |
-
rarity = (raw_rarity if raw_rarity else "Common").lower()
|
| 165 |
-
|
| 166 |
-
game_over = current.get("game_over") == "Yes"
|
| 167 |
-
|
| 168 |
-
# SAFEGUARD: Handle None values in string formatting safely
|
| 169 |
-
emoji = current.get("event_emoji") or "✨"
|
| 170 |
-
title = current.get("event_title") or "Strange Omen"
|
| 171 |
-
event_label = f"{emoji} {title}"
|
| 172 |
-
|
| 173 |
-
encounter = current.get("encounter") or "Go pick them up!"
|
| 174 |
-
mystery_label = current.get("mystery_title") or "The Wrong Memory"
|
| 175 |
-
score_total = current.get("score_total") or "0"
|
| 176 |
-
health = current.get("health") or "3"
|
| 177 |
-
reward_text = current.get("reward_text") or "Dodge the poison, collect the rest!"
|
| 178 |
-
state_cls = _scene_state_class(current)
|
| 179 |
-
|
| 180 |
-
if game_over:
|
| 181 |
-
status = "💀 GAME OVER: You stepped on a poisonous mushroom!"
|
| 182 |
-
omen = "Press Search Clearing to start a new run."
|
| 183 |
-
elif current.get("picked") == "Yes":
|
| 184 |
-
status = "✅ Area clear! Move to the next clearing."
|
| 185 |
-
omen = "Search the next clearing!"
|
| 186 |
-
else:
|
| 187 |
-
status = f"A {rarity} {mushroom_name} breaks through the moss!"
|
| 188 |
-
omen = "🍄 Study it before picking!"
|
| 189 |
-
|
| 190 |
-
active_sprites = _build_active_sprites(active_mushrooms or [])
|
| 191 |
-
anim_key = str(int(time.time() * 1000))[-6:]
|
| 192 |
-
|
| 193 |
-
return f'''
|
| 194 |
-
<div class="forest-scene rarity-{escape(rarity)} {pos_cls} {state_cls}" data-key="{anim_key}">
|
| 195 |
-
<div class="scene-hud">
|
| 196 |
-
<span>🧭 {escape(area.name)}</span>
|
| 197 |
-
<span>🎮 ({pos_x+1},{pos_y+1})</span>
|
| 198 |
-
<span>📖 {count} found</span>
|
| 199 |
-
<span>⭐ {escape(str(score_total))}</span>
|
| 200 |
-
<span>❤️ {escape(str(health))}/3</span>
|
| 201 |
-
<span>{escape(event_label)}</span>
|
| 202 |
-
<span>🧩 {escape(mystery_label)}</span>
|
| 203 |
-
</div>
|
| 204 |
-
|
| 205 |
-
<div class="scene-stage" aria-label="Magical forest scene">
|
| 206 |
-
<span class="tree tree-left">🌲</span>
|
| 207 |
-
<span class="tree tree-right">🌲</span>
|
| 208 |
-
<span class="grass grass-one">🌿</span>
|
| 209 |
-
<span class="grass grass-two">🍃</span>
|
| 210 |
-
<span class="myco-player" title="Myco">🍄</span>
|
| 211 |
-
<span class="mushroom-sprite" title="{escape(mushroom_name)}">{escape(sprite)}</span>
|
| 212 |
-
{active_sprites}
|
| 213 |
-
<span class="sparkle sparkle-one">✨</span>
|
| 214 |
-
<span class="sparkle sparkle-two">✦</span>
|
| 215 |
-
<span class="wood-log">🪵</span>
|
| 216 |
-
</div>
|
| 217 |
-
|
| 218 |
-
<div class="forest-map" aria-label="Forest grid">
|
| 219 |
-
{_movement_tiles(pos_x, pos_y, active_mushrooms)}
|
| 220 |
-
</div>
|
| 221 |
-
|
| 222 |
-
<div class="scene-caption">
|
| 223 |
-
<strong>{escape(str(status))}</strong>
|
| 224 |
-
<span>{escape(str(omen))}</span>
|
| 225 |
-
</div>
|
| 226 |
-
<div class="score-burst" aria-live="polite">{escape(str(reward_text))}</div>
|
| 227 |
-
<div class="story-moment">
|
| 228 |
-
<strong>{escape(str(event_label))}</strong>
|
| 229 |
-
<span>{escape(str(encounter))}</span>
|
| 230 |
-
</div>
|
| 231 |
-
</div>'''
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
# ---------------------------------------------------------------------------
|
| 235 |
-
# Active mushroom sprites builder
|
| 236 |
-
# ---------------------------------------------------------------------------
|
| 237 |
-
|
| 238 |
-
def _build_active_sprites(active_mushrooms: list) -> str:
|
| 239 |
-
sprites = []
|
| 240 |
-
for m in active_mushrooms:
|
| 241 |
-
if not isinstance(m, dict):
|
| 242 |
-
continue
|
| 243 |
-
if m.get("picked") == "Yes":
|
| 244 |
-
continue
|
| 245 |
-
|
| 246 |
-
# SAFEGUARD: Prevent int(None) crash
|
| 247 |
-
raw_x = m.get("mush_x")
|
| 248 |
-
raw_y = m.get("mush_y")
|
| 249 |
-
mx = int(raw_x) if raw_x is not None else 1
|
| 250 |
-
my = int(raw_y) if raw_y is not None else 1
|
| 251 |
-
|
| 252 |
-
mx = max(0, min(2, mx))
|
| 253 |
-
my = max(0, min(2, my))
|
| 254 |
-
is_poison = m.get("poison") == "Yes"
|
| 255 |
-
icon = "☠️" if is_poison else ("🍄🟫" if m.get("rarity") == "Legendary" else "🍄")
|
| 256 |
-
css_class = f"roam-sprite roam-{mx}-{my}" + (" roam-poison" if is_poison else "")
|
| 257 |
-
name = escape(str(m.get("name", "?")))
|
| 258 |
-
sprites.append(f'<span class="{css_class}" title="{name}">{icon}</span>')
|
| 259 |
-
return "\n ".join(sprites)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
# ---------------------------------------------------------------------------
|
| 263 |
-
# Helpers
|
| 264 |
-
# ---------------------------------------------------------------------------
|
| 265 |
-
|
| 266 |
-
def _safe_position(position):
|
| 267 |
-
if position is None:
|
| 268 |
-
return (1, 1)
|
| 269 |
-
try:
|
| 270 |
-
x, y = position
|
| 271 |
-
return (max(0, min(2, int(x))), max(0, min(2, int(y))))
|
| 272 |
-
except Exception:
|
| 273 |
-
return (1, 1)
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
def _movement_tiles(active_x, active_y, active_mushrooms=None):
|
| 277 |
-
"""Grid map tiles — marks player, mushrooms, and poison cells."""
|
| 278 |
-
mush_coords = {}
|
| 279 |
-
for m in (active_mushrooms or []):
|
| 280 |
-
if not isinstance(m, dict):
|
| 281 |
-
continue
|
| 282 |
-
if m.get("picked") == "Yes":
|
| 283 |
-
continue
|
| 284 |
-
mx, my = int(m.get("mush_x", -1)), int(m.get("mush_y", -1))
|
| 285 |
-
if 0 <= mx <= 2 and 0 <= my <= 2:
|
| 286 |
-
mush_coords[(mx, my)] = m
|
| 287 |
-
|
| 288 |
-
tiles = []
|
| 289 |
-
for y in range(3):
|
| 290 |
-
for x in range(3):
|
| 291 |
-
is_player = (x, y) == (int(active_x), int(active_y))
|
| 292 |
-
mush = mush_coords.get((x, y))
|
| 293 |
-
if is_player:
|
| 294 |
-
css = "map-tile active"
|
| 295 |
-
icon = "🍄"
|
| 296 |
-
elif mush:
|
| 297 |
-
is_p = mush.get("poison") == "Yes"
|
| 298 |
-
css = "map-tile has-poison" if is_p else "map-tile has-mush"
|
| 299 |
-
icon = "☠️" if is_p else "✨"
|
| 300 |
-
else:
|
| 301 |
-
css = "map-tile"
|
| 302 |
-
icon = "·"
|
| 303 |
-
tiles.append(f'<span class="{css}">{icon}</span>')
|
| 304 |
-
return "".join(tiles)
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
def _scene_state_class(current):
|
| 308 |
-
if not current:
|
| 309 |
-
return "state-waiting"
|
| 310 |
-
if current.get("game_over") == "Yes":
|
| 311 |
-
return "state-game-over"
|
| 312 |
-
if current.get("picked") == "Yes":
|
| 313 |
-
return "state-picked"
|
| 314 |
-
if current.get("poison") == "Yes":
|
| 315 |
-
return "state-danger"
|
| 316 |
-
return "state-discovered"
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
def _safe_collection(collection):
|
| 320 |
-
"""Return a list of dicts, parsing any string entries that Gradio may have serialized."""
|
| 321 |
-
import json as _json
|
| 322 |
-
result = []
|
| 323 |
-
for entry in list(collection or []):
|
| 324 |
-
if isinstance(entry, str):
|
| 325 |
-
try:
|
| 326 |
-
entry = _json.loads(entry)
|
| 327 |
-
except Exception:
|
| 328 |
-
continue
|
| 329 |
-
if isinstance(entry, dict):
|
| 330 |
-
result.append(entry)
|
| 331 |
-
return result
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
# ---------------------------------------------------------------------------
|
| 335 |
-
# Game status panel
|
| 336 |
-
# ---------------------------------------------------------------------------
|
| 337 |
-
|
| 338 |
-
def game_status_markdown(
|
| 339 |
-
current: "MushroomState | None",
|
| 340 |
-
collection: "CollectionState | None",
|
| 341 |
-
position: "tuple[int,int] | None" = None,
|
| 342 |
-
) -> str:
|
| 343 |
-
count = len(_safe_collection(collection))
|
| 344 |
-
pos_x, pos_y = _safe_position(position)
|
| 345 |
-
active_mystery = next_mystery(count)
|
| 346 |
-
mystery_line = _mystery_goal_line(active_mystery, count)
|
| 347 |
-
|
| 348 |
-
if current is None:
|
| 349 |
-
objective = "Goal: solve **one mystery** — why does Myco remember a vanished forest?"
|
| 350 |
-
discovery = "No active discovery yet. Move Myco, then Search Clearing."
|
| 351 |
-
ai_job = "Myco AI is roaming — watch it search, study, and collect automatically."
|
| 352 |
-
event = "**Event:** waiting for the first omen"
|
| 353 |
-
clue_line = mystery_line
|
| 354 |
-
thread_line = "**Mystery thread:** The Wrong Memory — search the first clearing."
|
| 355 |
-
bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
|
| 356 |
-
score_line = "**Score:** 0 spores · **Health:** 3/3"
|
| 357 |
-
else:
|
| 358 |
-
name = current.get("name", "Mystery mushroom")
|
| 359 |
-
studied = current.get("studied") == "Yes"
|
| 360 |
-
game_over = current.get("game_over") == "Yes"
|
| 361 |
-
picked = current.get("picked") == "Yes"
|
| 362 |
-
if game_over:
|
| 363 |
-
objective = "**💀 GAME OVER** — press **Search Clearing** to start a new run."
|
| 364 |
-
elif picked:
|
| 365 |
-
objective = "Current objective: **move to the next clearing and search again**."
|
| 366 |
-
else:
|
| 367 |
-
next_step = "Collect it to preserve the clue" if studied else "Study it, then Collect or Pick"
|
| 368 |
-
objective = f"Current objective: **{next_step}**."
|
| 369 |
-
bloom_line = f"**Impossible bloom:** {_mystery_bloom_meter(count)}"
|
| 370 |
-
score_line = (
|
| 371 |
-
f"**Score:** {current.get('score_total','0')} spores · "
|
| 372 |
-
f"**Health:** {current.get('health','3')}/3 · "
|
| 373 |
-
f"**Last:** {current.get('reward_text','—')}"
|
| 374 |
-
)
|
| 375 |
-
discovery = f"Active discovery: **{name}** · Rarity: **{current.get('rarity','Unknown')}**"
|
| 376 |
-
event = f"**Event:** {current.get('event_emoji','✨')} {current.get('event_title','Strange Omen')}"
|
| 377 |
-
clue_line = f"**Clue:** {current.get('clue','Myco is still listening.')}"
|
| 378 |
-
thread_line = (
|
| 379 |
-
f"**Mystery thread:** {current.get('mystery_title','The Wrong Memory')} — "
|
| 380 |
-
f"Next: {current.get('mystery_next','follow the whisper.')}"
|
| 381 |
-
)
|
| 382 |
-
ai_job = "Myco AI is watching — it will study and collect automatically while you explore."
|
| 383 |
-
|
| 384 |
-
return "\n".join((
|
| 385 |
-
"### 🎮 Quest Log",
|
| 386 |
-
f"**MycoDex:** {count} discoveries · **Map:** ({pos_x+1},{pos_y+1})",
|
| 387 |
-
discovery, objective, score_line, bloom_line,
|
| 388 |
-
event, clue_line, thread_line, mystery_line, ai_job,
|
| 389 |
-
))
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
# ---------------------------------------------------------------------------
|
| 393 |
-
# Mushroom card
|
| 394 |
-
# ---------------------------------------------------------------------------
|
| 395 |
-
|
| 396 |
-
def mushroom_card(
|
| 397 |
-
mushroom: "Mushroom | None",
|
| 398 |
-
clue: "str | None" = None,
|
| 399 |
-
secret: "str | None" = None,
|
| 400 |
-
state: "MushroomState | None" = None,
|
| 401 |
-
) -> str:
|
| 402 |
-
if mushroom is None:
|
| 403 |
-
return "### 🌲 Forest Clearing\nClick **Search Clearing** — watch Myco run toward the mushroom!"
|
| 404 |
-
s = state or {}
|
| 405 |
-
poison = mushroom.name in POISONOUS_MUSHROOM_NAMES
|
| 406 |
-
warning = "\n> ⚠️ **Myco senses danger — do NOT pick this without studying first!**" if poison else ""
|
| 407 |
-
return "\n".join((
|
| 408 |
-
f"### 🍄🟫 {mushroom.name}",
|
| 409 |
-
f"**{mushroom.rarity} discovery** · found near **{mushroom.habitat}**",
|
| 410 |
-
f"> {mushroom.lore}",
|
| 411 |
-
warning, "",
|
| 412 |
-
f"* **Edible:** {mushroom.edible}",
|
| 413 |
-
f"* **Magic:** {mushroom.magic}",
|
| 414 |
-
f"* **Danger:** {mushroom.danger}",
|
| 415 |
-
f"* **Value:** {_mushroom_value_label(mushroom.rarity)}",
|
| 416 |
-
f"* **Poison risk:** {_poison_risk_label(mushroom.name)}",
|
| 417 |
-
f"* **Event:** {s.get('event_emoji','✨')} {s.get('event_title','Strange Omen')}",
|
| 418 |
-
f"* **Mystery:** {s.get('mystery_title','The Wrong Memory')}",
|
| 419 |
-
f"* **Clue:** {clue or 'Study with Myco to reveal the clue.'}",
|
| 420 |
-
f"* **Secret:** {secret or 'Collect it to see how the MycoDex reacts.'}",
|
| 421 |
-
f"* **Status:** {'⚠️ POISONOUS' if poison else '✅ Appears safe'}",
|
| 422 |
-
))
|
| 423 |
-
|
| 424 |
-
def _mushroom_value_label(rarity):
|
| 425 |
-
return {"Legendary": "100 spores", "Rare": "35 spores"}.get(rarity, "10 spores")
|
| 426 |
-
|
| 427 |
-
def _poison_risk_label(name):
|
| 428 |
-
return "⚠️ POISONOUS — Game Over if picked!" if name in POISONOUS_MUSHROOM_NAMES else "No obvious poison"
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
# ---------------------------------------------------------------------------
|
| 432 |
-
# World map / Dex / Progress
|
| 433 |
-
# ---------------------------------------------------------------------------
|
| 434 |
-
|
| 435 |
-
def world_map_markdown(collection: "CollectionState | None") -> str:
|
| 436 |
-
count = len(_safe_collection(collection))
|
| 437 |
-
cards = []
|
| 438 |
-
for area in AREA_UNLOCKS:
|
| 439 |
-
unlocked = count >= area.required_discoveries
|
| 440 |
-
state = "unlocked" if unlocked else "locked"
|
| 441 |
-
badge = "✅ Unlocked" if unlocked else f"🔒 {area.required_discoveries - count} away"
|
| 442 |
-
cards.append("\n".join((
|
| 443 |
-
f'<div class="map-card {state}">',
|
| 444 |
-
' <div class="map-card-top">',
|
| 445 |
-
f' <strong>{escape(area.name)}</strong><span>{badge}</span>',
|
| 446 |
-
' </div>',
|
| 447 |
-
f' <p>{escape(area.description)}</p>',
|
| 448 |
-
'</div>',
|
| 449 |
-
)))
|
| 450 |
-
secrets = tuple(_secret_badge(t.name, count >= t.required_discoveries) for t in revealed_mysteries(count))
|
| 451 |
-
if not secrets:
|
| 452 |
-
secrets = (_secret_badge("First Spore", False),)
|
| 453 |
-
return "\n".join((
|
| 454 |
-
'<div class="world-map-panel">',
|
| 455 |
-
' <div class="world-map-header"><p class="eyebrow">World Map</p>',
|
| 456 |
-
' <h2>Forest Atlas</h2>',
|
| 457 |
-
f' <p>{count} discoveries logged.</p></div>',
|
| 458 |
-
f' <div class="achievement-row">{"".join(secrets)}</div>',
|
| 459 |
-
f' <div class="map-grid">{"".join(cards)}</div>',
|
| 460 |
-
'</div>',
|
| 461 |
-
))
|
| 462 |
-
|
| 463 |
-
def _secret_badge(label, unlocked):
|
| 464 |
-
state = "earned" if unlocked else "pending"
|
| 465 |
-
icon = "🔎" if unlocked else "◇"
|
| 466 |
-
return f'<span class="achievement {state}">{icon} {escape(label)}</span>'
|
| 467 |
-
|
| 468 |
-
def dex_markdown(collection: "CollectionState | None") -> str:
|
| 469 |
-
current_collection = _safe_collection(collection)
|
| 470 |
-
if not current_collection:
|
| 471 |
-
return EMPTY_DEX
|
| 472 |
-
rows = ["### 📖 MycoDex", ""]
|
| 473 |
-
for entry in current_collection:
|
| 474 |
-
# Entries can arrive as JSON strings if Gradio serialized the state
|
| 475 |
-
if isinstance(entry, str):
|
| 476 |
-
try:
|
| 477 |
-
import json as _json
|
| 478 |
-
entry = _json.loads(entry)
|
| 479 |
-
except Exception:
|
| 480 |
-
continue
|
| 481 |
-
if not isinstance(entry, dict):
|
| 482 |
-
continue
|
| 483 |
-
poison_flag = " ☠️" if entry.get("poison") == "Yes" else ""
|
| 484 |
-
rows.append(
|
| 485 |
-
f"* 🍄 **{entry.get('name','Unknown')}**{poison_flag} — "
|
| 486 |
-
f"{entry.get('rarity','Unknown')} — "
|
| 487 |
-
f"{entry.get('habitat','Unknown habitat')} — clue: {entry.get('clue','not studied yet')}"
|
| 488 |
-
)
|
| 489 |
-
return "\n".join(rows)
|
| 490 |
-
|
| 491 |
-
def progress_markdown(collection: "CollectionState | None") -> str:
|
| 492 |
-
count = len(_safe_collection(collection))
|
| 493 |
-
active_area = current_area(count)
|
| 494 |
-
upcoming_area = next_area(count)
|
| 495 |
-
unlocked_names = ", ".join(a.name for a in unlocked_areas(count))
|
| 496 |
-
rows = [
|
| 497 |
-
"### ✨ Forest Progress",
|
| 498 |
-
f"**MycoDex discoveries:** {count}",
|
| 499 |
-
f"**Current area:** {active_area.name} — {active_area.description}",
|
| 500 |
-
f"**Unlocked:** {unlocked_names}",
|
| 501 |
-
]
|
| 502 |
-
upcoming_mystery = next_mystery(count)
|
| 503 |
-
if upcoming_area:
|
| 504 |
-
rows.append(f"**Next area:** {upcoming_area.required_discoveries - count} more to unlock **{upcoming_area.name}**.")
|
| 505 |
-
else:
|
| 506 |
-
rows.append(f"**All areas visible.** You reached **{AREA_UNLOCKS[-1].name}**.")
|
| 507 |
-
if upcoming_mystery:
|
| 508 |
-
rows.append(f"**Next secret:** {upcoming_mystery.required_discoveries - count} more to reveal **{upcoming_mystery.name}**.")
|
| 509 |
-
else:
|
| 510 |
-
rows.append("**All current secrets found.**")
|
| 511 |
-
return "\n\n".join(rows)
|
| 512 |
-
|
| 513 |
-
def _mystery_goal_line(active_mystery, count):
|
| 514 |
-
if active_mystery is None:
|
| 515 |
-
return "**Mystery:** Every known clue is awake. Keep exploring for rare surprises."
|
| 516 |
-
remaining = active_mystery.required_discoveries - count
|
| 517 |
-
return f"**Mystery:** {remaining} clue discoveries until **{active_mystery.name}** — {active_mystery.clue}"
|
| 518 |
-
|
| 519 |
-
def _mystery_bloom_meter(count):
|
| 520 |
-
filled = max(0, min(5, count))
|
| 521 |
-
petals = "".join("🌕" if i < filled else "🌑" for i in range(5))
|
| 522 |
-
if filled >= 5:
|
| 523 |
-
return f"{petals} Impossible Bloom visible"
|
| 524 |
-
return f"{petals} {5 - filled} clue-whispers remain"
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|