byte-vortex commited on
Commit
4713899
Β·
verified Β·
1 Parent(s): 3be82f0

Upload folder using huggingface_hub

Browse files
.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.HF_TOKEN }}
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
+ """