"""Hugging Face Docker Space demo for Quiet Town Corner Shop. Interactive Gradio shell over the pure-Python shop simulation. """ from __future__ import annotations import json import os from pathlib import Path from typing import Any import gradio as gr # Project root is the Space root (Dockerfile WORKDIR) ROOT = Path(__file__).resolve().parent os.chdir(ROOT) from game.sim.day_cycle import Pace # noqa: E402 from game.sim.session import GameSession # noqa: E402 PACE_MAP = { "Relaxed": Pace.RELAXED, "Normal": Pace.NORMAL, "Brisk": Pace.BRISK, } HERO_CANDIDATES = [ ROOT / "assets/shop/interior_alley_flower_shop_01.jpg", ROOT / "assets/shop/interior_grad_v1.jpg", ROOT / "assets/shop/interior_morning_grad_v1.jpg", ROOT / "assets/shop/exterior_dusk_grad_v1.jpg", ] CHARACTER_CANDIDATES = [ ROOT / "assets/characters/regular_lan_portrait_01.jpg", ROOT / "assets/characters/regular_mori_portrait_01.jpg", ROOT / "assets/characters/regular_lan_grad_v1.jpg", ] def _pick_images(paths: list[Path], limit: int = 4) -> list[str]: out: list[str] = [] for p in paths: if p.is_file(): out.append(str(p)) if len(out) >= limit: break if len(out) < limit: shop = ROOT / "assets" / "shop" if shop.is_dir(): for p in sorted(shop.glob("*.jpg"))[: limit - len(out)]: out.append(str(p)) return out def _regular_visits_nonzero(raw: dict[str, Any] | None) -> dict[str, int]: if not raw: return {} return {k: int(v) for k, v in raw.items() if v} def run_one_day(seed: int, pace_name: str) -> tuple[str, str, list[str]]: pace = PACE_MAP.get(pace_name, Pace.NORMAL) session = GameSession(rng_seed=int(seed), pace=pace) report = session.run_until_night() summary = session.night_summary() payload = { "title": "Quiet Town Corner Shop · one-day sim", "seed": int(seed), "pace": pace_name, "day_report": { "day_index": report.day_index, "enters": report.enters, "sales": report.sales, "revenue": report.revenue, "wallet_end": report.wallet_end, "mood_line": report.mood_line, "phase_log": list(report.phase_log or []), "regular_visits": _regular_visits_nonzero(getattr(report, "regular_visits", None)), }, "night_summary": summary, } pretty = json.dumps(payload, ensure_ascii=False, indent=2) card = ( f"### Day result\n" f"- **Wallet**: {report.wallet_end}\n" f"- **Enters / sales / revenue**: {report.enters} / {report.sales} / {report.revenue}\n" f"- **Mood**: {report.mood_line}\n" f"- **Diary**: {summary.get('diary', '')}\n" f"- **Top atmosphere**: {', '.join(summary.get('top_dims') or [])}\n" ) gallery = _pick_images(HERO_CANDIDATES + CHARACTER_CANDIDATES, limit=6) return card, pretty, gallery def run_multiday(seed: int, pace_name: str, days: int) -> tuple[str, str, list[str]]: pace = PACE_MAP.get(pace_name, Pace.BRISK) days = max(1, min(int(days), 7)) session = GameSession(rng_seed=int(seed), pace=pace) rows: list[dict[str, Any]] = [] for _ in range(days): if hasattr(session, "restock"): try: session.restock("tulip_pink", 2) except Exception: pass report = session.run_until_night() summary = session.night_summary() rows.append( { "day": report.day_index, "enters": report.enters, "sales": report.sales, "revenue": report.revenue, "wallet_end": report.wallet_end, "mood": report.mood_line, "diary": summary.get("diary"), } ) if hasattr(session, "sleep"): session.sleep() pretty = json.dumps( {"title": "Multi-day wallet curve", "seed": int(seed), "pace": pace_name, "days": rows}, ensure_ascii=False, indent=2, ) lines = ["### Multi-day summary"] for r in rows: lines.append( f"- Day {r['day']}: wallet **{r['wallet_end']}**, " f"sales {r['sales']}, revenue {r['revenue']} — {r['mood']}" ) gallery = _pick_images(HERO_CANDIDATES + CHARACTER_CANDIDATES, limit=6) return "\n".join(lines), pretty, gallery INTRO = """ # 小镇角落小店 · Quiet Town Corner Shop Graduation-project **cozy flower-shop sim** (Python pure logic + content JSON + Imagine art). This Space is a **portfolio demo**: run the day-cycle simulation in the browser. Full source: [github.com/xiaoqianran/web005](https://github.com/xiaoqianran/web005) | Layer | Notes | |-------|--------| | Sim | open → tidy → guests → atmosphere / stock → regulars | | Content | JSON SKUs, NPCs, dialogue, tuning | | Art | shop / product / furniture / character stills | | Runtime | stdlib-first Python; Gradio shell for HF Docker Space | """ def build_ui() -> gr.Blocks: with gr.Blocks(title="Quiet Town Corner Shop") as demo: gr.Markdown(INTRO) with gr.Row(): seed = gr.Number(value=7, label="RNG seed", precision=0) pace = gr.Dropdown(choices=list(PACE_MAP.keys()), value="Brisk", label="Pace") days = gr.Slider(1, 7, value=3, step=1, label="Days (multi-day)") with gr.Row(): btn_day = gr.Button("Run one day", variant="primary") btn_multi = gr.Button("Run multi-day curve") card = gr.Markdown("Results will appear here.") raw = gr.Code(language="json", label="JSON report") gallery = gr.Gallery(label="Art samples", columns=3, height=360) btn_day.click(run_one_day, inputs=[seed, pace], outputs=[card, raw, gallery]) btn_multi.click(run_multiday, inputs=[seed, pace, days], outputs=[card, raw, gallery]) # Warm first paint demo.load( fn=lambda: run_one_day(7, "Brisk"), inputs=None, outputs=[card, raw, gallery], ) return demo if __name__ == "__main__": ui = build_ui() # HF Docker Spaces expect the app on 7860 ui.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")))