byte-vortex commited on
Commit
c1eb573
Β·
verified Β·
1 Parent(s): 15306d3

Upload folder using huggingface_hub

Browse files
.github/workflows/sync_to_hf.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout repository code
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - name: Set up Python environment
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.10'
21
+
22
+ - name: Install Hugging Face Hub library
23
+ run: pip install huggingface_hub
24
+
25
+ - name: Upload project files to HF Space
26
+ env:
27
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
28
+ run: |
29
+ python -c "
30
+ import os
31
+ from huggingface_hub import HfApi
32
+
33
+ # Initialize the API using your GitHub Actions secret
34
+ api = HfApi(token=os.environ['HF_TOKEN'])
35
+
36
+ # Upload everything in the repository folder directly to your Space
37
+ api.upload_folder(
38
+ folder_path='.',
39
+ repo_id='byte-vortex/Myco',
40
+ repo_type='space'
41
+ )
42
+ "
.gitignore ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ venv/
8
+ .venv/
9
+ env/
10
+ ENV/
11
+
12
+ # Environment variables
13
+ .env
14
+ .env.local
15
+
16
+ # Gradio
17
+ .gradio/
18
+
19
+ # Hugging Face cache
20
+ .cache/
21
+ huggingface_cache/
22
+
23
+ # Jupyter
24
+ .ipynb_checkpoints/
25
+
26
+ # macOS
27
+ .DS_Store
28
+
29
+ # VS Code
30
+ .vscode/
31
+
32
+ # PyCharm
33
+ .idea/
34
+
35
+ # Logs
36
+ *.log
37
+
38
+ # Build artifacts
39
+ build/
40
+ dist/
41
+ *.egg-info/
42
+
43
+ # Model files
44
+ *.gguf
45
+ *.bin
46
+ *.safetensors
47
+ models/downloads/
48
+ models/**/*.gguf
49
+ models/**/*.bin
50
+ models/**/*.safetensors
51
+
52
+ # Temporary files
53
+ tmp/
54
+ temp/
CODEx_USAGE.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Codex Usage
2
+
3
+ This repository intentionally documents how Codex contributed to the project so the Codex track can evaluate both the product quality and the development process.
4
+
5
+ ## Codex-Assisted Milestones
6
+
7
+ - Generated the initial Gradio skeleton for Myco's first playable loop.
8
+ - Implemented the mushroom discovery engine and companion response flow.
9
+ - Added MycoDex collection tracking with duplicate prevention.
10
+ - Refactored the app into clearer `app.py`, `game/`, `models/`, `ui/`, `data/`, and `tests/` sections.
11
+ - Added an in-repo mushroom catalog with Common, Rare, and Legendary discoveries.
12
+ - Added tests for the catalog, discovery loop, companion replies, collection tracking, and markdown renderers.
13
+
14
+ ## Design Goal
15
+
16
+ Myco is built to be a polished, small-scope project rather than a sprawling prototype: explore, discover a mushroom, ask Myco, and add the discovery to the MycoDex.
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Noriko Kono
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,15 +1,82 @@
1
- ---
2
- title: Myco
3
- emoji: 🌍
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.16.0
8
- python_version: '3.13'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: Build Small Hackathon Project
13
- ---
14
-
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ„ Myco
2
+
3
+ Myco is a cozy exploration game prototype for Gradio + Hugging Face Spaces. Myco, an AI mushroom companion, helps players discover strange fungi in a magical forest and collect them in a MycoDex.
4
+
5
+ ## First 60 Seconds
6
+
7
+ 1. Open the game.
8
+ 2. Meet Myco: "Hello! I'm Myco. Let's explore the forest."
9
+ 3. Click **Explore**.
10
+ 4. Discover a mushroom, such as **Glowcap**.
11
+ 5. Ask Myco about it.
12
+ 6. Add it to the **MycoDex**.
13
+
14
+ ## Hackathon MVP
15
+
16
+ - Explore button
17
+ - Mushroom discovery
18
+ - Myco chat panel
19
+ - MycoDex collection
20
+ - 30 starter mushroom species
21
+ - Common, Rare, and Legendary rarity tiers
22
+
23
+ The MVP intentionally skips combat, multiplayer, and a huge map so the first playable loop is clear and achievable.
24
+
25
+
26
+ ## Project Structure
27
+
28
+ ```text
29
+ hf-gradio-small-hackathon-myco/
30
+ β”œβ”€β”€ app.py
31
+ β”œβ”€β”€ requirements.txt
32
+ β”œβ”€β”€ .gitignore
33
+ β”œβ”€β”€ assets/
34
+ β”‚ β”œβ”€β”€ images/
35
+ β”‚ └── sounds/
36
+ β”œβ”€β”€ data/
37
+ β”œβ”€β”€ game/
38
+ β”œβ”€β”€ models/
39
+ β”œβ”€β”€ ui/
40
+ β”œβ”€β”€ tests/
41
+ β”œβ”€β”€ CODEx_USAGE.md
42
+ └── README.md
43
+ ```
44
+
45
+ `assets/` and `data/` are tracked with placeholder files so art, sounds, and game data have obvious homes. `models/` contains lightweight Python domain models, while heavyweight local model downloads remain ignored by Git through file patterns and `models/downloads/`.
46
+
47
+ ## Local Development
48
+
49
+ ```bash
50
+ python -m venv .venv
51
+ source .venv/bin/activate
52
+ pip install -r requirements.txt
53
+ python app.py
54
+ ```
55
+
56
+ Then open the local Gradio URL printed in the terminal.
57
+
58
+ ## Hugging Face Space
59
+
60
+ Create a Gradio Space and include these files at the repository root:
61
+
62
+ ```text
63
+ README.md
64
+ app.py
65
+ requirements.txt
66
+ .gitignore
67
+ ```
68
+
69
+ Hugging Face Spaces will install `requirements.txt` and run `app.py`.
70
+
71
+ ## Future Ideas
72
+
73
+ - Mushroom rarity progression
74
+ - Mushroom villages
75
+ - Mushroom recipes
76
+ - Mushroom evolution
77
+ - Generated mushroom art
78
+ - Optional small-model backend such as `gpt-oss-20b`
79
+
80
+ ## Codex Track Notes
81
+
82
+ See [`CODEx_USAGE.md`](CODEx_USAGE.md) for a concise record of how Codex helped shape the app architecture, gameplay loop, refactor, and tests.
app.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Spaces entrypoint for Myco."""
2
+
3
+ from ui.gradio_app import build_app
4
+
5
+
6
+ demo = build_app()
7
+
8
+ if __name__ == "__main__":
9
+ demo.launch()
assets/images/.gitkeep ADDED
File without changes
assets/sounds/.gitkeep ADDED
File without changes
data/.gitkeep ADDED
File without changes
data/mushrooms.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {"name": "Glowcap", "rarity": "Common", "habitat": "mossy logs", "lore": "I think this one glows at night, but I'm not completely sure."},
3
+ {"name": "Moonveil", "rarity": "Rare", "habitat": "moonlit clearings", "lore": "Its veil catches moonbeams like a tiny silver net."},
4
+ {"name": "Honeybell", "rarity": "Common", "habitat": "old beehives", "lore": "Forest beetles nap beneath its sticky amber cap."},
5
+ {"name": "Ember Button", "rarity": "Rare", "habitat": "warm ash beds", "lore": "It keeps a gentle heat long after campfires fade."},
6
+ {"name": "Velvet Spore", "rarity": "Common", "habitat": "fern shadows", "lore": "Touching the cap leaves a soft violet dust on your glove."},
7
+ {"name": "Rainwhistle", "rarity": "Common", "habitat": "stream banks", "lore": "When rain is near, its hollow stem hums a little tune."},
8
+ {"name": "Fox Lantern", "rarity": "Rare", "habitat": "fox trails", "lore": "Foxes seem to use these as quiet waymarkers after dusk."},
9
+ {"name": "Stardrop Morel", "rarity": "Legendary", "habitat": "fallen star hollows", "lore": "Its pits sparkle like someone tucked the night sky inside."},
10
+ {"name": "Puddle Parasol", "rarity": "Common", "habitat": "muddy puddles", "lore": "It opens wide enough to shelter a sleepy field mouse."},
11
+ {"name": "Lacefin", "rarity": "Rare", "habitat": "misty slopes", "lore": "The frills look delicate, but they spring back after every breeze."},
12
+ {"name": "Bluebell Inkcap", "rarity": "Common", "habitat": "bluebell patches", "lore": "It leaves blue-black trails that fade into tiny flower shapes."},
13
+ {"name": "Cinnamon Puff", "rarity": "Common", "habitat": "pine needles", "lore": "It smells cozy, though Myco still recommends caution."},
14
+ {"name": "Oracle Cup", "rarity": "Legendary", "habitat": "ancient roots", "lore": "Dew pools inside it in patterns that almost look like maps."},
15
+ {"name": "Tiny Crown", "rarity": "Rare", "habitat": "stump kingdoms", "lore": "Several beetles were clearly holding a meeting around it."},
16
+ {"name": "Marshmallow Shelf", "rarity": "Common", "habitat": "willow trunks", "lore": "It looks pillowy, but has the firmness of polished cork."},
17
+ {"name": "Ghost Gill", "rarity": "Legendary", "habitat": "fog pockets", "lore": "Its gills vanish unless you look from the corner of your eye."},
18
+ {"name": "Sunfreckle", "rarity": "Common", "habitat": "sunny meadows", "lore": "Golden dots move across the cap as the afternoon passes."},
19
+ {"name": "Acorn Bonnet", "rarity": "Common", "habitat": "oak roots", "lore": "Squirrels keep hiding acorns underneath its cap."},
20
+ {"name": "Glass Stem", "rarity": "Legendary", "habitat": "crystal springs", "lore": "Its stem is clear enough to see bubbles rising through it."},
21
+ {"name": "Pepper Pixie", "rarity": "Rare", "habitat": "bramble edges", "lore": "A sneezy little mushroom that dusts the air with silver specks."},
22
+ {"name": "Dream Oysterlace", "rarity": "Legendary", "habitat": "sleepy groves", "lore": "Standing nearby makes the forest sound softer and farther away."},
23
+ {"name": "Moss Muffin", "rarity": "Common", "habitat": "deep moss beds", "lore": "It grows in plump clusters that look like a tiny bakery window."},
24
+ {"name": "Wisp Ear", "rarity": "Rare", "habitat": "lantern groves", "lore": "It tilts toward whispers and away from loud bootsteps."},
25
+ {"name": "Ruby Knuckle", "rarity": "Legendary", "habitat": "red clay banks", "lore": "Its red bumps are shiny, knobbly, and oddly cheerful."},
26
+ {"name": "Snowcap", "rarity": "Rare", "habitat": "cold hilltops", "lore": "A cool powder rests on top even in warm weather."},
27
+ {"name": "Clockwork Chanterelle", "rarity": "Legendary", "habitat": "abandoned paths", "lore": "Its ridges tick softly, though nobody knows what time it keeps."},
28
+ {"name": "Lavender Lantern", "rarity": "Rare", "habitat": "twilight gardens", "lore": "A calm purple glow gathers beneath the cap at sunset."},
29
+ {"name": "Pebblecap", "rarity": "Common", "habitat": "gravel paths", "lore": "It disguises itself as a stone until the forest gets quiet."},
30
+ {"name": "Tea Saucer", "rarity": "Common", "habitat": "cottage ruins", "lore": "Rainwater in its cap smells faintly of chamomile."},
31
+ {"name": "Aurora Fan", "rarity": "Legendary", "habitat": "northern pines", "lore": "Its fan-shaped cap shimmers green and pink when clouds move."}
32
+ ]
game/__init__.py ADDED
File without changes
game/catalog.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mushroom catalog loading for Myco."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ from models.mushroom import Mushroom
10
+
11
+ CATALOG_PATH = Path(__file__).resolve().parents[1] / "data" / "mushrooms.json"
12
+
13
+
14
+ @lru_cache(maxsize=1)
15
+ def load_mushrooms() -> tuple[Mushroom, ...]:
16
+ """Load the in-repo mushroom catalog."""
17
+ raw_mushrooms = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
18
+ return tuple(Mushroom.from_dict(item) for item in raw_mushrooms)
game/engine.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core Myco gameplay actions independent from Gradio."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from collections.abc import Sequence
7
+
8
+ from game.catalog import load_mushrooms
9
+ from game.state import (
10
+ ChatHistory,
11
+ CollectionState,
12
+ MushroomState,
13
+ collection_contains,
14
+ mushroom_from_state,
15
+ welcome_history,
16
+ )
17
+ from models.mushroom import Mushroom
18
+
19
+
20
+ def choose_mushroom(catalog: Sequence[Mushroom] | None = None) -> Mushroom:
21
+ """Choose a mushroom discovery from the catalog."""
22
+ mushrooms = tuple(catalog or load_mushrooms())
23
+ return random.choice(mushrooms)
24
+
25
+
26
+ def discover_mushroom(collection: CollectionState, catalog: Sequence[Mushroom] | None = None) -> tuple[Mushroom, MushroomState, ChatHistory]:
27
+ """Discover a mushroom and return the updated discovery state."""
28
+ mushroom = choose_mushroom(catalog)
29
+ current = mushroom.to_dict()
30
+ history = welcome_history()
31
+ history.append(
32
+ {
33
+ "role": "assistant",
34
+ "content": f"We found a {mushroom.name}! Ask me about it, or add it to your MycoDex.",
35
+ }
36
+ )
37
+ return mushroom, current, history
38
+
39
+
40
+ def myco_reply(message: str, history: ChatHistory, current: MushroomState | None) -> tuple[str, ChatHistory]:
41
+ """Generate a lightweight companion reply without requiring an external model."""
42
+ clean_message = message.strip()
43
+ if not clean_message:
44
+ return "", history
45
+
46
+ next_history = [*history, {"role": "user", "content": clean_message}]
47
+
48
+ if current:
49
+ mushroom = mushroom_from_state(current)
50
+ answer = (
51
+ f"I think this is **{mushroom.name}**. {mushroom.lore} "
52
+ "The MycoDex still marks edible, magic, and danger as unknown, "
53
+ "so let's observe it gently instead of tasting anything."
54
+ )
55
+ else:
56
+ answer = "Let's explore first. The forest usually reveals a mushroom when we move slowly and listen."
57
+
58
+ next_history.append({"role": "assistant", "content": answer})
59
+ return "", next_history
60
+
61
+
62
+ def collect_current(
63
+ current: MushroomState | None,
64
+ collection: CollectionState,
65
+ history: ChatHistory,
66
+ ) -> tuple[CollectionState, ChatHistory]:
67
+ """Add the current mushroom to the MycoDex if it is not already collected."""
68
+ if current is None:
69
+ return collection, [*history, {"role": "assistant", "content": "Let's discover a mushroom before updating the MycoDex."}]
70
+
71
+ if collection_contains(collection, current["name"]):
72
+ return collection, [*history, {"role": "assistant", "content": f"{current['name']} is already safe in your MycoDex."}]
73
+
74
+ updated_collection = [*collection, current]
75
+ return updated_collection, [*history, {"role": "assistant", "content": f"Added {current['name']} to the MycoDex. Nice find!"}]
game/state.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """State helpers for Myco's first playable loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from models.mushroom import Mushroom
6
+
7
+ WELCOME_MESSAGE = "Hello! I'm Myco. Let's explore the forest."
8
+ EMPTY_DEX = "Your MycoDex is empty. Discover a mushroom, then add it here."
9
+
10
+ ChatHistory = list[dict[str, str]]
11
+ MushroomState = dict[str, str]
12
+ CollectionState = list[MushroomState]
13
+
14
+
15
+ def welcome_history() -> ChatHistory:
16
+ """Return the opening Myco companion message."""
17
+ return [{"role": "assistant", "content": WELCOME_MESSAGE}]
18
+
19
+
20
+ def mushroom_from_state(current: MushroomState) -> Mushroom:
21
+ """Hydrate a mushroom from Gradio state."""
22
+ return Mushroom.from_dict(current)
23
+
24
+
25
+ def collection_contains(collection: CollectionState, mushroom_name: str) -> bool:
26
+ """Return whether the MycoDex already has a mushroom by name."""
27
+ return any(entry["name"] == mushroom_name for entry in collection)
models/__init__.py ADDED
File without changes
models/mushroom.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Domain models for Myco discoveries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Mushroom:
11
+ """A discoverable mushroom species for the MycoDex."""
12
+
13
+ name: str
14
+ rarity: str
15
+ habitat: str
16
+ lore: str
17
+ edible: str = "Unknown"
18
+ magic: str = "Unknown"
19
+ danger: str = "Unknown"
20
+
21
+ @classmethod
22
+ def from_dict(cls, data: dict[str, Any]) -> "Mushroom":
23
+ """Create a mushroom from JSON-compatible data."""
24
+ return cls(
25
+ name=str(data["name"]),
26
+ rarity=str(data["rarity"]),
27
+ habitat=str(data["habitat"]),
28
+ lore=str(data["lore"]),
29
+ edible=str(data.get("edible", "Unknown")),
30
+ magic=str(data.get("magic", "Unknown")),
31
+ danger=str(data.get("danger", "Unknown")),
32
+ )
33
+
34
+ def to_dict(self) -> dict[str, str]:
35
+ """Return a JSON/state-friendly representation."""
36
+ return {
37
+ "name": self.name,
38
+ "rarity": self.rarity,
39
+ "habitat": self.habitat,
40
+ "lore": self.lore,
41
+ "edible": self.edible,
42
+ "magic": self.magic,
43
+ "danger": self.danger,
44
+ }
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio>=5.0,<6.0
tests/test_game_loop.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Myco's core game loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+
7
+ from game.catalog import load_mushrooms
8
+ from game.engine import collect_current, discover_mushroom, myco_reply
9
+ from game.state import EMPTY_DEX, welcome_history
10
+ from models.mushroom import Mushroom
11
+ from ui.renderers import dex_markdown, mushroom_card
12
+
13
+
14
+ class MycoGameLoopTests(unittest.TestCase):
15
+ """Coverage for the first playable Myco loop."""
16
+
17
+ def test_catalog_has_mvp_scale_and_rarities(self) -> None:
18
+ mushrooms = load_mushrooms()
19
+
20
+ self.assertEqual(len(mushrooms), 30)
21
+ self.assertEqual({mushroom.rarity for mushroom in mushrooms}, {"Common", "Rare", "Legendary"})
22
+
23
+ def test_discover_mushroom_returns_current_state_and_myco_prompt(self) -> None:
24
+ glowcap = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.")
25
+
26
+ mushroom, current, history = discover_mushroom([], catalog=[glowcap])
27
+
28
+ self.assertEqual(mushroom, glowcap)
29
+ self.assertEqual(current["name"], "Glowcap")
30
+ self.assertEqual(history[0], {"role": "assistant", "content": "Hello! I'm Myco. Let's explore the forest."})
31
+ self.assertIn("We found a Glowcap", history[1]["content"])
32
+
33
+ def test_myco_reply_comments_on_current_mushroom(self) -> None:
34
+ current = Mushroom(
35
+ "Glowcap",
36
+ "Common",
37
+ "mossy logs",
38
+ "I think this one glows at night, but I'm not completely sure.",
39
+ ).to_dict()
40
+
41
+ prompt, history = myco_reply("What is it?", welcome_history(), current)
42
+
43
+ self.assertEqual(prompt, "")
44
+ self.assertEqual(history[-1]["role"], "assistant")
45
+ self.assertIn("I think this is **Glowcap**", history[-1]["content"])
46
+ self.assertIn("not completely sure", history[-1]["content"])
47
+
48
+ def test_collect_current_adds_once(self) -> None:
49
+ current = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.").to_dict()
50
+
51
+ collection, history = collect_current(current, [], welcome_history())
52
+ second_collection, second_history = collect_current(current, collection, history)
53
+
54
+ self.assertEqual([entry["name"] for entry in collection], ["Glowcap"])
55
+ self.assertEqual(second_collection, collection)
56
+ self.assertIn("already safe", second_history[-1]["content"])
57
+
58
+ def test_renderers_show_unknown_traits_and_empty_dex(self) -> None:
59
+ mushroom = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.")
60
+
61
+ card = mushroom_card(mushroom)
62
+
63
+ self.assertIn("**Edible:** Unknown", card)
64
+ self.assertIn("**Magic:** Unknown", card)
65
+ self.assertIn("**Danger:** Unknown", card)
66
+ self.assertEqual(dex_markdown([]), EMPTY_DEX)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ unittest.main()
ui/__init__.py ADDED
File without changes
ui/gradio_app.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio app composition for Myco."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gradio as gr
6
+
7
+ from game.engine import collect_current, discover_mushroom, myco_reply
8
+ from game.state import CollectionState, MushroomState, welcome_history
9
+ from ui.renderers import dex_markdown, mushroom_card
10
+
11
+
12
+ def explore(collection: CollectionState) -> tuple[str, MushroomState, list[dict[str, str]], str]:
13
+ """Gradio callback for the Explore button."""
14
+ mushroom, current, history = discover_mushroom(collection)
15
+ return mushroom_card(mushroom), current, history, dex_markdown(collection)
16
+
17
+
18
+ def ask_myco(
19
+ message: str,
20
+ history: list[dict[str, str]],
21
+ current: MushroomState | None,
22
+ ) -> tuple[str, list[dict[str, str]]]:
23
+ """Gradio callback for the companion chat panel."""
24
+ return myco_reply(message, history, current)
25
+
26
+
27
+ def add_to_dex(
28
+ current: MushroomState | None,
29
+ collection: CollectionState,
30
+ history: list[dict[str, str]],
31
+ ) -> tuple[CollectionState, str, list[dict[str, str]]]:
32
+ """Gradio callback for collecting the current mushroom."""
33
+ updated_collection, updated_history = collect_current(current, collection, history)
34
+ return updated_collection, dex_markdown(updated_collection), updated_history
35
+
36
+
37
+ def build_app() -> gr.Blocks:
38
+ """Build the Gradio interface for Hugging Face Spaces."""
39
+ theme = gr.themes.Soft(primary_hue="green", secondary_hue="emerald")
40
+ with gr.Blocks(theme=theme, title="Myco") as demo:
41
+ current_mushroom = gr.State(None)
42
+ collection = gr.State([])
43
+
44
+ gr.Markdown(
45
+ """
46
+ # πŸ„ Myco
47
+ **Explore a magical forest with Myco, an AI mushroom companion. Discover edible, magical, dangerous, and legendary fungi while completing your MycoDex.**
48
+ """
49
+ )
50
+
51
+ with gr.Row():
52
+ with gr.Column(scale=2):
53
+ discovery = gr.Markdown(mushroom_card(None), elem_id="discovery-card")
54
+ with gr.Row():
55
+ explore_button = gr.Button("Explore", variant="primary")
56
+ add_button = gr.Button("Add to MycoDex")
57
+ with gr.Column(scale=3):
58
+ chat = gr.Chatbot(value=welcome_history(), label="Myco Chat", type="messages", height=360)
59
+ with gr.Row():
60
+ prompt = gr.Textbox(
61
+ label="Ask Myco",
62
+ placeholder="What do you know about this mushroom?",
63
+ scale=4,
64
+ )
65
+ ask_button = gr.Button("Ask Myco", scale=1)
66
+
67
+ dex = gr.Markdown(dex_markdown([]), elem_id="mycodex")
68
+
69
+ explore_button.click(
70
+ explore,
71
+ inputs=[collection],
72
+ outputs=[discovery, current_mushroom, chat, dex],
73
+ )
74
+ ask_button.click(ask_myco, inputs=[prompt, chat, current_mushroom], outputs=[prompt, chat])
75
+ prompt.submit(ask_myco, inputs=[prompt, chat, current_mushroom], outputs=[prompt, chat])
76
+ add_button.click(add_to_dex, inputs=[current_mushroom, collection, chat], outputs=[collection, dex, chat])
77
+
78
+ return demo
ui/renderers.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Markdown renderers for the Myco Gradio interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from game.state import CollectionState, EMPTY_DEX
6
+ from models.mushroom import Mushroom
7
+
8
+
9
+ def mushroom_card(mushroom: Mushroom | None) -> str:
10
+ """Render the current mushroom discovery as markdown."""
11
+ if mushroom is None:
12
+ return "### 🌲 Forest Clearing\nClick **Explore** to search for a strange fungus."
13
+
14
+ return (
15
+ f"### πŸ„ {mushroom.name}\n"
16
+ f"* **Rarity:** {mushroom.rarity}\n"
17
+ f"* **Habitat:** {mushroom.habitat}\n"
18
+ f"* **Edible:** {mushroom.edible}\n"
19
+ f"* **Magic:** {mushroom.magic}\n"
20
+ f"* **Danger:** {mushroom.danger}\n"
21
+ )
22
+
23
+
24
+ def dex_markdown(collection: CollectionState) -> str:
25
+ """Render collected mushrooms as a compact MycoDex."""
26
+ if not collection:
27
+ return EMPTY_DEX
28
+
29
+ rows = ["### πŸ“– MycoDex", ""]
30
+ for entry in collection:
31
+ rows.append(f"* πŸ„ **{entry['name']}** β€” {entry['rarity']} β€” found near {entry['habitat']}")
32
+ return "\n".join(rows)