Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .github/workflows/sync_to_hf.yml +42 -0
- .gitignore +54 -0
- CODEx_USAGE.md +16 -0
- LICENSE +21 -0
- README.md +82 -15
- app.py +9 -0
- assets/images/.gitkeep +0 -0
- assets/sounds/.gitkeep +0 -0
- data/.gitkeep +0 -0
- data/mushrooms.json +32 -0
- game/__init__.py +0 -0
- game/catalog.py +18 -0
- game/engine.py +75 -0
- game/state.py +27 -0
- models/__init__.py +0 -0
- models/mushroom.py +44 -0
- requirements.txt +1 -0
- tests/test_game_loop.py +70 -0
- ui/__init__.py +0 -0
- ui/gradio_app.py +78 -0
- ui/renderers.py +32 -0
.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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|