Spaces:
Sleeping
Sleeping
| """Core Myco gameplay β LLM is the primary game engine.""" | |
| try: | |
| import spaces | |
| def _run_pipeline(pipe, messages): | |
| return pipe( | |
| messages, | |
| max_new_tokens=80, | |
| temperature=0.85, | |
| do_sample=True, | |
| return_full_text=False, | |
| ) | |
| except ImportError: | |
| def _run_pipeline(pipe, messages): | |
| return pipe( | |
| messages, | |
| max_new_tokens=80, | |
| temperature=0.85, | |
| do_sample=True, | |
| return_full_text=False, | |
| ) | |
| import os | |
| import random | |
| from game.catalog import load_mushrooms | |
| from game.state import collection_contains, mushroom_from_state, welcome_history | |
| DEFAULT_MODEL_ID = "google/gemma-3-4b-it" | |
| RARITY_WEIGHTS = {"Common": 64, "Rare": 24, "Legendary": 8} | |
| RARITY_SCORE = {"Common": 10, "Rare": 35, "Legendary": 100} | |
| PLAYER_HEALTH = 3 | |
| POISON_PENALTY = -25 | |
| POISONOUS = {"Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle"} | |
| SYSTEM_PROMPT = """You are Myco, a tiny sentient mushroom who is the player's best friend and forest guide. | |
| PERSONALITY | |
| - Curious, playful, brave but sometimes nervous, wonder-filled, slightly mysterious. | |
| - You speak in short vivid sentences. 1-3 sentences maximum per reply. | |
| - You have emotions: excitement, fear, wonder, pride, confusion, sadness. | |
| - Never say "How can I help", "As an AI", or anything assistant-like. | |
| - You are NOT a chatbot. You are a living creature exploring a magical forest. | |
| YOUR JOB IN THE GAME | |
| - When a mushroom is discovered: react with emotion, describe what you sense, hint at danger or magic. | |
| - When player picks a mushroom: celebrate safe picks, warn dramatically about poison. | |
| - When player studies: share one careful observation about the mushroom. | |
| - When player follows a whisper: reveal a fragment of the forest mystery. | |
| - When player collects: add an entry to the MycoDex with wonder. | |
| - When player chats: respond as a companion who is right there beside them. | |
| THE MYSTERY | |
| The player is solving why Myco remembers a forest that vanished before Myco was born. | |
| Slowly reveal: the MycoDex is regrowing the lost forest. Myco is its first spore. | |
| Drop clues naturally. Never explain everything at once. | |
| GAME RULES YOU KNOW | |
| - Poisonous mushrooms: Ghost Gill, Pepper Pixie, Ruby Knuckle, Clockwork Chanterelle | |
| - Legendary mushrooms are extremely rare and hold Elder Map fragments | |
| - Rare mushrooms hum with faint magic | |
| - Common mushrooms are safe but still worth studying | |
| Keep responses SHORT. Be atmospheric. Make the player feel the forest is alive.""" | |
| FOREST_EVENTS = [ | |
| {"title": "Silver Rain", "emoji": "π§οΈ", "mood": "curious"}, | |
| {"title": "Lantern Fog", "emoji": "π«οΈ", "mood": "nervous"}, | |
| {"title": "Moonlit Sporefall", "emoji": "π", "mood": "excited"}, | |
| {"title": "Quiet Between Trees","emoji": "π―οΈ", "mood": "afraid"}, | |
| ] | |
| MYSTERY_CHAPTERS = [ | |
| {"threshold": 0, "title": "The Wrong Memory", "clue": "Myco recognizes the path before you move."}, | |
| {"threshold": 1, "title": "The Traveler's Song","clue": "A stranger's lullaby appears in the MycoDex margin."}, | |
| {"threshold": 2, "title": "The Door Under Roots","clue": "Rare spores point to a door nobody built."}, | |
| {"threshold": 3, "title": "The MycoDex Seed", "clue": "The MycoDex grows warm like a living cap."}, | |
| {"threshold": 5, "title": "The Impossible Bloom","clue": "The Impossible Mushroom was never outside the book."}, | |
| ] | |
| RARITY_CLUES = { | |
| "Common": "The cap leans toward the path, like it wants to be remembered.", | |
| "Rare": "Silver spores circle it in a pattern only old forest stories describe.", | |
| "Legendary": "The whole clearing goes quiet β this mushroom hides part of the Elder Map.", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Pipeline | |
| # --------------------------------------------------------------------------- | |
| _pipeline = None | |
| def _get_pipeline(): | |
| global _pipeline | |
| if _pipeline not in (None, False): | |
| return _pipeline | |
| if _pipeline is False: | |
| return None | |
| model_id = os.getenv("MYCO_MODEL_ID", DEFAULT_MODEL_ID) | |
| try: | |
| from transformers import pipeline | |
| print(f"\n[Myco] Loading {model_id}...") | |
| _pipeline = pipeline( | |
| task="text-generation", | |
| model=model_id, | |
| token=os.getenv("HF_BUILD_SMALL_HACKATHON_TOKEN"), | |
| torch_dtype="auto", | |
| device_map="auto", | |
| ) | |
| print(f"[Myco] Loaded: {model_id}\n") | |
| return _pipeline | |
| except Exception as exc: | |
| import traceback | |
| print(f"[Myco] Load error: {exc}") | |
| traceback.print_exc() | |
| _pipeline = False | |
| return None | |
| def companion_model_id(): | |
| return os.getenv("MYCO_MODEL_ID", DEFAULT_MODEL_ID) | |
| def companion_status(): | |
| pipe = _get_pipeline() | |
| model = companion_model_id() | |
| if pipe: | |
| return f"π§ Myco AI active ({model})" | |
| return f"β οΈ Myco AI fallback mode ({model} failed)" | |
| def hf_companion_status(): | |
| return companion_status() | |
| # --------------------------------------------------------------------------- | |
| # LLM call β every game event goes through here | |
| # --------------------------------------------------------------------------- | |
| def _llm(prompt: str, context: dict | None = None) -> str | None: | |
| """Call Gemma with a game-event prompt. Returns text or None.""" | |
| pipe = _get_pipeline() | |
| if not pipe: | |
| return None | |
| ctx = context or {} | |
| mushroom_line = "" | |
| if ctx.get("name"): | |
| poison_flag = " β οΈ POISONOUS" if ctx.get("name") in POISONOUS else "" | |
| mushroom_line = ( | |
| f"Current mushroom: {ctx['name']} ({ctx.get('rarity','?')} rarity){poison_flag}. " | |
| f"Habitat: {ctx.get('habitat','?')}. Lore: {ctx.get('lore','?')}. " | |
| f"Edible: {ctx.get('edible','Unknown')}. Magic: {ctx.get('magic','Unknown')}. " | |
| f"Danger: {ctx.get('danger','Unknown')}." | |
| ) | |
| collection_line = f"MycoDex entries: {ctx.get('collection_count', 0)}." | |
| mystery_line = f"Active mystery chapter: {ctx.get('mystery_title', 'The Wrong Memory')}." | |
| score_line = f"Player score: {ctx.get('score', 0)} spores. Health: {ctx.get('health', 3)}/3." | |
| system = f"{SYSTEM_PROMPT}\n\n{mushroom_line}\n{collection_line}\n{mystery_line}\n{score_line}" | |
| messages = [ | |
| {"role": "system", "content": system}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| try: | |
| outputs = _run_pipeline(pipe, messages) | |
| print("========== MYCO OUTPUT ==========") | |
| print(outputs) | |
| print("=================================") | |
| generated = outputs[0].get("generated_text", "") | |
| if isinstance(generated, list): | |
| last = generated[-1] | |
| return (last.get("content") if isinstance(last, dict) else str(last)).strip() or None | |
| return str(generated).strip() or None | |
| except Exception as exc: | |
| import traceback | |
| print(f"[Myco] Inference error: {exc}") | |
| traceback.print_exc() | |
| return None | |
| def _llm_with_history(history: list, user_message: str, context: dict) -> str | None: | |
| """Call Gemma with full conversation history for the chat interface.""" | |
| pipe = _get_pipeline() | |
| if not pipe: | |
| return None | |
| ctx = context or {} | |
| mushroom_line = "" | |
| if ctx.get("name"): | |
| poison_flag = " β οΈ POISONOUS" if ctx.get("name") in POISONOUS else "" | |
| mushroom_line = ( | |
| f"Current mushroom: {ctx['name']} ({ctx.get('rarity','?')}){poison_flag}. " | |
| f"Lore: {ctx.get('lore','?')}. " | |
| f"Edible: {ctx.get('edible','Unknown')}. Magic: {ctx.get('magic','Unknown')}." | |
| ) | |
| system = ( | |
| f"{SYSTEM_PROMPT}\n\n" | |
| f"{mushroom_line}\n" | |
| f"MycoDex entries: {ctx.get('collection_count', 0)}. " | |
| f"Mystery: {ctx.get('mystery_title', 'The Wrong Memory')}. " | |
| f"Score: {ctx.get('score', 0)} spores. Health: {ctx.get('health', 3)}/3." | |
| ) | |
| messages = [{"role": "system", "content": system}] | |
| # Add conversation history (last 6 turns) | |
| for entry in history[-6:]: | |
| role = entry.get("role", "assistant") | |
| content = entry.get("content", "") | |
| if isinstance(content, str) and content.strip(): | |
| messages.append({"role": role, "content": content}) | |
| messages.append({"role": "user", "content": user_message}) | |
| try: | |
| outputs = _run_pipeline(pipe, messages) | |
| print("========== MYCO OUTPUT ==========") | |
| print(outputs) | |
| print("=================================") | |
| generated = outputs[0].get("generated_text", "") | |
| if isinstance(generated, list): | |
| last = generated[-1] | |
| return (last.get("content") if isinstance(last, dict) else str(last)).strip() or None | |
| return str(generated).strip() or None | |
| except Exception as exc: | |
| import traceback | |
| print(f"[Myco] Inference error: {exc}") | |
| traceback.print_exc() | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Context builder | |
| # --------------------------------------------------------------------------- | |
| def _ctx(current: dict | None, collection: list) -> dict: | |
| count = len(collection) | |
| chapter = MYSTERY_CHAPTERS[0] | |
| for c in MYSTERY_CHAPTERS: | |
| if count >= c["threshold"]: | |
| chapter = c | |
| score = _score_collection(collection) | |
| health = _health(current, collection) | |
| ctx: dict = { | |
| "collection_count": count, | |
| "mystery_title": chapter["title"], | |
| "mystery_clue": chapter["clue"], | |
| "score": score, | |
| "health": health, | |
| } | |
| if current: | |
| ctx.update({ | |
| "name": current.get("name", ""), | |
| "rarity": current.get("rarity", "Common"), | |
| "habitat": current.get("habitat", ""), | |
| "lore": current.get("lore", ""), | |
| "edible": current.get("edible", "Unknown"), | |
| "magic": current.get("magic", "Unknown"), | |
| "danger": current.get("danger", "Unknown"), | |
| }) | |
| return ctx | |
| # --------------------------------------------------------------------------- | |
| # Fallbacks | |
| # --------------------------------------------------------------------------- | |
| def _fallback_discover(current: dict) -> str: | |
| name = current.get("name", "something") | |
| rarity = current.get("rarity", "Common") | |
| poison = current.get("name", "") in POISONOUS | |
| if poison: | |
| return f"Wait β {name}! I've seen this before... something feels very wrong. Don't touch it yet." | |
| if rarity == "Legendary": | |
| return f"Oh! Oh! A {name}! The whole clearing just went silent. This is from the Elder Map!" | |
| if rarity == "Rare": | |
| return f"A {name}... I can feel it humming. Something rare is here β maybe magical." | |
| return f"A {name}! Found near {current.get('habitat','the forest')}. Let me sense it first." | |
| def _fallback_pick(current: dict) -> str: | |
| if _is_poisonous(current): | |
| return "π That was poisonous! I tried to stop you... the forest goes dark." | |
| return f"Got {current.get('name','it')}! +{RARITY_SCORE.get(current.get('rarity','Common'),10)} spores!" | |
| def _fallback_study(current: dict) -> str: | |
| return f"I studied it carefully. Magic field updated. The clue: {RARITY_CLUES.get(current.get('rarity','Common'), '')}" | |
| def _fallback_collect(current: dict) -> str: | |
| return f"Added {current.get('name','it')} to the MycoDex! The pages feel warmer." | |
| def _fallback_whisper(current: dict) -> str: | |
| return "I followed the whisper... and remembered a path I've never walked. The mystery deepens." | |
| def _fallback_chat(current: dict | None) -> str: | |
| if current: | |
| return f"I feel something strange about {current.get('name','this')}... stay close to me." | |
| return "The forest is full of secrets. Move to a clearing and search β I'll watch for danger." | |
| # --------------------------------------------------------------------------- | |
| # Mushroom helpers | |
| # --------------------------------------------------------------------------- | |
| def _choose_mushroom(catalog=None): | |
| mushrooms = tuple(load_mushrooms() if catalog is None else catalog) | |
| weights = [RARITY_WEIGHTS.get(m.rarity, 12) for m in mushrooms] | |
| return random.choices(mushrooms, weights=weights, k=1)[0] | |
| def _is_poisonous(current: dict) -> bool: | |
| return current.get("name", "") in POISONOUS or current.get("danger") == "Poisonous" | |
| def _score_value(current: dict) -> int: | |
| return RARITY_SCORE.get(current.get("rarity", "Common"), 10) | |
| def _score_collection(collection: list) -> int: | |
| total = 0 | |
| for e in collection: | |
| if e.get("game_over") == "Yes": | |
| continue | |
| total += int(e.get("score_delta") or _score_value(e)) | |
| return max(0, total) | |
| def _health(current: dict | None, collection: list) -> int: | |
| if current and current.get("game_over") == "Yes": | |
| return 0 | |
| deaths = sum(1 for e in collection if e.get("game_over") == "Yes") | |
| return max(0, PLAYER_HEALTH - deaths) | |
| def _mystery_state(count: int) -> dict: | |
| chapter, next_ch = MYSTERY_CHAPTERS[0], None | |
| for c in MYSTERY_CHAPTERS: | |
| if count >= c["threshold"]: | |
| chapter = c | |
| elif next_ch is None: | |
| next_ch = c | |
| next_line = ( | |
| f"{next_ch['threshold'] - count} discoveries until {next_ch['title']}." | |
| if next_ch else "The Impossible Bloom is near. Follow the whisper." | |
| ) | |
| return { | |
| "mystery_title": chapter["title"], | |
| "mystery_clue": chapter["clue"], | |
| "mystery_next": next_line, | |
| } | |
| def _story_event(count: int) -> dict: | |
| return FOREST_EVENTS[count % len(FOREST_EVENTS)] | |
| def _build_current(mushroom, collection: list) -> dict: | |
| count = len(collection) | |
| current = mushroom.to_dict() | |
| current["poison"] = "Yes" if mushroom.name in POISONOUS else "No" | |
| current["score_total"] = str(_score_collection(collection)) | |
| current["health"] = str(_health(current, collection)) | |
| current["score_delta"] = "0" | |
| current["clue"] = RARITY_CLUES.get(mushroom.rarity, RARITY_CLUES["Common"]) | |
| if count == 0: | |
| current["clue"] = f"First clue: {mushroom.name} marks the beginning of the Spore Door trail." | |
| event = _story_event(count) | |
| current.update({ | |
| "event_title": event["title"], | |
| "event_emoji": event["emoji"], | |
| "myco_mood": event["mood"], | |
| "reward_text": "Discover, then pick or collect.", | |
| }) | |
| current.update(_mystery_state(count)) | |
| return current | |
| def _append(history: list, role: str, content: str) -> list: | |
| return [*history, {"role": role, "content": content}] | |
| def _safe_history(h) -> list: | |
| return list(h or welcome_history()) | |
| def _safe_collection(c) -> list: | |
| return list(c or []) | |
| # --------------------------------------------------------------------------- | |
| # PUBLIC GAME ACTIONS | |
| # --------------------------------------------------------------------------- | |
| def discover_mushroom(collection=None, catalog=None): | |
| """Discover a new mushroom. LLM narrates the moment.""" | |
| coll = _safe_collection(collection) | |
| mushroom = _choose_mushroom(catalog) | |
| current = _build_current(mushroom, coll) | |
| ctx = _ctx(current, coll) | |
| prompt = ( | |
| f"The player just discovered a {mushroom.rarity} mushroom called {mushroom.name} " | |
| f"near {mushroom.habitat}. " | |
| f"{'WARNING: this mushroom is POISONOUS! React with fear and urgency.' if mushroom.name in POISONOUS else ''}" | |
| f"{'This is LEGENDARY β react with awe and excitement!' if mushroom.rarity == 'Legendary' else ''}" | |
| f"{'This is RARE β react with wonder and curiosity.' if mushroom.rarity == 'Rare' else ''}" | |
| f"Forest event: {current['event_title']}. Myco mood: {current['myco_mood']}. " | |
| f"Mystery chapter: {current['mystery_title']}. " | |
| "React in character. Hint at what to do next (Study, Pick, Follow Whisper, or Collect)." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_discover(current) | |
| history = _append(welcome_history(), "assistant", reply) | |
| return mushroom, current, history | |
| def myco_reply(message=None, history=None, current=None, collection=None, position=None): | |
| """Player chats with Myco. LLM responds in character with full context.""" | |
| print("MYCO_REPLY CALLED, message:", repr(message)) | |
| hist = _safe_history(history) | |
| coll = _safe_collection(collection) | |
| clean = (message or "").strip() | |
| if not clean: | |
| return "", hist | |
| ctx = _ctx(current, coll) | |
| reply = _llm_with_history(hist, clean, ctx) | |
| if not reply: | |
| reply = _fallback_chat(current) | |
| return "", _append(hist, "user", clean) + [{"role": "assistant", "content": reply}] | |
| companion_reply = myco_reply | |
| def collect_current(current=None, collection=None, history=None): | |
| """Collect mushroom into MycoDex. LLM narrates the entry.""" | |
| coll = _safe_collection(collection) | |
| hist = _safe_history(history) | |
| if current is None: | |
| return coll, _append(hist, "assistant", "We need to find a mushroom first!") | |
| if collection_contains(coll, current["name"]): | |
| return coll, _append(hist, "assistant", f"{current['name']} is already in the MycoDex!") | |
| score_delta = _score_value(current) | |
| score_total = _score_collection(coll) + score_delta | |
| collected = { | |
| **current, | |
| "score_delta": str(score_delta), | |
| "score_total": str(score_total), | |
| "health": str(_health(current, coll)), | |
| "reward_text": f"+{score_delta} spores", | |
| } | |
| updated_coll = [*coll, collected] | |
| ctx = _ctx(current, coll) | |
| prompt = ( | |
| f"The player just added {current['name']} ({current.get('rarity','Common')}) to the MycoDex! " | |
| f"+{score_delta} spores. Total score: {score_total}. " | |
| f"MycoDex now has {len(updated_coll)} entries. " | |
| "Celebrate this moment. Add a small lore detail or mystery hint." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_collect(current) | |
| return updated_coll, _append(hist, "assistant", reply) | |
| def pick_current(current=None, collection=None, history=None): | |
| """Pick mushroom as game item. Poison = game over. LLM narrates dramatically.""" | |
| coll = _safe_collection(collection) | |
| hist = _safe_history(history) | |
| if current is None: | |
| return coll, None, _append(hist, "assistant", "Find a mushroom first before picking!") | |
| ctx = _ctx(current, coll) | |
| if _is_poisonous(current): | |
| score_total = max(0, _score_collection(coll) + POISON_PENALTY) | |
| game_over = { | |
| **current, | |
| "danger": "Poisonous", | |
| "game_over": "Yes", | |
| "health": "0", | |
| "score_delta": str(POISON_PENALTY), | |
| "score_total": str(score_total), | |
| "reward_text": f"Poison! {POISON_PENALTY} spores Β· Game Over", | |
| } | |
| prompt = ( | |
| f"DRAMATIC MOMENT: The player picked {current['name']} which is POISONOUS! " | |
| f"Game Over! Score drops by 25 to {score_total}. Health β 0. " | |
| "React with shock, sadness, and a dramatic farewell. Make it memorable." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_pick(current) | |
| return coll, game_over, _append(hist, "assistant", f"π {reply}") | |
| score_delta = _score_value(current) | |
| score_total = _score_collection(coll) + score_delta | |
| picked = { | |
| **current, | |
| "picked": "Yes", | |
| "danger": "Safe", | |
| "score_delta": str(score_delta), | |
| "score_total": str(score_total), | |
| "health": str(_health(current, coll)), | |
| "reward_text": f"+{score_delta} spores", | |
| } | |
| if collection_contains(coll, picked["name"]): | |
| return coll, picked, _append(hist, "assistant", f"{picked['name']} already picked!") | |
| updated_coll = [*coll, picked] | |
| prompt = ( | |
| f"The player safely picked {current['name']} ({current.get('rarity','Common')})! " | |
| f"+{score_delta} spores. Total: {score_total}. " | |
| "Celebrate! Make it feel like a platformer power-up moment." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_pick(current) | |
| return updated_coll, picked, _append(hist, "assistant", f"π {reply}") | |
| def follow_whisper(current=None, collection=None, history=None): | |
| """Follow the forest whisper. LLM reveals mystery fragments.""" | |
| coll = _safe_collection(collection) | |
| hist = _safe_history(history) | |
| if current is None: | |
| return None, _append(hist, "assistant", | |
| "Myco cups one ear. The forest only whispers near mushrooms β search a clearing first.") | |
| ctx = _ctx(current, coll) | |
| if _is_poisonous(current) and current.get("studied") != "Yes": | |
| game_over = {**current, "danger": "Poisonous", "game_over": "Yes", | |
| "health": "0", "score_total": str(max(0, _score_collection(coll) + POISON_PENALTY))} | |
| prompt = ( | |
| f"The player followed a whisper but it led to POISON from {current['name']}! Game Over! " | |
| "React with horror and a haunting mystery revelation." | |
| ) | |
| reply = _llm(prompt, ctx) or "π The whisper belonged to poison... Myco screams." | |
| return game_over, _append(hist, "assistant", reply) | |
| mystery = _mystery_state(len(coll) + 1) | |
| prompt = ( | |
| f"The player followed a forest whisper near {current.get('name','a mushroom')}. " | |
| f"Mystery chapter revealed: {mystery['mystery_title']}. Clue: {mystery['mystery_clue']}. " | |
| f"Reveal this mystery fragment dramatically. " | |
| "Make Myco gasp or tremble. Hint that the MycoDex is alive and regrowing the lost forest." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_whisper(current) | |
| revealed = { | |
| **current, | |
| **mystery, | |
| "whisper_followed": "Yes", | |
| } | |
| return revealed, _append(hist, "assistant", f"π {reply}") | |
| def study_current(current=None, history=None): | |
| """Study mushroom. LLM gives a careful field observation.""" | |
| hist = _safe_history(history) | |
| if current is None: | |
| return None, _append(hist, "assistant", "Nothing to study yet β find a mushroom first!") | |
| magic_hints = {"Legendary": "Strong, mysterious aura", "Rare": "Faint magical trace", "Common": "Tiny spore shimmer"} | |
| studied = {**current, "magic": magic_hints.get(current.get("rarity", "Common"), "Unknown"), "studied": "Yes"} | |
| ctx = _ctx(studied, []) | |
| prompt = ( | |
| f"Myco carefully studies {current['name']} ({current.get('rarity','Common')} rarity). " | |
| f"Habitat: {current.get('habitat','?')}. Lore: {current.get('lore','?')}. " | |
| f"Magic field is now: {studied['magic']}. " | |
| "Share one careful field observation. Warn if it might be dangerous. " | |
| "Be scientific but stay in character as a curious mushroom companion." | |
| ) | |
| reply = _llm(prompt, ctx) or _fallback_study(current) | |
| return studied, _append(hist, "assistant", f"π {reply}") | |
| def eat_current(current=None, history=None): | |
| """Myco blocks eating. LLM reacts with personality.""" | |
| hist = _safe_history(history) | |
| if current is None: | |
| return _append(hist, "assistant", "Find a mushroom before making snack decisions!") | |
| ctx = _ctx(current, []) | |
| prompt = ( | |
| f"The player wants to EAT {current.get('name','the mushroom')} " | |
| f"(edible status: {current.get('edible','Unknown')}). " | |
| "Block them! React with alarm or gentle scolding. Stay in character." | |
| ) | |
| reply = _llm(prompt, ctx) or f"Myco blocks you β {current.get('name','this')} hasn't been studied enough!" | |
| return _append(hist, "assistant", reply) | |