Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .github/workflows/deploy-space.yml +63 -0
- .github/workflows/sync_to_hf.yml +1 -1
- CODEx_USAGE.md +28 -0
- README.md +51 -0
- app.py +5 -0
- game/engine.py +45 -0
- game/progression.py +40 -0
- scripts/check_hf_token.py +32 -0
- scripts/deploy_space.py +50 -0
- sitecustomize.py +15 -0
- tests/__init__.py +0 -0
- tests/test_game_loop.py +39 -0
- ui/gradio_app.py +60 -0
- ui/renderers.py +42 -0
- ui/styles.py +47 -0
.github/workflows/deploy-space.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy Hugging Face Space
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- main
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
# Put the repo on PYTHONPATH so Python imports sitecustomize.py for inline
|
| 11 |
+
# `python -c` validation steps as well as normal scripts.
|
| 12 |
+
PYTHONPATH: .
|
| 13 |
+
# If the GitHub secret is absent, this may be empty; sitecustomize.py and
|
| 14 |
+
# scripts/check_hf_token.py keep empty-token runs from failing with KeyError.
|
| 15 |
+
# Define this at workflow scope so any future inline `python -c` step can read
|
| 16 |
+
# os.environ["HF_BUILD_SMALL_HACKATHON_TOKEN"] without raising KeyError.
|
| 17 |
+
# If the GitHub secret is absent, the value is an empty string and deploy steps skip.
|
| 18 |
+
HF_BUILD_SMALL_HACKATHON_TOKEN: ${{ secrets.HF_BUILD_SMALL_HACKATHON_TOKEN }}
|
| 19 |
+
HF_SPACE_ID: byte-vortex/Myco
|
| 20 |
+
|
| 21 |
+
jobs:
|
| 22 |
+
deploy-space:
|
| 23 |
+
runs-on: ubuntu-latest
|
| 24 |
+
jobs:
|
| 25 |
+
deploy-space:
|
| 26 |
+
runs-on: ubuntu-latest
|
| 27 |
+
env:
|
| 28 |
+
HF_SPACE_ID: byte-vortex/Myco
|
| 29 |
+
steps:
|
| 30 |
+
- name: Check out repository
|
| 31 |
+
uses: actions/checkout@v4
|
| 32 |
+
|
| 33 |
+
- name: Set up Python
|
| 34 |
+
uses: actions/setup-python@v5
|
| 35 |
+
with:
|
| 36 |
+
python-version: "3.10"
|
| 37 |
+
|
| 38 |
+
- name: Install deployment dependency
|
| 39 |
+
run: python -m pip install "huggingface_hub>=0.33.5,<2.0"
|
| 40 |
+
|
| 41 |
+
- name: Normalize optional Hugging Face token env
|
| 42 |
+
run: echo "HF_BUILD_SMALL_HACKATHON_TOKEN=${HF_BUILD_SMALL_HACKATHON_TOKEN:-}" >> "$GITHUB_ENV"
|
| 43 |
+
|
| 44 |
+
- name: Check Hugging Face token
|
| 45 |
+
id: hf_token
|
| 46 |
+
- name: Check Hugging Face token
|
| 47 |
+
id: hf_token
|
| 48 |
+
env:
|
| 49 |
+
HF_BUILD_SMALL_HACKATHON_TOKEN: ${{ secrets.HF_BUILD_SMALL_HACKATHON_TOKEN }}
|
| 50 |
+
run: python scripts/check_hf_token.py
|
| 51 |
+
|
| 52 |
+
- name: Deploy Space
|
| 53 |
+
if: steps.hf_token.outputs.available == 'true'
|
| 54 |
+
env:
|
| 55 |
+
HF_BUILD_SMALL_HACKATHON_TOKEN: ${{ secrets.HF_BUILD_SMALL_HACKATHON_TOKEN }}
|
| 56 |
+
HF_SPACE_ID: byte-vortex/Myco
|
| 57 |
+
run: python scripts/deploy_space.py
|
| 58 |
+
|
| 59 |
+
- name: Skip deployment notice
|
| 60 |
+
if: steps.hf_token.outputs.available != 'true'
|
| 61 |
+
run: |
|
| 62 |
+
echo "HF_BUILD_SMALL_HACKATHON_TOKEN is not configured; skipping Space deploy."
|
| 63 |
+
echo "Add it in GitHub repository Settings β Secrets and variables β Actions."
|
.github/workflows/sync_to_hf.yml
CHANGED
|
@@ -24,7 +24,7 @@ jobs:
|
|
| 24 |
|
| 25 |
- name: Upload project files to HF Space
|
| 26 |
env:
|
| 27 |
-
HF_TOKEN: ${{ secrets.
|
| 28 |
run: |
|
| 29 |
python -c "
|
| 30 |
import os
|
|
|
|
| 24 |
|
| 25 |
- name: Upload project files to HF Space
|
| 26 |
env:
|
| 27 |
+
HF_TOKEN: ${{ secrets.HF_BUILD_SMALL_HACKATHON_TOKEN }}
|
| 28 |
run: |
|
| 29 |
python -c "
|
| 30 |
import os
|
CODEx_USAGE.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 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
|
|
@@ -10,6 +13,31 @@ This repository intentionally documents how Codex contributed to the project so
|
|
| 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 |
|
|
|
|
| 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. The goal is to show a real development trail with Codex-attributed commits, not to maximize token spend, model size, or architectural complexity.
|
| 4 |
+
|
| 5 |
+
## What Codex Helped Build
|
| 6 |
This repository intentionally documents how Codex contributed to the project so the Codex track can evaluate both the product quality and the development process.
|
| 7 |
|
| 8 |
## Codex-Assisted Milestones
|
|
|
|
| 13 |
- Refactored the app into clearer `app.py`, `game/`, `models/`, `ui/`, `data/`, and `tests/` sections.
|
| 14 |
- Added an in-repo mushroom catalog with Common, Rare, and Legendary discoveries.
|
| 15 |
- Added tests for the catalog, discovery loop, companion replies, collection tracking, and markdown renderers.
|
| 16 |
+
- Polished the first impression with a forest hero panel, Myco mascot, custom styling, action buttons, and forest progression goals.
|
| 17 |
+
- Added submission-oriented documentation so the Hugging Face Space README can point judges back to the public GitHub repository.
|
| 18 |
+
|
| 19 |
+
## Codex-Visible Development Trail
|
| 20 |
+
|
| 21 |
+
The repository is intentionally split into meaningful changes that a judge can understand from the Git history, such as:
|
| 22 |
+
|
| 23 |
+
1. Initialize the project and supporting files.
|
| 24 |
+
2. Add the Gradio app entrypoint and dependencies.
|
| 25 |
+
3. Create the mushroom catalog and game engine.
|
| 26 |
+
4. Implement Myco companion dialogue and MycoDex collection tracking.
|
| 27 |
+
5. Refactor into `game/`, `models/`, `ui/`, `data/`, and `tests/`.
|
| 28 |
+
6. Add tests and submission documentation.
|
| 29 |
+
|
| 30 |
+
## Design Goal
|
| 31 |
+
|
| 32 |
+
Myco is built to be a polished, small-scope project rather than a sprawling prototype: start exploring, discover a mushroom, ask Myco, choose whether to eat, study, or collect, and make progress through the MycoDex.
|
| 33 |
+
|
| 34 |
+
## PR Workflow Note
|
| 35 |
+
|
| 36 |
+
Codex currently cannot update a pull request after that PR has been changed outside of Codex. If that happens, continue the work in Codex and open a fresh pull request rather than trying to reuse the externally modified PR.
|
| 37 |
+
|
| 38 |
+
## Submission Reminder
|
| 39 |
+
|
| 40 |
+
Before submitting the Hugging Face Space, publish the repository, replace the placeholder GitHub URL in `README.md` with the final public repo link, and confirm the live Space is linked at https://huggingface.co/spaces/byte-vortex/Myco.
|
| 41 |
|
| 42 |
## Design Goal
|
| 43 |
|
README.md
CHANGED
|
@@ -20,6 +20,26 @@ Myco is a cozy exploration game prototype for Gradio + Hugging Face Spaces. Myco
|
|
| 20 |
|
| 21 |
1. Open the game.
|
| 22 |
2. Meet Myco: "Hello! I'm Myco. Let's explore the forest."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
3. Click **Explore**.
|
| 24 |
4. Discover a mushroom, such as **Glowcap**.
|
| 25 |
5. Ask Myco about it.
|
|
@@ -55,6 +75,7 @@ hf-gradio-small-hackathon-myco/
|
|
| 55 |
βββ README.md
|
| 56 |
```
|
| 57 |
|
|
|
|
| 58 |
`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 happen in `game/`.
|
| 59 |
|
| 60 |
## Local Development
|
|
@@ -70,6 +91,12 @@ Then open the local Gradio URL printed in the terminal.
|
|
| 70 |
|
| 71 |
## Hugging Face Space
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
Create a Gradio Space and include these files at the repository root:
|
| 74 |
|
| 75 |
```text
|
|
@@ -81,6 +108,15 @@ requirements.txt
|
|
| 81 |
|
| 82 |
Hugging Face Spaces will install `requirements.txt` and run `app.py`.
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
## Future Ideas
|
| 85 |
|
| 86 |
- Mushroom rarity progression
|
|
@@ -92,6 +128,21 @@ Hugging Face Spaces will install `requirements.txt` and run `app.py`.
|
|
| 92 |
|
| 93 |
## Codex Track Notes
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
See [`CODEx_USAGE.md`](CODEx_USAGE.md) for a concise record of how Codex helped shape the app architecture, gameplay loop, refactor, and tests.
|
| 96 |
|
| 97 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 20 |
|
| 21 |
1. Open the game.
|
| 22 |
2. Meet Myco: "Hello! I'm Myco. Let's explore the forest."
|
| 23 |
+
3. Click **Start Exploring**.
|
| 24 |
+
4. Discover a mushroom, such as **Glowcap**.
|
| 25 |
+
5. Ask Myco about it.
|
| 26 |
+
6. Decide whether to **Eat**, **Collect**, or **Study**.
|
| 27 |
+
7. Add it to the **MycoDex** and watch forest goals progress.
|
| 28 |
+
|
| 29 |
+
## Hackathon MVP
|
| 30 |
+
|
| 31 |
+
- Polished forest-themed hero panel with Myco mascot
|
| 32 |
+
- Start Exploring button
|
| 33 |
+
- Mushroom discovery
|
| 34 |
+
- Myco chat panel
|
| 35 |
+
- Eat, Collect, and Study choices
|
| 36 |
+
- MycoDex collection
|
| 37 |
+
- Forest progression goals for Crystal Grove, Moonlit Cavern, and Mushroom Elder
|
| 38 |
+
- 30 starter mushroom species
|
| 39 |
+
- Common, Rare, and Legendary rarity tiers
|
| 40 |
+
|
| 41 |
+
The MVP intentionally skips combat, multiplayer, and a huge map so the first playable loop can feel polished instead of oversized.
|
| 42 |
+
|
| 43 |
3. Click **Explore**.
|
| 44 |
4. Discover a mushroom, such as **Glowcap**.
|
| 45 |
5. Ask Myco about it.
|
|
|
|
| 75 |
βββ README.md
|
| 76 |
```
|
| 77 |
|
| 78 |
+
`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/`.
|
| 79 |
`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 happen in `game/`.
|
| 80 |
|
| 81 |
## Local Development
|
|
|
|
| 91 |
|
| 92 |
## Hugging Face Space
|
| 93 |
|
| 94 |
+
**Live Space:** https://huggingface.co/spaces/byte-vortex/Myco
|
| 95 |
+
|
| 96 |
+
**GitHub repository:** https://github.com/YOUR_USERNAME/hf-gradio-small-hackathon-myco
|
| 97 |
+
|
| 98 |
+
Replace `YOUR_USERNAME` with the public GitHub owner before submitting the Space so judges can inspect the Codex-attributed commit history.
|
| 99 |
+
|
| 100 |
Create a Gradio Space and include these files at the repository root:
|
| 101 |
|
| 102 |
```text
|
|
|
|
| 108 |
|
| 109 |
Hugging Face Spaces will install `requirements.txt` and run `app.py`.
|
| 110 |
|
| 111 |
+
### GitHub Actions Deployment Token
|
| 112 |
+
|
| 113 |
+
If you use the included deploy workflow, add `HF_BUILD_SMALL_HACKATHON_TOKEN` as a GitHub Actions secret with write access to the Space. The workflow sets `PYTHONPATH=.` so inline Python checks import `sitecustomize.py`, normalizes an empty `HF_BUILD_SMALL_HACKATHON_TOKEN` into `$GITHUB_ENV`, and skips deployment with a warning when the token is empty or missing. This prevents `os.environ["HF_BUILD_SMALL_HACKATHON_TOKEN"]` checks from failing with `KeyError`.
|
| 114 |
+
If you use the included deploy workflow, add `HF_BUILD_SMALL_HACKATHON_TOKEN` as a GitHub Actions secret with write access to the Space. The workflow defines `HF_BUILD_SMALL_HACKATHON_TOKEN` at workflow scope, so even future inline `python -c` checks can read `os.environ["HF_BUILD_SMALL_HACKATHON_TOKEN"]` without raising `KeyError`; when the secret is empty or missing, deployment is skipped with a warning.
|
| 115 |
+
If you use the included deploy workflow, add `HF_BUILD_SMALL_HACKATHON_TOKEN` as a GitHub Actions secret with write access to the Space. The workflow checks for the secret with `scripts/check_hf_token.py`; when the secret is missing, deployment is skipped with a warning instead of failing with `KeyError: 'HF_BUILD_SMALL_HACKATHON_TOKEN'`.
|
| 116 |
+
|
| 117 |
+
## Future Ideas
|
| 118 |
+
|
| 119 |
+
- More animated Myco mascot states
|
| 120 |
## Future Ideas
|
| 121 |
|
| 122 |
- Mushroom rarity progression
|
|
|
|
| 128 |
|
| 129 |
## Codex Track Notes
|
| 130 |
|
| 131 |
+
This project is optimized for a finished, charming build with clear evidence of Codex-assisted developmentβnot for spending the most tokens, using the largest model, or adding unnecessary complexity.
|
| 132 |
+
|
| 133 |
+
See [`CODEx_USAGE.md`](CODEx_USAGE.md) for a concise record of how Codex helped shape the app architecture, gameplay loop, refactor, and tests.
|
| 134 |
+
|
| 135 |
+
### Codex PR Workflow
|
| 136 |
+
|
| 137 |
+
If a pull request is changed outside of Codex, create a new Codex PR for follow-up work. This keeps the Codex-attributed development trail clear and avoids trying to update a PR Codex can no longer manage.
|
| 138 |
+
|
| 139 |
+
### Submission Checklist
|
| 140 |
+
|
| 141 |
+
- [ ] Publish this repository on GitHub.
|
| 142 |
+
- [ ] Replace the placeholder GitHub repository link above with the public repo URL.
|
| 143 |
+
- [ ] Confirm the public repo history includes Codex-attributed commits from the Codex workflow.
|
| 144 |
+
- [ ] Confirm the live Space link works: https://huggingface.co/spaces/byte-vortex/Myco
|
| 145 |
+
- [ ] Deploy the Space and verify the first playable loop: Start Exploring β Ask Myco β Study or Collect β MycoDex progress.
|
| 146 |
See [`CODEx_USAGE.md`](CODEx_USAGE.md) for a concise record of how Codex helped shape the app architecture, gameplay loop, refactor, and tests.
|
| 147 |
|
| 148 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 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()
|
|
|
|
| 1 |
"""Hugging Face Spaces entrypoint for Myco."""
|
| 2 |
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from ui.gradio_app import build_app
|
| 6 |
+
from ui.styles import APP_CSS
|
| 7 |
from ui.gradio_app import build_app
|
| 8 |
|
| 9 |
|
| 10 |
demo = build_app()
|
| 11 |
|
| 12 |
if __name__ == "__main__":
|
| 13 |
+
demo.launch(theme=gr.themes.Soft(primary_hue="green", secondary_hue="emerald"), css=APP_CSS)
|
| 14 |
demo.launch()
|
game/engine.py
CHANGED
|
@@ -26,11 +26,16 @@ def choose_mushroom(catalog: Sequence[Mushroom] | None = None) -> Mushroom:
|
|
| 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 |
)
|
|
@@ -73,3 +78,43 @@ def collect_current(
|
|
| 73 |
|
| 74 |
updated_collection = [*collection, current]
|
| 75 |
return updated_collection, [*history, {"role": "assistant", "content": f"Added {current['name']} to the MycoDex. Nice find!"}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
discovery_count = len(collection)
|
| 30 |
current = mushroom.to_dict()
|
| 31 |
history = welcome_history()
|
| 32 |
history.append(
|
| 33 |
{
|
| 34 |
"role": "assistant",
|
| 35 |
+
"content": (
|
| 36 |
+
f"We found a {mushroom.name}! Ask me about it, then decide whether to "
|
| 37 |
+
f"eat, collect, or study it. Your MycoDex has {discovery_count} entries."
|
| 38 |
+
),
|
| 39 |
"content": f"We found a {mushroom.name}! Ask me about it, or add it to your MycoDex.",
|
| 40 |
}
|
| 41 |
)
|
|
|
|
| 78 |
|
| 79 |
updated_collection = [*collection, current]
|
| 80 |
return updated_collection, [*history, {"role": "assistant", "content": f"Added {current['name']} to the MycoDex. Nice find!"}]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def study_current(current: MushroomState | None, history: ChatHistory) -> tuple[MushroomState | None, ChatHistory]:
|
| 84 |
+
"""Study the current mushroom to reveal one cautious field observation."""
|
| 85 |
+
if current is None:
|
| 86 |
+
return None, [*history, {"role": "assistant", "content": "Let's find a mushroom before we study anything."}]
|
| 87 |
+
|
| 88 |
+
studied = {**current, "magic": _study_hint(current["rarity"]), "studied": "Yes"}
|
| 89 |
+
message = (
|
| 90 |
+
f"I studied the {current['name']} closely. New MycoDex note: "
|
| 91 |
+
f"magic is now marked as **{studied['magic']}**. Still no tasting without certainty!"
|
| 92 |
+
)
|
| 93 |
+
return studied, [*history, {"role": "assistant", "content": message}]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def eat_current(current: MushroomState | None, history: ChatHistory) -> ChatHistory:
|
| 97 |
+
"""Handle the Eat choice safely with Myco's companion personality."""
|
| 98 |
+
if current is None:
|
| 99 |
+
return [*history, {"role": "assistant", "content": "Let's discover a mushroom before making snack decisions."}]
|
| 100 |
+
|
| 101 |
+
mushroom = mushroom_from_state(current)
|
| 102 |
+
return [
|
| 103 |
+
*history,
|
| 104 |
+
{
|
| 105 |
+
"role": "assistant",
|
| 106 |
+
"content": (
|
| 107 |
+
f"Myco gently blocks the {mushroom.name}. Its edible field is still **{mushroom.edible}**, "
|
| 108 |
+
"so the winning move is to study or collect it instead of eating it."
|
| 109 |
+
),
|
| 110 |
+
},
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _study_hint(rarity: str) -> str:
|
| 115 |
+
"""Return a premium-feeling but cautious study result by rarity."""
|
| 116 |
+
if rarity == "Legendary":
|
| 117 |
+
return "Strong, mysterious aura"
|
| 118 |
+
if rarity == "Rare":
|
| 119 |
+
return "Faint magical trace"
|
| 120 |
+
return "Tiny spore shimmer"
|
game/progression.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Progression goals for Myco's collectible forest loop."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass(frozen=True)
|
| 9 |
+
class AreaUnlock:
|
| 10 |
+
"""A forest area unlocked by MycoDex progress."""
|
| 11 |
+
|
| 12 |
+
name: str
|
| 13 |
+
required_discoveries: int
|
| 14 |
+
description: str
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
AREA_UNLOCKS: tuple[AreaUnlock, ...] = (
|
| 18 |
+
AreaUnlock("Mossy Trail", 0, "Myco's favorite beginner path, full of soft moss and gentle spores."),
|
| 19 |
+
AreaUnlock("Crystal Grove", 10, "A glittering grove where rare caps grow beside singing stones."),
|
| 20 |
+
AreaUnlock("Moonlit Cavern", 25, "A blue-lit cave where legendary fungi bloom after midnight."),
|
| 21 |
+
AreaUnlock("Mushroom Elder", 50, "The hidden elder of the forest, waiting for a true MycoDex keeper."),
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def unlocked_areas(discovery_count: int) -> tuple[AreaUnlock, ...]:
|
| 26 |
+
"""Return every area unlocked by the current MycoDex count."""
|
| 27 |
+
return tuple(area for area in AREA_UNLOCKS if discovery_count >= area.required_discoveries)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def current_area(discovery_count: int) -> AreaUnlock:
|
| 31 |
+
"""Return the highest unlocked area for a discovery count."""
|
| 32 |
+
return unlocked_areas(discovery_count)[-1]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def next_area(discovery_count: int) -> AreaUnlock | None:
|
| 36 |
+
"""Return the next locked area, if any."""
|
| 37 |
+
for area in AREA_UNLOCKS:
|
| 38 |
+
if discovery_count < area.required_discoveries:
|
| 39 |
+
return area
|
| 40 |
+
return None
|
scripts/check_hf_token.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check whether the Hugging Face deployment token is available."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
TOKEN_ENV_VAR = "HF_BUILD_SMALL_HACKATHON_TOKEN"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main() -> None:
|
| 12 |
+
"""Emit a GitHub Actions output instead of raising KeyError for a missing token."""
|
| 13 |
+
token = os.environ.get(TOKEN_ENV_VAR, "").strip()
|
| 14 |
+
available = "true" if token else "false"
|
| 15 |
+
|
| 16 |
+
github_output = os.environ.get("GITHUB_OUTPUT")
|
| 17 |
+
if github_output:
|
| 18 |
+
with Path(github_output).open("a", encoding="utf-8") as output_file:
|
| 19 |
+
output_file.write(f"available={available}\n")
|
| 20 |
+
|
| 21 |
+
if token:
|
| 22 |
+
print(f"{TOKEN_ENV_VAR} is configured; Hugging Face Space deployment can continue.")
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
print(
|
| 26 |
+
f"::warning::{TOKEN_ENV_VAR} is not configured. "
|
| 27 |
+
"Skipping Hugging Face Space deployment instead of failing with KeyError."
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
main()
|
scripts/deploy_space.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deploy Myco to the configured Hugging Face Space."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
TOKEN_ENV_VAR = "HF_BUILD_SMALL_HACKATHON_TOKEN"
|
| 9 |
+
DEFAULT_SPACE_ID = "byte-vortex/Myco"
|
| 10 |
+
IGNORE_PATTERNS = (
|
| 11 |
+
".git/*",
|
| 12 |
+
".github/*",
|
| 13 |
+
"__pycache__/*",
|
| 14 |
+
"**/__pycache__/*",
|
| 15 |
+
"*.pyc",
|
| 16 |
+
".gradio/*",
|
| 17 |
+
".cache/*",
|
| 18 |
+
"models/downloads/*",
|
| 19 |
+
"models/**/*.gguf",
|
| 20 |
+
"models/**/*.bin",
|
| 21 |
+
"models/**/*.safetensors",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main() -> None:
|
| 26 |
+
"""Upload the repository contents to the Hugging Face Space when a token is present."""
|
| 27 |
+
token = os.environ.get(TOKEN_ENV_VAR, "").strip()
|
| 28 |
+
if not token:
|
| 29 |
+
print(
|
| 30 |
+
f"::warning::{TOKEN_ENV_VAR} is not configured. "
|
| 31 |
+
"Skipping Hugging Face Space deployment."
|
| 32 |
+
)
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
from huggingface_hub import HfApi
|
| 36 |
+
|
| 37 |
+
repo_id = os.environ.get("HF_SPACE_ID", DEFAULT_SPACE_ID).strip() or DEFAULT_SPACE_ID
|
| 38 |
+
HfApi().upload_folder(
|
| 39 |
+
folder_path=".",
|
| 40 |
+
repo_id=repo_id,
|
| 41 |
+
repo_type="space",
|
| 42 |
+
token=token,
|
| 43 |
+
ignore_patterns=IGNORE_PATTERNS,
|
| 44 |
+
commit_message="Deploy Myco from GitHub Actions",
|
| 45 |
+
)
|
| 46 |
+
print(f"Deployed Myco to https://huggingface.co/spaces/{repo_id}")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
sitecustomize.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Repository-wide Python startup defaults for CI helpers.
|
| 2 |
+
|
| 3 |
+
Python imports ``sitecustomize`` automatically when this repository is on
|
| 4 |
+
``sys.path``. GitHub Actions and hackathon validation sometimes run inline
|
| 5 |
+
``python -c`` snippets from the repository root before project helper scripts
|
| 6 |
+
can execute. Provide a harmless default for optional deployment secrets so
|
| 7 |
+
``os.environ["HF_BUILD_SMALL_HACKATHON_TOKEN"]`` does not raise ``KeyError``
|
| 8 |
+
when the secret has not been configured.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
os.environ.setdefault("HF_BUILD_SMALL_HACKATHON_TOKEN", "")
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_game_loop.py
CHANGED
|
@@ -5,6 +5,11 @@ from __future__ import annotations
|
|
| 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
|
|
@@ -29,6 +34,7 @@ class MycoGameLoopTests(unittest.TestCase):
|
|
| 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(
|
|
@@ -55,6 +61,37 @@ class MycoGameLoopTests(unittest.TestCase):
|
|
| 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 |
|
|
@@ -63,6 +100,8 @@ class MycoGameLoopTests(unittest.TestCase):
|
|
| 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 |
|
|
|
|
| 5 |
import unittest
|
| 6 |
|
| 7 |
from game.catalog import load_mushrooms
|
| 8 |
+
from game.engine import collect_current, discover_mushroom, eat_current, myco_reply, study_current
|
| 9 |
+
from game.progression import current_area, next_area, unlocked_areas
|
| 10 |
+
from game.state import EMPTY_DEX, welcome_history
|
| 11 |
+
from models.mushroom import Mushroom
|
| 12 |
+
from ui.renderers import dex_markdown, mushroom_card, progress_markdown
|
| 13 |
from game.engine import collect_current, discover_mushroom, myco_reply
|
| 14 |
from game.state import EMPTY_DEX, welcome_history
|
| 15 |
from models.mushroom import Mushroom
|
|
|
|
| 34 |
self.assertEqual(current["name"], "Glowcap")
|
| 35 |
self.assertEqual(history[0], {"role": "assistant", "content": "Hello! I'm Myco. Let's explore the forest."})
|
| 36 |
self.assertIn("We found a Glowcap", history[1]["content"])
|
| 37 |
+
self.assertIn("eat, collect, or study", history[1]["content"])
|
| 38 |
|
| 39 |
def test_myco_reply_comments_on_current_mushroom(self) -> None:
|
| 40 |
current = Mushroom(
|
|
|
|
| 61 |
self.assertEqual(second_collection, collection)
|
| 62 |
self.assertIn("already safe", second_history[-1]["content"])
|
| 63 |
|
| 64 |
+
def test_study_current_reveals_magic_hint(self) -> None:
|
| 65 |
+
current = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.").to_dict()
|
| 66 |
+
|
| 67 |
+
studied, history = study_current(current, welcome_history())
|
| 68 |
+
|
| 69 |
+
self.assertIsNotNone(studied)
|
| 70 |
+
assert studied is not None
|
| 71 |
+
self.assertEqual(studied["magic"], "Tiny spore shimmer")
|
| 72 |
+
self.assertEqual(studied["studied"], "Yes")
|
| 73 |
+
self.assertIn("New MycoDex note", history[-1]["content"])
|
| 74 |
+
|
| 75 |
+
def test_eat_current_keeps_unknown_mushrooms_safe(self) -> None:
|
| 76 |
+
current = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.").to_dict()
|
| 77 |
+
|
| 78 |
+
history = eat_current(current, welcome_history())
|
| 79 |
+
|
| 80 |
+
self.assertIn("gently blocks", history[-1]["content"])
|
| 81 |
+
self.assertIn("instead of eating", history[-1]["content"])
|
| 82 |
+
|
| 83 |
+
def test_progression_unlocks_areas_by_collection_count(self) -> None:
|
| 84 |
+
self.assertEqual(current_area(0).name, "Mossy Trail")
|
| 85 |
+
self.assertEqual(next_area(0).name, "Crystal Grove")
|
| 86 |
+
self.assertEqual(current_area(10).name, "Crystal Grove")
|
| 87 |
+
self.assertEqual(next_area(25).name, "Mushroom Elder")
|
| 88 |
+
self.assertEqual([area.name for area in unlocked_areas(25)], ["Mossy Trail", "Crystal Grove", "Moonlit Cavern"])
|
| 89 |
+
|
| 90 |
+
def test_renderers_show_unknown_traits_progress_and_empty_dex(self) -> None:
|
| 91 |
+
mushroom = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.")
|
| 92 |
+
|
| 93 |
+
card = mushroom_card(mushroom)
|
| 94 |
+
progress = progress_markdown([])
|
| 95 |
def test_renderers_show_unknown_traits_and_empty_dex(self) -> None:
|
| 96 |
mushroom = Mushroom("Glowcap", "Common", "mossy logs", "I think this one glows at night, but I'm not completely sure.")
|
| 97 |
|
|
|
|
| 100 |
self.assertIn("**Edible:** Unknown", card)
|
| 101 |
self.assertIn("**Magic:** Unknown", card)
|
| 102 |
self.assertIn("**Danger:** Unknown", card)
|
| 103 |
+
self.assertIn("Forest Progress", progress)
|
| 104 |
+
self.assertIn("Crystal Grove", progress)
|
| 105 |
self.assertEqual(dex_markdown([]), EMPTY_DEX)
|
| 106 |
|
| 107 |
|
ui/gradio_app.py
CHANGED
|
@@ -4,6 +4,16 @@ 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
|
|
@@ -28,6 +38,34 @@ 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)
|
|
@@ -41,6 +79,7 @@ def build_app() -> gr.Blocks:
|
|
| 41 |
current_mushroom = gr.State(None)
|
| 42 |
collection = gr.State([])
|
| 43 |
|
|
|
|
| 44 |
gr.Markdown(
|
| 45 |
"""
|
| 46 |
# π Myco
|
|
@@ -52,6 +91,11 @@ def build_app() -> gr.Blocks:
|
|
| 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):
|
|
@@ -59,16 +103,32 @@ def build_app() -> gr.Blocks:
|
|
| 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])
|
|
|
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
+
from game.engine import collect_current, discover_mushroom, eat_current, myco_reply, study_current
|
| 8 |
+
from game.state import CollectionState, MushroomState, welcome_history
|
| 9 |
+
from models.mushroom import Mushroom
|
| 10 |
+
from ui.renderers import dex_markdown, hero_markdown, mushroom_card, progress_markdown
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def explore(collection: CollectionState) -> tuple[str, MushroomState, list[dict[str, str]], str, str]:
|
| 14 |
+
"""Gradio callback for the Start Exploring button."""
|
| 15 |
+
mushroom, current, history = discover_mushroom(collection)
|
| 16 |
+
return mushroom_card(mushroom), current, history, dex_markdown(collection), progress_markdown(collection)
|
| 17 |
from game.engine import collect_current, discover_mushroom, myco_reply
|
| 18 |
from game.state import CollectionState, MushroomState, welcome_history
|
| 19 |
from ui.renderers import dex_markdown, mushroom_card
|
|
|
|
| 38 |
current: MushroomState | None,
|
| 39 |
collection: CollectionState,
|
| 40 |
history: list[dict[str, str]],
|
| 41 |
+
) -> tuple[CollectionState, str, list[dict[str, str]], str]:
|
| 42 |
+
"""Gradio callback for collecting the current mushroom."""
|
| 43 |
+
updated_collection, updated_history = collect_current(current, collection, history)
|
| 44 |
+
return updated_collection, dex_markdown(updated_collection), updated_history, progress_markdown(updated_collection)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def study_mushroom(
|
| 48 |
+
current: MushroomState | None,
|
| 49 |
+
history: list[dict[str, str]],
|
| 50 |
+
) -> tuple[MushroomState | None, str, list[dict[str, str]]]:
|
| 51 |
+
"""Gradio callback for studying the current mushroom."""
|
| 52 |
+
updated_current, updated_history = study_current(current, history)
|
| 53 |
+
return updated_current, mushroom_card_from_state(updated_current), updated_history
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def eat_mushroom(
|
| 57 |
+
current: MushroomState | None,
|
| 58 |
+
history: list[dict[str, str]],
|
| 59 |
+
) -> list[dict[str, str]]:
|
| 60 |
+
"""Gradio callback for the Eat choice."""
|
| 61 |
+
return eat_current(current, history)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def mushroom_card_from_state(current: MushroomState | None) -> str:
|
| 65 |
+
"""Render a mushroom card from Gradio state."""
|
| 66 |
+
if current is None:
|
| 67 |
+
return mushroom_card(None)
|
| 68 |
+
return mushroom_card(Mushroom.from_dict(current))
|
| 69 |
) -> tuple[CollectionState, str, list[dict[str, str]]]:
|
| 70 |
"""Gradio callback for collecting the current mushroom."""
|
| 71 |
updated_collection, updated_history = collect_current(current, collection, history)
|
|
|
|
| 79 |
current_mushroom = gr.State(None)
|
| 80 |
collection = gr.State([])
|
| 81 |
|
| 82 |
+
gr.HTML(hero_markdown())
|
| 83 |
gr.Markdown(
|
| 84 |
"""
|
| 85 |
# π Myco
|
|
|
|
| 91 |
with gr.Column(scale=2):
|
| 92 |
discovery = gr.Markdown(mushroom_card(None), elem_id="discovery-card")
|
| 93 |
with gr.Row():
|
| 94 |
+
explore_button = gr.Button("Start Exploring", variant="primary")
|
| 95 |
+
with gr.Row():
|
| 96 |
+
eat_button = gr.Button("Eat")
|
| 97 |
+
collect_button = gr.Button("Collect")
|
| 98 |
+
study_button = gr.Button("Study")
|
| 99 |
explore_button = gr.Button("Explore", variant="primary")
|
| 100 |
add_button = gr.Button("Add to MycoDex")
|
| 101 |
with gr.Column(scale=3):
|
|
|
|
| 103 |
with gr.Row():
|
| 104 |
prompt = gr.Textbox(
|
| 105 |
label="Ask Myco",
|
| 106 |
+
placeholder="Myco, should I eat this?",
|
| 107 |
placeholder="What do you know about this mushroom?",
|
| 108 |
scale=4,
|
| 109 |
)
|
| 110 |
ask_button = gr.Button("Ask Myco", scale=1)
|
| 111 |
|
| 112 |
+
with gr.Row():
|
| 113 |
+
progress = gr.Markdown(progress_markdown([]), elem_id="progress-panel")
|
| 114 |
+
dex = gr.Markdown(dex_markdown([]), elem_id="mycodex")
|
| 115 |
dex = gr.Markdown(dex_markdown([]), elem_id="mycodex")
|
| 116 |
|
| 117 |
explore_button.click(
|
| 118 |
explore,
|
| 119 |
inputs=[collection],
|
| 120 |
+
outputs=[discovery, current_mushroom, chat, dex, progress],
|
| 121 |
+
)
|
| 122 |
+
ask_button.click(ask_myco, inputs=[prompt, chat, current_mushroom], outputs=[prompt, chat])
|
| 123 |
+
prompt.submit(ask_myco, inputs=[prompt, chat, current_mushroom], outputs=[prompt, chat])
|
| 124 |
+
collect_button.click(
|
| 125 |
+
add_to_dex,
|
| 126 |
+
inputs=[current_mushroom, collection, chat],
|
| 127 |
+
outputs=[collection, dex, chat, progress],
|
| 128 |
+
)
|
| 129 |
+
study_button.click(study_mushroom, inputs=[current_mushroom, chat], outputs=[current_mushroom, discovery, chat])
|
| 130 |
+
eat_button.click(eat_mushroom, inputs=[current_mushroom, chat], outputs=[chat])
|
| 131 |
+
|
| 132 |
outputs=[discovery, current_mushroom, chat, dex],
|
| 133 |
)
|
| 134 |
ask_button.click(ask_myco, inputs=[prompt, chat, current_mushroom], outputs=[prompt, chat])
|
ui/renderers.py
CHANGED
|
@@ -2,10 +2,29 @@
|
|
| 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:
|
|
@@ -30,3 +49,26 @@ def dex_markdown(collection: CollectionState) -> str:
|
|
| 30 |
for entry in collection:
|
| 31 |
rows.append(f"* π **{entry['name']}** β {entry['rarity']} β found near {entry['habitat']}")
|
| 32 |
return "\n".join(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from game.progression import AREA_UNLOCKS, current_area, next_area, unlocked_areas
|
| 6 |
from game.state import CollectionState, EMPTY_DEX
|
| 7 |
from models.mushroom import Mushroom
|
| 8 |
|
| 9 |
|
| 10 |
+
def hero_markdown() -> str:
|
| 11 |
+
"""Render a polished first-impression hero panel."""
|
| 12 |
+
return """
|
| 13 |
+
<div class="myco-hero">
|
| 14 |
+
<div class="myco-mascot">π</div>
|
| 15 |
+
<div>
|
| 16 |
+
<p class="eyebrow">Cozy AI companion game</p>
|
| 17 |
+
<h1>Myco</h1>
|
| 18 |
+
<p>Explore a magical forest with Myco, discover strange fungi, and complete your MycoDex.</p>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def mushroom_card(mushroom: Mushroom | None) -> str:
|
| 25 |
+
"""Render the current mushroom discovery as markdown."""
|
| 26 |
+
if mushroom is None:
|
| 27 |
+
return "### π² Forest Clearing\nClick **Start Exploring** to search for a strange fungus."
|
| 28 |
def mushroom_card(mushroom: Mushroom | None) -> str:
|
| 29 |
"""Render the current mushroom discovery as markdown."""
|
| 30 |
if mushroom is None:
|
|
|
|
| 49 |
for entry in collection:
|
| 50 |
rows.append(f"* π **{entry['name']}** β {entry['rarity']} β found near {entry['habitat']}")
|
| 51 |
return "\n".join(rows)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def progress_markdown(collection: CollectionState) -> str:
|
| 55 |
+
"""Render collection goals and area unlock progress."""
|
| 56 |
+
count = len(collection)
|
| 57 |
+
active_area = current_area(count)
|
| 58 |
+
upcoming_area = next_area(count)
|
| 59 |
+
unlocked_names = ", ".join(area.name for area in unlocked_areas(count))
|
| 60 |
+
|
| 61 |
+
rows = [
|
| 62 |
+
"### β¨ Forest Progress",
|
| 63 |
+
f"**MycoDex discoveries:** {count}",
|
| 64 |
+
f"**Current area:** {active_area.name} β {active_area.description}",
|
| 65 |
+
f"**Unlocked:** {unlocked_names}",
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
if upcoming_area:
|
| 69 |
+
remaining = upcoming_area.required_discoveries - count
|
| 70 |
+
rows.append(f"**Next goal:** {remaining} more discoveries to unlock **{upcoming_area.name}**.")
|
| 71 |
+
else:
|
| 72 |
+
rows.append(f"**All goals visible:** You have reached **{AREA_UNLOCKS[-1].name}**.")
|
| 73 |
+
|
| 74 |
+
return "\n\n".join(rows)
|
ui/styles.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom CSS for Myco's polished Gradio presentation."""
|
| 2 |
+
|
| 3 |
+
APP_CSS = """
|
| 4 |
+
.gradio-container {
|
| 5 |
+
background:
|
| 6 |
+
radial-gradient(circle at top left, rgba(255, 244, 190, 0.55), transparent 32rem),
|
| 7 |
+
linear-gradient(135deg, #10281f 0%, #214732 48%, #f6e7c8 100%);
|
| 8 |
+
}
|
| 9 |
+
.myco-hero {
|
| 10 |
+
display: flex;
|
| 11 |
+
gap: 1.25rem;
|
| 12 |
+
align-items: center;
|
| 13 |
+
padding: 1.5rem;
|
| 14 |
+
border-radius: 28px;
|
| 15 |
+
background: rgba(255, 250, 231, 0.88);
|
| 16 |
+
border: 1px solid rgba(64, 96, 70, 0.24);
|
| 17 |
+
box-shadow: 0 20px 60px rgba(15, 40, 26, 0.22);
|
| 18 |
+
}
|
| 19 |
+
.myco-mascot {
|
| 20 |
+
display: grid;
|
| 21 |
+
place-items: center;
|
| 22 |
+
width: 6rem;
|
| 23 |
+
height: 6rem;
|
| 24 |
+
border-radius: 999px;
|
| 25 |
+
font-size: 4rem;
|
| 26 |
+
background: linear-gradient(145deg, #fff8d9, #f5b7a7);
|
| 27 |
+
box-shadow: inset 0 -10px 24px rgba(121, 62, 35, 0.16);
|
| 28 |
+
}
|
| 29 |
+
.eyebrow {
|
| 30 |
+
margin: 0;
|
| 31 |
+
color: #2f6f4f;
|
| 32 |
+
font-weight: 800;
|
| 33 |
+
letter-spacing: 0.08em;
|
| 34 |
+
text-transform: uppercase;
|
| 35 |
+
}
|
| 36 |
+
.myco-hero h1 {
|
| 37 |
+
margin: 0.1rem 0;
|
| 38 |
+
font-size: 3rem;
|
| 39 |
+
color: #173d2a;
|
| 40 |
+
}
|
| 41 |
+
#discovery-card, #mycodex, #progress-panel {
|
| 42 |
+
padding: 1rem;
|
| 43 |
+
border-radius: 20px;
|
| 44 |
+
background: rgba(255, 250, 235, 0.9);
|
| 45 |
+
border: 1px solid rgba(72, 108, 75, 0.18);
|
| 46 |
+
}
|
| 47 |
+
"""
|