{"id":50744342,"url":"https://github.com/wheeyls/lingua-valley","last_synced_at":"2026-06-10T19:30:28.983Z","repository":{"id":361508123,"uuid":"1254702034","full_name":"wheeyls/lingua-valley","owner":"wheeyls","description":"A Stardew Valley-inspired RPG that teaches Spanish — areas gated by language comprehension, not XP.","archived":false,"fork":false,"pushed_at":"2026-06-07T20:30:56.000Z","size":325,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-07T22:21:12.574Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://lingua-valley.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wheeyls.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-30T22:44:09.000Z","updated_at":"2026-06-07T20:31:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wheeyls/lingua-valley","commit_stats":null,"previous_names":["wheeyls/lingua-valley"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/wheeyls/lingua-valley","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wheeyls%2Flingua-valley","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wheeyls%2Flingua-valley/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wheeyls%2Flingua-valley/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wheeyls%2Flingua-valley/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wheeyls","download_url":"https://codeload.github.com/wheeyls/lingua-valley/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wheeyls%2Flingua-valley/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34168086,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-10T19:30:28.146Z","updated_at":"2026-06-10T19:30:28.971Z","avatar_url":"https://github.com/wheeyls.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lingua Valley\n\nA Stardew Valley–inspired RPG where **you don't level up by grinding XP — you level up by learning Spanish.**\n\nThe world is divided into areas, each tied to a CEFR proficiency level (A1, A2, …).\nYou can physically walk anywhere, but the areas are guarded by a **soft gate**: if\nyou wander into a zone above your level, the NPCs literally speak over your head.\nTheir dialogue is *garbled in proportion to how little you understand*, and you\ncan't act on quests you can't comprehend. The only way forward is to stay in your\ncurrent area, play its learning challenges, and master the skills the next zone\ndemands.\n\n## The core loop\n\n1. **Explore** an area (arrow keys / WASD).\n2. **Talk** to NPCs (walk close, press SPACE). Each NPC speaks at their area's level.\n3. **Learn** — NPCs offer learning challenges:\n   - **Voiced conversation gate** (Rosa, the greetings NPC): she *speaks* to you\n     (OpenAI TTS), you reply *by voice* (hold SPACE), Whisper transcribes it, and\n     GPT-4o — playing both the NPC and a CEFR examiner — grades whether you actually\n     communicated at the target level. Only a genuine spoken exchange opens the gate.\n   - **Multiple-choice vocab quiz** (other NPCs): the lighter fallback challenge.\n4. **Progress** — once you master *every* objective of your level, your effective\n   level rises, and the next area's speech becomes clear enough to act on.\n\nThere is no health, no XP bar, no character level. Your \"level\" is simply the\nhighest CEFR tier whose objectives you've fully mastered.\n\n## AI voice \u0026 conversation (Whisper + GPT-4o + TTS)\n\nThe conversation gate is powered by OpenAI, proxied through serverless functions\nin `/api` so the API key never reaches the browser:\n\n| Endpoint | Purpose | Model |\n|----------|---------|-------|\n| `POST /api/transcribe` | Speech → text (player's spoken Spanish) | `whisper-1` |\n| `POST /api/converse`   | NPC reply + CEFR grading (structured JSON) | `gpt-4o` |\n| `POST /api/speak`      | Text → speech (NPC voices) | `gpt-4o-mini-tts` |\n\nThe wire contract lives in `src/domain/conversation.ts` and is shared by both the\nclient and the functions. The gate only opens when the model says the objective is\nmet *and* the latest utterance clears the `gateShouldOpen` thresholds\n(`src/domain/conversation.ts`) — covered by unit tests.\n\nIf the backend is unreachable or the mic is unavailable, the conversation scene\ndegrades gracefully (shows a message; you can leave with ESC).\n\n## The vertical slice\n\n- **Plaza del Saludo (A1)** — greetings, introductions, numbers 1–10, courtesy.\n- **El Mercado (A2)** — market quantities, food vocab, bargaining.\n\nWalk through the archway on the right into the Mercado before finishing A1 and\nyou'll see the soft gate in action: the vendor's speech is mostly `····` noise.\n\n## Architecture (domain-first)\n\nLanguage-learning logic is kept **pure and testable**, separate from Phaser:\n\n```\napi/                 ← serverless functions (run on Vercel; keys server-side)\n  transcribe.ts      ← Whisper STT\n  converse.ts        ← GPT-4o NPC reply + CEFR grading\n  speak.ts           ← OpenAI TTS\n  _lib/openai.ts     ← shared client + model config\nsrc/\n  domain/            ← pure logic, no Phaser (unit-tested)\n    cefr.ts          ← CEFR levels + learning objective types\n    proficiency.ts   ← what the player has mastered; computes effective level\n    comprehension.ts ← the soft-gate model: clarity + dialogue garbling\n    conversation.ts  ← wire contract + gate thresholds for voiced conversation\n  content/\n    curriculum.ts    ← A1/A2 objectives + vocab\n    world.ts         ← areas, NPCs, level-tagged dialogue, voices\n  game/\n    state.ts         ← bridges domain ↔ Phaser, persists to localStorage\n    voice.ts         ← mic capture + audio playback\n    api.ts           ← client wrapper for the /api endpoints\n  scenes/            ← Phaser rendering layer\n    WorldScene       ← map, movement, NPC proximity/interaction\n    DialogueScene    ← renders dialogue through the comprehension model\n    ConversationScene← voiced conversation gate (Whisper + GPT-4o + TTS)\n    MinigameScene    ← the vocab quiz that masters objectives\n    HudScene         ← proficiency tracker + over-level warnings\n  main.ts\n```\n\nThe comprehension/soft-gate rules live in `src/domain/comprehension.ts` and the\nvoiced-gate thresholds in `src/domain/conversation.ts`; both are covered by tests\nin `src/domain/__tests__/`.\n\n## Run it\n\n### Frontend only (no AI voice)\n\n```bash\nnpm install\nnpm run dev        # http://localhost:5173 — world, movement, vocab quizzes\n```\n\nThe voiced conversation gate needs the `/api` functions, which Vite doesn't serve.\n\n### Full stack with voice (Whisper/GPT-4o/TTS)\n\n```bash\nnpm i -g vercel            # one-time\nvercel link                # link to your Vercel project\nvercel env pull            # or: export OPENAI_API_KEY=sk-...\nnpm run dev:full           # `vercel dev` — serves the game AND /api on one port\n```\n\nSet **`OPENAI_API_KEY`** in your Vercel project (Settings → Environment Variables)\nfor the deployed app, or locally via `vercel env` / a `.env` file used by `vercel dev`.\n\nOther scripts:\n\n```bash\nnpm run build      # typecheck + production build\nnpm run typecheck  # typechecks both src and api\nnpm test           # domain + full gameplay scenario tests (vitest)\n```\n\n## Adapter profiles \u0026 local-fakes (test without real services)\n\nThe game is built **hexagonally**: the domain depends only on *ports* (interfaces),\nand adapters implement them. This means the entire game — economy, persistence,\nauth, and **multiplayer presence** — can run against in-memory fakes with no\nnetwork, no Supabase, no OpenAI, and no websockets.\n\n`makeAdapters(profile)` (in `src/app/adapters.ts`) selects the implementation:\n\n| Profile        | Persistence | Grading (LLM)        | Multiplayer            | Clock        |\n|----------------|-------------|----------------------|------------------------|--------------|\n| `test`         | in-memory   | scripted fake        | in-process bus (fake)  | advanceable  |\n| `local-fakes`  | in-memory   | scripted fake        | in-process bus + ghosts| advanceable  |\n| `guest`        | localStorage| real `/api/converse` | none (noop)            | system       |\n| `cloud`        | (Supabase*) | real `/api`          | (Supabase Realtime*)   | system       |\n\n\\* Real Supabase adapters slot in at the composition root when configured —\nthat's the only place that changes.\n\n**Run the fully-faked sandbox** (no API keys, scriptable multiplayer):\n\n```\nnpm run dev    then open  http://localhost:5173/?dev=fakes\n```\n\nA **dev harness** (`DevScene`) appears bottom-left with shortcuts:\n- `G` — spawn a wandering ghost player (tests remote avatars with no network)\n- `N` — advance the clock one day (tests Focus regen + SRS card maturation)\n- `P` / `F` — set the next conversation grade to PASS / FAIL\n- live readout of pesos, focus, skills, mastery\n\nYou can also force a profile with `VITE_ADAPTER_PROFILE=local-fakes`.\n\n### Gameplay tests\n\n`src/app/__tests__/scenarios.test.ts` drives whole sessions against the fakes:\nsingle graded turn → rewards; multi-day practice → objective mastery; Focus\nbudget exhaustion + next-day refill; presence join/move/leave + ghosts; auth\nguest→account. All deterministic, zero framework mocks.\n\n## Extending\n\n- **New objectives/vocab:** add to `src/content/curriculum.ts`.\n- **New areas/NPCs/dialogue:** add to `src/content/world.ts` (tag lines with a CEFR level).\n- **New mini-game types:** add a scene like `MinigameScene` and launch it from `DialogueScene`.\n- **Tune the gate difficulty:** edit `clarityFor` / `ACTIONABLE_THRESHOLD` in `comprehension.ts`.\n- **Tune the economy:** edit pure functions in `src/domain/economy.ts` / `srs.ts` (covered by tests).\n- **Add a real service:** implement the relevant port (`PlayerStateRepository`,\n  `PresenceGateway`, etc.) in `src/net/`, then wire it in `makeAdapters(\"cloud\")`.\n  Nothing else changes — the domain and scenes are untouched.\n\nSee `docs/DESIGN.md` for the economy, schema, and architecture, and `AGENTS.md`\nfor the ports-and-adapters laws.\n\nGuest progress is saved to `localStorage`; signed-in progress will sync to\nSupabase (cloud profile) once configured.\n\n## Database migrations (automated)\n\nSchema changes live in `supabase/migrations/` as timestamped `.sql` files and\nare applied automatically by GitHub Actions — **no more copy-pasting SQL into the\ndashboard.**\n\n- **`.github/workflows/migrate.yml`** runs `supabase db push` on every push to\n  `main` that touches `supabase/migrations/**` (and can be run manually from the\n  Actions tab via *Run workflow*).\n- **`.github/workflows/ci.yml`** runs typecheck + tests on every push/PR.\n\n### One-time setup\n\nAdd three repo secrets (GitHub → Settings → Secrets and variables → Actions):\n\n| Secret | Where to get it |\n|--------|-----------------|\n| `SUPABASE_ACCESS_TOKEN` | Supabase → Account → Access Tokens → Generate |\n| `SUPABASE_PROJECT_REF` | Your project ref (the subdomain of the project URL, e.g. `onsqzglrnppdmppygzvp`) |\n| `SUPABASE_DB_PASSWORD` | Supabase → Project Settings → Database → Database password |\n\n**Baseline note:** the early migrations (`0001`–`0004`, now timestamped) were\napplied by hand before this automation existed. They all use `IF NOT EXISTS`, so\nre-running is safe. If the CLI's history table is empty, the first `db push` will\n(harmlessly) re-apply them; if it complains, run once locally:\n\n```bash\nsupabase link --project-ref \u003cref\u003e\nsupabase migration repair --status applied \u003cmigration_timestamp\u003e   # mark prior ones as applied\n```\n\n### Adding a new migration\n\n```bash\nsupabase migration new add_something   # creates a timestamped file\n# …edit the generated SQL…\ngit add supabase/migrations \u0026\u0026 git commit \u0026\u0026 git push   # CI applies it on main\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwheeyls%2Flingua-valley","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwheeyls%2Flingua-valley","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwheeyls%2Flingua-valley/lists"}