| """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 |
|
|
| |
| ROOT = Path(__file__).resolve().parent |
| os.chdir(ROOT) |
|
|
| from game.sim.day_cycle import Pace |
| from game.sim.session import GameSession |
|
|
| 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]) |
|
|
| |
| demo.load( |
| fn=lambda: run_one_day(7, "Brisk"), |
| inputs=None, |
| outputs=[card, raw, gallery], |
| ) |
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| ui = build_ui() |
| |
| ui.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860"))) |
|
|