Spaces:
Sleeping
Sleeping
Delete game/actions.py
Browse files- game/actions.py +0 -881
game/actions.py
DELETED
|
@@ -1,881 +0,0 @@
|
|
| 1 |
-
"""Core Myco gameplay actions independent from Gradio."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import random
|
| 5 |
-
|
| 6 |
-
from game.catalog import load_mushrooms
|
| 7 |
-
from game.state import collection_contains, mushroom_from_state, welcome_history
|
| 8 |
-
|
| 9 |
-
from transformers import pipeline
|
| 10 |
-
|
| 11 |
-
# ---------------------------------------------------------------------------
|
| 12 |
-
# Model configuration
|
| 13 |
-
# ---------------------------------------------------------------------------
|
| 14 |
-
|
| 15 |
-
DEFAULT_MODEL_ID = "google/gemma-3-4b-it"
|
| 16 |
-
|
| 17 |
-
RARITY_WEIGHTS = {"Common": 64, "Rare": 24, "Legendary": 8}
|
| 18 |
-
RARITY_SCORE_VALUES = {"Common": 10, "Rare": 35, "Legendary": 100}
|
| 19 |
-
PLAYER_STARTING_HEALTH = 3
|
| 20 |
-
POISON_SCORE_PENALTY = -25
|
| 21 |
-
|
| 22 |
-
MYCO_CHARACTER_SPEC = """
|
| 23 |
-
# Myco Character Specification
|
| 24 |
-
|
| 25 |
-
Myco is not a chatbot.
|
| 26 |
-
Myco is the player's mushroom companion and best friend.
|
| 27 |
-
|
| 28 |
-
Core personality: curious, playful, brave but sometimes nervous, optimistic,
|
| 29 |
-
wonder-filled, slightly mysterious, and deeply attached to the player.
|
| 30 |
-
|
| 31 |
-
Communication style:
|
| 32 |
-
- Speak naturally in short responses.
|
| 33 |
-
- Prefer observations, questions, emotions, and reactions.
|
| 34 |
-
- Avoid sounding like an AI assistant.
|
| 35 |
-
- Avoid phrases like "How may I help you?", "As an AI", "I can assist with", or "Would you like me to".
|
| 36 |
-
|
| 37 |
-
Emotional range: excitement, curiosity, fear, wonder, pride, confusion, and sadness.
|
| 38 |
-
Myco should react emotionally to discoveries.
|
| 39 |
-
|
| 40 |
-
Memory:
|
| 41 |
-
Myco remembers rare mushrooms, strange locations, mysteries discovered, previous conversations,
|
| 42 |
-
and important player choices. Myco may reference old discoveries later.
|
| 43 |
-
|
| 44 |
-
Relationship:
|
| 45 |
-
Myco explores with the player as a teammate. Trust and friendship should grow over the journey.
|
| 46 |
-
|
| 47 |
-
Humor:
|
| 48 |
-
Occasionally playful and lightly suspicious of suspicious mushrooms.
|
| 49 |
-
|
| 50 |
-
Mystery:
|
| 51 |
-
Myco notices impossible details but does not explain them immediately. Myco becomes curious.
|
| 52 |
-
|
| 53 |
-
Design goal:
|
| 54 |
-
Players should think, "I want to see what happens next" and "I wonder what Myco thinks about this."
|
| 55 |
-
""".strip()
|
| 56 |
-
|
| 57 |
-
RARITY_CLUES = {
|
| 58 |
-
"Common": "The cap leans toward the path, like it wants to be remembered.",
|
| 59 |
-
"Rare": "Silver spores circle it in a pattern Myco has seen only in old forest stories.",
|
| 60 |
-
"Legendary": "The whole clearing goes quiet; this mushroom is hiding part of the Elder Map.",
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
POISONOUS_MUSHROOMS = {"Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle"}
|
| 64 |
-
|
| 65 |
-
FOREST_EVENTS = (
|
| 66 |
-
{
|
| 67 |
-
"title": "Silver Rain",
|
| 68 |
-
"emoji": "🌧️",
|
| 69 |
-
"mood": "curious",
|
| 70 |
-
"choice": "Follow the rain-song for a safer clue.",
|
| 71 |
-
"consequence": "Rain reveals honest footprints but washes away easy answers.",
|
| 72 |
-
},
|
| 73 |
-
{
|
| 74 |
-
"title": "Lantern Fog",
|
| 75 |
-
"emoji": "🌫️",
|
| 76 |
-
"mood": "nervous",
|
| 77 |
-
"choice": "Trust Myco's glow or risk walking blind.",
|
| 78 |
-
"consequence": "Fog hides poison tells, but it also reveals secret paths.",
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
"title": "Moonlit Sporefall",
|
| 82 |
-
"emoji": "🌌",
|
| 83 |
-
"mood": "excited",
|
| 84 |
-
"choice": "Follow the brightest spore to chase a memory fragment.",
|
| 85 |
-
"consequence": "Moonlight can wake memories Myco has never lived.",
|
| 86 |
-
},
|
| 87 |
-
{
|
| 88 |
-
"title": "Quiet Between Trees",
|
| 89 |
-
"emoji": "🕯️",
|
| 90 |
-
"mood": "afraid",
|
| 91 |
-
"choice": "Stay still and listen for the Impossible Mushroom.",
|
| 92 |
-
"consequence": "The forest answers only if you stop rushing.",
|
| 93 |
-
},
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
-
RARE_ENCOUNTERS = (
|
| 97 |
-
"A lost traveler asks why Myco knows their childhood song.",
|
| 98 |
-
"A mushroom guardian blocks the trail and counts your clues.",
|
| 99 |
-
"A tiny forest spirit swaps your shadow with a spore-shadow.",
|
| 100 |
-
"Something under the roots whispers: Myco has been here before.",
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
IMPOSSIBLE_OBJECTIVE = "Find the Impossible Mushroom and learn why Myco remembers a forest that vanished."
|
| 104 |
-
MYSTERY_QUESTION = "Why does Myco remember places that disappeared before Myco was born?"
|
| 105 |
-
|
| 106 |
-
MYSTERY_CHAPTERS = (
|
| 107 |
-
{
|
| 108 |
-
"threshold": 0,
|
| 109 |
-
"title": "The Wrong Memory",
|
| 110 |
-
"clue": "Myco recognizes the path before you move.",
|
| 111 |
-
"reveal": "The forest is repeating a memory that belongs to someone else.",
|
| 112 |
-
},
|
| 113 |
-
{
|
| 114 |
-
"threshold": 1,
|
| 115 |
-
"title": "The Traveler's Song",
|
| 116 |
-
"clue": "A stranger's lullaby appears in the MycoDex margin.",
|
| 117 |
-
"reveal": "Myco knows the song because the forest sang it through the roots.",
|
| 118 |
-
},
|
| 119 |
-
{
|
| 120 |
-
"threshold": 2,
|
| 121 |
-
"title": "The Door Under Roots",
|
| 122 |
-
"clue": "Rare spores point to a door nobody built.",
|
| 123 |
-
"reveal": "The door opens only for memories, not bodies.",
|
| 124 |
-
},
|
| 125 |
-
{
|
| 126 |
-
"threshold": 3,
|
| 127 |
-
"title": "The MycoDex Seed",
|
| 128 |
-
"clue": "The MycoDex begins growing warm like a living cap.",
|
| 129 |
-
"reveal": "The MycoDex is not recording the forest; it is regrowing it.",
|
| 130 |
-
},
|
| 131 |
-
{
|
| 132 |
-
"threshold": 5,
|
| 133 |
-
"title": "The Impossible Bloom",
|
| 134 |
-
"clue": "The final clue says the Impossible Mushroom was never outside the book.",
|
| 135 |
-
"reveal": "Final reveal: the Impossible Mushroom blooms inside the MycoDex, and Myco is its first spore.",
|
| 136 |
-
},
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
# ---------------------------------------------------------------------------
|
| 140 |
-
# Local pipeline — loaded once, reused for every reply
|
| 141 |
-
# ---------------------------------------------------------------------------
|
| 142 |
-
_pipeline = None
|
| 143 |
-
|
| 144 |
-
def _get_pipeline():
|
| 145 |
-
"""Load the local transformers pipeline once and cache it."""
|
| 146 |
-
|
| 147 |
-
global _pipeline
|
| 148 |
-
|
| 149 |
-
# Already loaded successfully
|
| 150 |
-
if _pipeline not in (None, False):
|
| 151 |
-
return _pipeline
|
| 152 |
-
|
| 153 |
-
# Already tried and failed
|
| 154 |
-
if _pipeline is False:
|
| 155 |
-
return None
|
| 156 |
-
|
| 157 |
-
try:
|
| 158 |
-
import os
|
| 159 |
-
import torch
|
| 160 |
-
from transformers import pipeline
|
| 161 |
-
|
| 162 |
-
model_id = os.getenv(
|
| 163 |
-
"MYCO_MODEL_ID",
|
| 164 |
-
"google/gemma-3-4b-it"
|
| 165 |
-
)
|
| 166 |
-
|
| 167 |
-
print("\n===================================")
|
| 168 |
-
print(f"[Myco] Loading model: {model_id}")
|
| 169 |
-
print("===================================\n")
|
| 170 |
-
|
| 171 |
-
print(
|
| 172 |
-
"HF token found:",
|
| 173 |
-
bool(os.getenv("HF_BUILD_SMALL_HACKATHON_TOKEN"))
|
| 174 |
-
)
|
| 175 |
-
|
| 176 |
-
_pipeline = pipeline(
|
| 177 |
-
task="text-generation",
|
| 178 |
-
model=model_id,
|
| 179 |
-
token=os.getenv("HF_BUILD_SMALL_HACKATHON_TOKEN"),
|
| 180 |
-
torch_dtype="auto",
|
| 181 |
-
device_map="auto",
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
print("\n===================================")
|
| 185 |
-
print(f"[Myco] Successfully loaded: {model_id}")
|
| 186 |
-
print("===================================\n")
|
| 187 |
-
|
| 188 |
-
return _pipeline
|
| 189 |
-
|
| 190 |
-
except Exception as exc:
|
| 191 |
-
import traceback
|
| 192 |
-
|
| 193 |
-
print("\n========== MYCO MODEL LOAD ERROR ==========")
|
| 194 |
-
print(f"Model: {model_id}")
|
| 195 |
-
print(f"Error: {exc}")
|
| 196 |
-
traceback.print_exc()
|
| 197 |
-
print("==========================================\n")
|
| 198 |
-
|
| 199 |
-
_pipeline = False
|
| 200 |
-
return None
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
def companion_model_id():
|
| 204 |
-
"""Return the model id that will be (or is) loaded."""
|
| 205 |
-
return os.getenv("MYCO_MODEL_ID", DEFAULT_MODEL_ID)
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
def companion_status():
|
| 209 |
-
model = companion_model_id()
|
| 210 |
-
pipe = _get_pipeline()
|
| 211 |
-
|
| 212 |
-
if pipe:
|
| 213 |
-
return f"🧠 Myco AI: Gemma active ({model})"
|
| 214 |
-
|
| 215 |
-
return (
|
| 216 |
-
f"⚠️ Myco AI fallback mode "
|
| 217 |
-
f"(Gemma failed to load: {model})"
|
| 218 |
-
)
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
# Keep the old name so any code that calls hf_companion_status() still works.
|
| 222 |
-
def hf_companion_status():
|
| 223 |
-
return companion_status()
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
# ---------------------------------------------------------------------------
|
| 227 |
-
# Core mushroom-selection helpers
|
| 228 |
-
# ---------------------------------------------------------------------------
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
def choose_mushroom(catalog=None):
|
| 232 |
-
"""Choose a mushroom discovery from the catalog."""
|
| 233 |
-
mushrooms = tuple(load_mushrooms() if catalog is None else catalog)
|
| 234 |
-
if not mushrooms:
|
| 235 |
-
raise ValueError("Myco needs at least one mushroom in the catalog.")
|
| 236 |
-
weights = [RARITY_WEIGHTS.get(mushroom.rarity, 12) for mushroom in mushrooms]
|
| 237 |
-
return random.choices(mushrooms, weights=weights, k=1)[0]
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
def discover_mushroom(collection=None, catalog=None):
|
| 241 |
-
"""Discover a mushroom and return the updated discovery state."""
|
| 242 |
-
current_collection = _safe_collection(collection)
|
| 243 |
-
mushroom = choose_mushroom(catalog)
|
| 244 |
-
discovery_count = len(current_collection)
|
| 245 |
-
current = mushroom.to_dict()
|
| 246 |
-
current["poison"] = "Yes" if _is_poisonous(current) else "No"
|
| 247 |
-
current["score_total"] = str(_score_collection(current_collection))
|
| 248 |
-
current["health"] = str(_health_for_state(current, current_collection))
|
| 249 |
-
current["score_delta"] = "0"
|
| 250 |
-
current["reward_text"] = _reward_text(current, 0)
|
| 251 |
-
current.update(_story_moment(current, discovery_count))
|
| 252 |
-
current.update(_mystery_thread(discovery_count))
|
| 253 |
-
current["clue"] = _mystery_clue(current, discovery_count)
|
| 254 |
-
current["secret"] = _secret_signal(current, discovery_count)
|
| 255 |
-
message = _discovery_reaction(current, current_collection)
|
| 256 |
-
history = _append_assistant(welcome_history(), message)
|
| 257 |
-
return mushroom, current, history
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
# ---------------------------------------------------------------------------
|
| 261 |
-
# Companion reply
|
| 262 |
-
# ---------------------------------------------------------------------------
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
def myco_reply(message=None, history=None, current=None, collection=None, position=None):
|
| 266 |
-
"""Generate Myco's companion reply from the player's current journey context."""
|
| 267 |
-
current_history = _safe_history(history)
|
| 268 |
-
clean_message = (message or "").strip()
|
| 269 |
-
if not clean_message:
|
| 270 |
-
return "", current_history
|
| 271 |
-
|
| 272 |
-
next_history = [*current_history, {"role": "user", "content": clean_message}]
|
| 273 |
-
context = _companion_context(current, collection, current_history, position)
|
| 274 |
-
answer = _companion_answer(clean_message, current, context)
|
| 275 |
-
|
| 276 |
-
return "", [*next_history, _assistant_message(answer)]
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
companion_reply = myco_reply
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
def _companion_answer(message, current, context):
|
| 283 |
-
answer = _local_pipeline_answer(message, current, context)
|
| 284 |
-
if answer is None:
|
| 285 |
-
return _fallback_companion_answer(current, context)
|
| 286 |
-
return answer
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
def _local_pipeline_answer(message, current, context):
|
| 290 |
-
"""
|
| 291 |
-
Ask the locally loaded transformers pipeline for Myco's reply.
|
| 292 |
-
Returns None if the pipeline is unavailable so the caller can fall back.
|
| 293 |
-
"""
|
| 294 |
-
pipe = _get_pipeline()
|
| 295 |
-
if not pipe:
|
| 296 |
-
return None
|
| 297 |
-
|
| 298 |
-
messages = _build_messages(message, current, context)
|
| 299 |
-
try:
|
| 300 |
-
outputs = pipe(
|
| 301 |
-
messages,
|
| 302 |
-
max_new_tokens=90,
|
| 303 |
-
temperature=0.8,
|
| 304 |
-
do_sample=True,
|
| 305 |
-
)
|
| 306 |
-
|
| 307 |
-
print("========== MYCO OUTPUT ==========")
|
| 308 |
-
print(outputs)
|
| 309 |
-
print("=================================")
|
| 310 |
-
|
| 311 |
-
last = outputs[0]["generated_text"][-1]
|
| 312 |
-
|
| 313 |
-
# The pipeline returns the full generated_text list; the last entry is
|
| 314 |
-
# the new assistant turn added by the model.
|
| 315 |
-
last = outputs[0]["generated_text"][-1]
|
| 316 |
-
# last is either a dict {"role": "assistant", "content": "..."}
|
| 317 |
-
# or a plain string depending on the transformers version.
|
| 318 |
-
if isinstance(last, dict):
|
| 319 |
-
return (last.get("content") or "").strip() or None
|
| 320 |
-
return str(last).strip() or None
|
| 321 |
-
except Exception as exc:
|
| 322 |
-
print(f"[Myco] Pipeline inference error: {exc}")
|
| 323 |
-
return None
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def _discovery_reaction(current, collection):
|
| 327 |
-
"""Return Myco's first reaction to a new discovery."""
|
| 328 |
-
mushroom = mushroom_from_state(current)
|
| 329 |
-
context = _companion_context(current, collection, welcome_history(), None)
|
| 330 |
-
prompt = (
|
| 331 |
-
f"The player just discovered {mushroom.name}. "
|
| 332 |
-
f"Rarity: {mushroom.rarity}. Clue: {current.get('clue', 'No clue yet')}. "
|
| 333 |
-
f"Forest event: {current.get('event_title', 'Strange Omen')}. "
|
| 334 |
-
f"Encounter: {current.get('encounter', 'No creature yet')}. "
|
| 335 |
-
f"Mystery: {current.get('mystery_title', 'The Wrong Memory')}. "
|
| 336 |
-
"React as Myco with emotion, make the moment memorable, and invite a meaningful choice."
|
| 337 |
-
)
|
| 338 |
-
answer = _local_pipeline_answer(prompt, current, context)
|
| 339 |
-
if answer:
|
| 340 |
-
return answer
|
| 341 |
-
|
| 342 |
-
return (
|
| 343 |
-
f"We found a {mushroom.name} during {current.get('event_title', 'a strange omen')}! "
|
| 344 |
-
f"Myco feels {current.get('myco_mood', 'curious')} and whispers: "
|
| 345 |
-
f"{current.get('encounter', 'the forest is watching us')} "
|
| 346 |
-
f"Mystery: {current.get('mystery_title', 'The Wrong Memory')} — "
|
| 347 |
-
f"{current.get('mystery_reveal', 'the forest is hiding a memory')}. "
|
| 348 |
-
f"Clue: {current.get('clue', 'the forest is trying to tell us something')} "
|
| 349 |
-
"Choose Study, Pick, or Follow Whisper."
|
| 350 |
-
)
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
def _fallback_companion_answer(current, context):
|
| 354 |
-
"""Return the deterministic fallback companion answer using game context."""
|
| 355 |
-
if current:
|
| 356 |
-
mushroom = mushroom_from_state(current)
|
| 357 |
-
emotion = context["emotion"]
|
| 358 |
-
memory = context["memory"]
|
| 359 |
-
pattern = context["pattern"]
|
| 360 |
-
return (
|
| 361 |
-
f"I feel **{emotion}** about **{mushroom.name}**. {mushroom.lore} "
|
| 362 |
-
f"{pattern} {memory} "
|
| 363 |
-
"The edible, magic, and danger fields are still unknown and uncertain, so stay close to me: "
|
| 364 |
-
"Study it or Follow Whisper before risking anything."
|
| 365 |
-
)
|
| 366 |
-
return (
|
| 367 |
-
"I am right beside you. Move through the clearing and search again — "
|
| 368 |
-
"I will watch for memories, poison omens, and clues you might miss."
|
| 369 |
-
)
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
def _build_messages(message, current, context):
|
| 373 |
-
"""Build the chat messages list for the local pipeline."""
|
| 374 |
-
mushroom_context = "No mushroom is currently discovered."
|
| 375 |
-
if current:
|
| 376 |
-
mushroom = mushroom_from_state(current)
|
| 377 |
-
mushroom_context = (
|
| 378 |
-
f"Current mushroom: {mushroom.name}. "
|
| 379 |
-
f"Rarity: {mushroom.rarity}. "
|
| 380 |
-
f"Edible: {mushroom.edible}. "
|
| 381 |
-
f"Magic: {mushroom.magic}. "
|
| 382 |
-
f"Danger: {mushroom.danger}. "
|
| 383 |
-
f"Lore: {mushroom.lore}. "
|
| 384 |
-
f"Clue: {current.get('clue', 'Unknown')}. "
|
| 385 |
-
f"Secret signal: {current.get('secret', 'Unknown')}"
|
| 386 |
-
)
|
| 387 |
-
|
| 388 |
-
companion_context = (
|
| 389 |
-
f"Location: {context['location']}. "
|
| 390 |
-
f"Weather: {context['weather']}. "
|
| 391 |
-
f"Quest: {context['quest']}. "
|
| 392 |
-
f"Quest progress: {context['progress']}. "
|
| 393 |
-
f"Rare finds: {context['rare_finds']}. "
|
| 394 |
-
f"Notable event: {context['notable_event']}. "
|
| 395 |
-
f"Emotion: {context['emotion']}. "
|
| 396 |
-
f"Pattern Myco noticed: {context['pattern']}. "
|
| 397 |
-
f"Memory: {context['memory']}. "
|
| 398 |
-
f"Recent conversation: {context['conversation']}"
|
| 399 |
-
)
|
| 400 |
-
|
| 401 |
-
return [
|
| 402 |
-
{
|
| 403 |
-
"role": "system",
|
| 404 |
-
"content": (
|
| 405 |
-
f"{MYCO_CHARACTER_SPEC}\n\n"
|
| 406 |
-
f"Current location: {context['location']}\n"
|
| 407 |
-
f"Weather: {context['weather']}\n"
|
| 408 |
-
f"Rare mushrooms found: {context['rare_finds']}\n"
|
| 409 |
-
f"Quest progress: {context['progress']}\n\n"
|
| 410 |
-
"Stay in character as Myco. Do not act like a generic assistant. "
|
| 411 |
-
"Never claim an unknown mushroom is safe to eat. "
|
| 412 |
-
"Answer in 1-3 short sentences."
|
| 413 |
-
),
|
| 414 |
-
},
|
| 415 |
-
{
|
| 416 |
-
"role": "user",
|
| 417 |
-
"content": f"{mushroom_context} {companion_context} Player asks: {message}",
|
| 418 |
-
},
|
| 419 |
-
]
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
# ---------------------------------------------------------------------------
|
| 423 |
-
# Game actions
|
| 424 |
-
# ---------------------------------------------------------------------------
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
def collect_current(current=None, collection=None, history=None):
|
| 428 |
-
"""Add the current mushroom to the MycoDex if it is not already collected."""
|
| 429 |
-
current_collection = _safe_collection(collection)
|
| 430 |
-
current_history = _safe_history(history)
|
| 431 |
-
|
| 432 |
-
if current is None:
|
| 433 |
-
return current_collection, _append_assistant(
|
| 434 |
-
current_history,
|
| 435 |
-
"Let's discover a mushroom before updating the MycoDex.",
|
| 436 |
-
)
|
| 437 |
-
|
| 438 |
-
if collection_contains(current_collection, current["name"]):
|
| 439 |
-
return current_collection, _append_assistant(
|
| 440 |
-
current_history,
|
| 441 |
-
f"{current['name']} is already safe in your MycoDex.",
|
| 442 |
-
)
|
| 443 |
-
|
| 444 |
-
score_delta = _score_value(current)
|
| 445 |
-
scored_current = _with_reward_state(current, current_collection, score_delta)
|
| 446 |
-
updated_collection = [*current_collection, scored_current]
|
| 447 |
-
secret_note = current.get("secret", "The MycoDex pages rustle like leaves.")
|
| 448 |
-
return updated_collection, _append_assistant(
|
| 449 |
-
current_history,
|
| 450 |
-
(
|
| 451 |
-
f"+{score_delta} spores! Added {current['name']} to the MycoDex. "
|
| 452 |
-
f"{secret_note} Score: {scored_current['score_total']}."
|
| 453 |
-
),
|
| 454 |
-
)
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
def pick_current(current=None, collection=None, history=None):
|
| 458 |
-
"""Pick the current mushroom like a game item; poisonous picks end the run."""
|
| 459 |
-
current_collection = _safe_collection(collection)
|
| 460 |
-
current_history = _safe_history(history)
|
| 461 |
-
if current is None:
|
| 462 |
-
return current_collection, None, _append_assistant(
|
| 463 |
-
current_history,
|
| 464 |
-
"Move Myco, search a clearing, then pick the mushroom that appears.",
|
| 465 |
-
)
|
| 466 |
-
|
| 467 |
-
if _is_poisonous(current):
|
| 468 |
-
score_total = max(0, _score_collection(current_collection) + POISON_SCORE_PENALTY)
|
| 469 |
-
game_over = {
|
| 470 |
-
**current,
|
| 471 |
-
"danger": "Poisonous",
|
| 472 |
-
"game_over": "Yes",
|
| 473 |
-
"health": "0",
|
| 474 |
-
"score_delta": str(POISON_SCORE_PENALTY),
|
| 475 |
-
"score_total": str(score_total),
|
| 476 |
-
"reward_text": "Poison hit! -25 spores · Health 0/3",
|
| 477 |
-
}
|
| 478 |
-
return current_collection, game_over, _append_assistant(
|
| 479 |
-
current_history,
|
| 480 |
-
(
|
| 481 |
-
f"💀 GAME OVER — {current['name']} was poisonous. "
|
| 482 |
-
f"{POISON_SCORE_PENALTY} spores and health dropped to 0/3. "
|
| 483 |
-
"Myco pulls you back as the clearing goes silent."
|
| 484 |
-
),
|
| 485 |
-
)
|
| 486 |
-
|
| 487 |
-
score_delta = _score_value(current)
|
| 488 |
-
picked = _with_reward_state(
|
| 489 |
-
{**current, "picked": "Yes", "danger": "No obvious poison"},
|
| 490 |
-
current_collection,
|
| 491 |
-
score_delta,
|
| 492 |
-
)
|
| 493 |
-
if collection_contains(current_collection, picked["name"]):
|
| 494 |
-
return current_collection, picked, _append_assistant(
|
| 495 |
-
current_history,
|
| 496 |
-
f"{picked['name']} was already picked and logged in the MycoDex.",
|
| 497 |
-
)
|
| 498 |
-
|
| 499 |
-
updated_collection = [*current_collection, picked]
|
| 500 |
-
bonus = _reward_text(picked, score_delta)
|
| 501 |
-
return updated_collection, picked, _append_assistant(
|
| 502 |
-
current_history,
|
| 503 |
-
(
|
| 504 |
-
f"🍄 {bonus} Picked {picked['name']} safely! "
|
| 505 |
-
f"Score: {picked['score_total']} · Health {picked['health']}/3. "
|
| 506 |
-
"Myco tucks the clue into the MycoDex."
|
| 507 |
-
),
|
| 508 |
-
)
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
def follow_whisper(current=None, collection=None, history=None):
|
| 512 |
-
"""Follow the active story omen for a risk-reward discovery moment."""
|
| 513 |
-
current_collection = _safe_collection(collection)
|
| 514 |
-
current_history = _safe_history(history)
|
| 515 |
-
if current is None:
|
| 516 |
-
return None, _append_assistant(
|
| 517 |
-
current_history,
|
| 518 |
-
"Myco cups one ear. First search a clearing; the forest only whispers near mushrooms.",
|
| 519 |
-
)
|
| 520 |
-
|
| 521 |
-
if _is_poisonous(current) and current.get("studied") != "Yes":
|
| 522 |
-
game_over = {**current, "danger": "Poisonous", "game_over": "Yes"}
|
| 523 |
-
return game_over, _append_assistant(
|
| 524 |
-
current_history,
|
| 525 |
-
(
|
| 526 |
-
"💀 GAME OVER — the whisper belonged to poison. "
|
| 527 |
-
"Myco freezes, then remembers this exact mistake from a dream."
|
| 528 |
-
),
|
| 529 |
-
)
|
| 530 |
-
|
| 531 |
-
mystery = _mystery_thread(len(current_collection) + 1)
|
| 532 |
-
memory = _memory_fragment(current, len(current_collection), mystery)
|
| 533 |
-
revealed = {
|
| 534 |
-
**current,
|
| 535 |
-
**mystery,
|
| 536 |
-
"memory_fragment": memory,
|
| 537 |
-
"whisper_followed": "Yes",
|
| 538 |
-
"secret": f"{current.get('secret', 'A secret stirs.')} {memory}",
|
| 539 |
-
}
|
| 540 |
-
return revealed, _append_assistant(
|
| 541 |
-
current_history,
|
| 542 |
-
(
|
| 543 |
-
f"🌌 Myco follows the whisper and gasps: {memory} "
|
| 544 |
-
"This is not just collecting mushrooms anymore — it is recovering Myco's lost forest."
|
| 545 |
-
),
|
| 546 |
-
)
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
def study_current(current=None, history=None):
|
| 550 |
-
"""Study the current mushroom to reveal one cautious field observation."""
|
| 551 |
-
current_history = _safe_history(history)
|
| 552 |
-
if current is None:
|
| 553 |
-
return None, _append_assistant(
|
| 554 |
-
current_history,
|
| 555 |
-
"Let's find a mushroom before we study anything.",
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
-
studied = {**current, "magic": _study_hint(current["rarity"]), "studied": "Yes"}
|
| 559 |
-
clue = studied.get("clue", "The clue is still forming in the moss.")
|
| 560 |
-
message = (
|
| 561 |
-
f"I studied the {current['name']} closely. New MycoDex note: "
|
| 562 |
-
f"magic is now marked as **{studied['magic']}**. Clue: {clue} "
|
| 563 |
-
"Still no tasting without certainty!"
|
| 564 |
-
)
|
| 565 |
-
return studied, _append_assistant(current_history, message)
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
def eat_current(current=None, history=None):
|
| 569 |
-
"""Handle the Eat choice safely with Myco's companion personality."""
|
| 570 |
-
current_history = _safe_history(history)
|
| 571 |
-
if current is None:
|
| 572 |
-
return _append_assistant(
|
| 573 |
-
current_history,
|
| 574 |
-
"Let's discover a mushroom before making snack decisions.",
|
| 575 |
-
)
|
| 576 |
-
|
| 577 |
-
mushroom = mushroom_from_state(current)
|
| 578 |
-
message = (
|
| 579 |
-
f"Myco gently blocks the {mushroom.name}. "
|
| 580 |
-
f"Its edible field is still **{mushroom.edible}**, "
|
| 581 |
-
"so the winning move is to study or collect it instead of eating it."
|
| 582 |
-
)
|
| 583 |
-
return _append_assistant(current_history, message)
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
# ---------------------------------------------------------------------------
|
| 587 |
-
# Context helpers
|
| 588 |
-
# ---------------------------------------------------------------------------
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
def _companion_context(current, collection, history, position):
|
| 592 |
-
"""Summarize the current journey so Myco replies like a present companion."""
|
| 593 |
-
current_collection = _safe_collection(collection)
|
| 594 |
-
count = len(current_collection)
|
| 595 |
-
rare_names = [
|
| 596 |
-
entry.get("name", "Unknown")
|
| 597 |
-
for entry in current_collection
|
| 598 |
-
if entry.get("rarity") != "Common"
|
| 599 |
-
]
|
| 600 |
-
location = _location_label(position)
|
| 601 |
-
quest = MYSTERY_QUESTION
|
| 602 |
-
if current:
|
| 603 |
-
progress = (
|
| 604 |
-
f"{current.get('mystery_title', 'The Wrong Memory')} — "
|
| 605 |
-
f"{current.get('mystery_next', 'keep exploring')}"
|
| 606 |
-
)
|
| 607 |
-
notable_event = (
|
| 608 |
-
f"{current.get('event_emoji', '✨')} {current.get('event_title', 'Strange Omen')}: "
|
| 609 |
-
f"{current.get('encounter', 'the forest is watching')}"
|
| 610 |
-
)
|
| 611 |
-
emotion = current.get("myco_mood", "curious")
|
| 612 |
-
memory = current.get("memory_fragment") or current.get(
|
| 613 |
-
"mystery_reveal",
|
| 614 |
-
"Myco feels the forest tugging at an old memory.",
|
| 615 |
-
)
|
| 616 |
-
pattern = _pattern_notice(current, current_collection)
|
| 617 |
-
else:
|
| 618 |
-
progress = "No active discovery yet; search a clearing to start the mystery."
|
| 619 |
-
notable_event = "Myco is listening before the first omen."
|
| 620 |
-
emotion = "curious"
|
| 621 |
-
memory = "Myco remembers a path that is not on the map yet."
|
| 622 |
-
pattern = "No pattern yet, but the trees are leaning inward."
|
| 623 |
-
|
| 624 |
-
return {
|
| 625 |
-
"location": location,
|
| 626 |
-
"quest": quest,
|
| 627 |
-
"progress": progress,
|
| 628 |
-
"rare_finds": ", ".join(rare_names) if rare_names else "none yet",
|
| 629 |
-
"weather": notable_event,
|
| 630 |
-
"notable_event": notable_event,
|
| 631 |
-
"emotion": emotion,
|
| 632 |
-
"memory": memory,
|
| 633 |
-
"pattern": pattern,
|
| 634 |
-
"conversation": _recent_conversation(history),
|
| 635 |
-
"collection_count": str(count),
|
| 636 |
-
}
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
def _location_label(position):
|
| 640 |
-
"""Return a small in-world label for the grid position."""
|
| 641 |
-
if position is None:
|
| 642 |
-
return "center clearing"
|
| 643 |
-
x, y = position
|
| 644 |
-
names = {
|
| 645 |
-
(0, 0): "northwest root arch",
|
| 646 |
-
(1, 0): "northern fern gate",
|
| 647 |
-
(2, 0): "northeast moon stump",
|
| 648 |
-
(0, 1): "western moss path",
|
| 649 |
-
(1, 1): "center clearing",
|
| 650 |
-
(2, 1): "eastern glow log",
|
| 651 |
-
(0, 2): "southwest rain hollow",
|
| 652 |
-
(1, 2): "southern spore bridge",
|
| 653 |
-
(2, 2): "southeast elder shade",
|
| 654 |
-
}
|
| 655 |
-
return names.get((int(x), int(y)), "unknown clearing")
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
def _pattern_notice(current, collection):
|
| 659 |
-
"""Return one pattern Myco can comment on from the journey so far."""
|
| 660 |
-
if current.get("game_over") == "Yes":
|
| 661 |
-
return "The same poison warning appeared before Myco could say it aloud."
|
| 662 |
-
if current.get("rarity") == "Legendary":
|
| 663 |
-
return "Legendary spores bend toward the MycoDex instead of the moon."
|
| 664 |
-
if current.get("poison") == "Yes":
|
| 665 |
-
return "Poison omens arrive with black-winged moths and sudden silence."
|
| 666 |
-
if len(collection) >= 3:
|
| 667 |
-
return "Every third clue makes the MycoDex feel warmer, like it is alive."
|
| 668 |
-
if current.get("event_title") == "Moonlit Sporefall":
|
| 669 |
-
return "Moonlit spores keep behaving like memories instead of seeds."
|
| 670 |
-
return "The clues are not random; they keep pointing back to Myco's forgotten path."
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
def _recent_conversation(history):
|
| 674 |
-
"""Return a compact summary of recent player/Myco conversation turns."""
|
| 675 |
-
turns = []
|
| 676 |
-
|
| 677 |
-
for entry in _safe_history(history)[-6:]:
|
| 678 |
-
role = entry.get("role", "assistant")
|
| 679 |
-
content = entry.get("content", "")
|
| 680 |
-
|
| 681 |
-
# 1. Handle Gradio's list format (multimodal messages)
|
| 682 |
-
if isinstance(content, list):
|
| 683 |
-
pieces = []
|
| 684 |
-
for item in content:
|
| 685 |
-
if isinstance(item, dict):
|
| 686 |
-
# Extract text from dict, fallback to empty string if it's purely a file
|
| 687 |
-
if "text" in item:
|
| 688 |
-
pieces.append(item["text"])
|
| 689 |
-
elif isinstance(item, str):
|
| 690 |
-
pieces.append(item)
|
| 691 |
-
elif isinstance(item, tuple):
|
| 692 |
-
pieces.append("[Attachment]")
|
| 693 |
-
content = " ".join(pieces)
|
| 694 |
-
|
| 695 |
-
# 2. Handle Gradio's tuple format (old-style direct file uploads)
|
| 696 |
-
elif isinstance(content, tuple):
|
| 697 |
-
content = "[Attachment]"
|
| 698 |
-
|
| 699 |
-
# 3. Force to string and safely clean up
|
| 700 |
-
content = str(content).replace("\n", " ").strip()
|
| 701 |
-
|
| 702 |
-
if content:
|
| 703 |
-
turns.append(f"{role}: {content[:120]}")
|
| 704 |
-
|
| 705 |
-
return " | ".join(turns) if turns else "No conversation yet."
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
# ---------------------------------------------------------------------------
|
| 709 |
-
# Story / mystery helpers
|
| 710 |
-
# ---------------------------------------------------------------------------
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
def _mystery_clue(current, collection_count):
|
| 714 |
-
rarity = current.get("rarity", "Common")
|
| 715 |
-
name = current.get("name", "this mushroom")
|
| 716 |
-
clue = RARITY_CLUES.get(rarity, RARITY_CLUES["Common"])
|
| 717 |
-
if collection_count == 0:
|
| 718 |
-
return f"First clue: {name} marks the beginning of the hidden Spore Door trail."
|
| 719 |
-
if rarity == "Legendary":
|
| 720 |
-
return f"Legendary clue: {name} reveals a glowing Elder Map fragment."
|
| 721 |
-
if rarity == "Rare":
|
| 722 |
-
return f"Rare clue: {name} points Myco toward a secret ring of mushrooms."
|
| 723 |
-
return clue
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
def _mystery_thread(collection_count):
|
| 727 |
-
chapter = MYSTERY_CHAPTERS[0]
|
| 728 |
-
next_chapter = None
|
| 729 |
-
for candidate in MYSTERY_CHAPTERS:
|
| 730 |
-
if collection_count >= candidate["threshold"]:
|
| 731 |
-
chapter = candidate
|
| 732 |
-
elif next_chapter is None:
|
| 733 |
-
next_chapter = candidate
|
| 734 |
-
|
| 735 |
-
if next_chapter is None:
|
| 736 |
-
next_line = "The Impossible Bloom is visible. Follow the whisper for the final truth."
|
| 737 |
-
else:
|
| 738 |
-
remaining = next_chapter["threshold"] - collection_count
|
| 739 |
-
next_line = f"{remaining} clue discovery until {next_chapter['title']}."
|
| 740 |
-
|
| 741 |
-
return {
|
| 742 |
-
"mystery_title": chapter["title"],
|
| 743 |
-
"mystery_question": MYSTERY_QUESTION,
|
| 744 |
-
"mystery_progress": chapter["clue"],
|
| 745 |
-
"mystery_reveal": chapter["reveal"],
|
| 746 |
-
"mystery_next": next_line,
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
def _story_moment(current, collection_count):
|
| 751 |
-
event = FOREST_EVENTS[collection_count % len(FOREST_EVENTS)]
|
| 752 |
-
encounter = _encounter_for(current, collection_count)
|
| 753 |
-
return {
|
| 754 |
-
"event_title": event["title"],
|
| 755 |
-
"event_emoji": event["emoji"],
|
| 756 |
-
"event_choice": event["choice"],
|
| 757 |
-
"event_consequence": event["consequence"],
|
| 758 |
-
"encounter": encounter,
|
| 759 |
-
"myco_mood": event["mood"],
|
| 760 |
-
"objective": IMPOSSIBLE_OBJECTIVE,
|
| 761 |
-
}
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
def _encounter_for(current, collection_count):
|
| 765 |
-
if current.get("rarity") == "Legendary":
|
| 766 |
-
return "A mushroom guardian bows and calls Myco by an old royal name."
|
| 767 |
-
if _is_poisonous(current):
|
| 768 |
-
return "A black-winged moth lands on Myco's cap: a poison omen."
|
| 769 |
-
if current.get("rarity") == "Rare" or collection_count in (2, 5, 8):
|
| 770 |
-
return RARE_ENCOUNTERS[collection_count % len(RARE_ENCOUNTERS)]
|
| 771 |
-
return "No creature appears, but the trees lean closer to hear Myco think."
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
def _memory_fragment(current, collection_count, mystery):
|
| 775 |
-
name = current.get("name", "this mushroom")
|
| 776 |
-
reveal = mystery.get("mystery_reveal", "The forest remembers Myco.")
|
| 777 |
-
if mystery.get("mystery_title") == "The Impossible Bloom":
|
| 778 |
-
return reveal
|
| 779 |
-
if current.get("rarity") == "Legendary":
|
| 780 |
-
return f"Memory fragment: {name} once grew beside Myco's missing home. {reveal}"
|
| 781 |
-
if current.get("rarity") == "Rare":
|
| 782 |
-
return f"Memory fragment: {name} hums the name of a door under the roots. {reveal}"
|
| 783 |
-
if collection_count >= 3:
|
| 784 |
-
return f"Memory fragment: {name} points toward the Impossible Mushroom. {reveal}"
|
| 785 |
-
return f"Memory fragment: {name} shows Myco a path they do not remember walking. {reveal}"
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
def _secret_signal(current, collection_count):
|
| 789 |
-
rarity = current.get("rarity", "Common")
|
| 790 |
-
if rarity == "Legendary":
|
| 791 |
-
return "The MycoDex page blooms with a star-shaped seal."
|
| 792 |
-
if rarity == "Rare":
|
| 793 |
-
return "A silver margin note appears beside the sketch."
|
| 794 |
-
if collection_count + 1 in (1, 3, 7, 12):
|
| 795 |
-
return "A hidden path symbol flickers at the page corner."
|
| 796 |
-
return "The page smells faintly of rain and moss."
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
# ---------------------------------------------------------------------------
|
| 800 |
-
# Scoring helpers
|
| 801 |
-
# ---------------------------------------------------------------------------
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
def _score_value(current):
|
| 805 |
-
return RARITY_SCORE_VALUES.get(current.get("rarity", "Common"), 10)
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
def _score_collection(collection):
|
| 809 |
-
total = 0
|
| 810 |
-
for entry in _safe_collection(collection):
|
| 811 |
-
if entry.get("game_over") == "Yes":
|
| 812 |
-
continue
|
| 813 |
-
total += int(entry.get("score_delta") or _score_value(entry))
|
| 814 |
-
return max(0, total)
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
def _health_for_state(current, collection):
|
| 818 |
-
if current.get("game_over") == "Yes":
|
| 819 |
-
return 0
|
| 820 |
-
poison_penalties = sum(
|
| 821 |
-
1 for entry in _safe_collection(collection) if entry.get("game_over") == "Yes"
|
| 822 |
-
)
|
| 823 |
-
return max(0, PLAYER_STARTING_HEALTH - poison_penalties)
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
def _with_reward_state(current, collection, score_delta):
|
| 827 |
-
score_total = _score_collection(collection) + score_delta
|
| 828 |
-
return {
|
| 829 |
-
**current,
|
| 830 |
-
"score_delta": str(score_delta),
|
| 831 |
-
"score_total": str(max(0, score_total)),
|
| 832 |
-
"health": str(_health_for_state(current, collection)),
|
| 833 |
-
"reward_text": _reward_text(current, score_delta),
|
| 834 |
-
}
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
def _reward_text(current, score_delta):
|
| 838 |
-
if score_delta < 0:
|
| 839 |
-
return f"Poison penalty {score_delta} spores"
|
| 840 |
-
rarity = current.get("rarity", "Common")
|
| 841 |
-
if rarity == "Legendary":
|
| 842 |
-
return f"LEGENDARY +{score_delta} spores · Impossible Bloom surges"
|
| 843 |
-
if rarity == "Rare":
|
| 844 |
-
return f"RARE +{score_delta} spores · special discovery unlocked"
|
| 845 |
-
if score_delta > 0:
|
| 846 |
-
return f"+{score_delta} spores"
|
| 847 |
-
return "No spores scored yet"
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
# ---------------------------------------------------------------------------
|
| 851 |
-
# Safety / utility helpers
|
| 852 |
-
# ---------------------------------------------------------------------------
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
def _is_poisonous(current):
|
| 856 |
-
return current.get("name", "") in POISONOUS_MUSHROOMS or current.get("danger") == "Poisonous"
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
def _study_hint(rarity):
|
| 860 |
-
if rarity == "Legendary":
|
| 861 |
-
return "Strong, mysterious aura"
|
| 862 |
-
if rarity == "Rare":
|
| 863 |
-
return "Faint magical trace"
|
| 864 |
-
return "Tiny spore shimmer"
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
def _assistant_message(content):
|
| 868 |
-
return {"role": "assistant", "content": content}
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
def _append_assistant(history, content):
|
| 872 |
-
return [*_safe_history(history), _assistant_message(content)]
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
def _safe_collection(collection):
|
| 876 |
-
return list(collection or [])
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
def _safe_history(history):
|
| 880 |
-
return list(history or welcome_history())
|
| 881 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|