This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bé vui học — a Phoenix LiveView site of Vietnamese family board games played pass-and-play on one shared device. All UI copy is Vietnamese.
AGENTS.md holds the Phoenix 1.8 / LiveView / Elixir framework conventions for this project; read it for anything about Layouts.app, <.icon>, streams, forms, or Tailwind import syntax. This file covers what is specific to this codebase.
make dev # deps.get + mix phx.server → http://localhost:4050
mix setup # deps + assets install/build (first run)
mix precommit # REQUIRED before finishing: compile --warnings-as-errors, deps.unlock --unused, format, test
mix test # full suite
mix test test/game_hub/games/co_ca_ngua/board_test.exs:42 # single test by line
mix assets.build # tailwind + esbuild (no npm — both are Mix-managed binaries, no node_modules)
make hooks # one-time: installs the version-bump pre-commit hook
make deploy # VPS deploy: full release extract + restart (use this)
make deploy-hot # OTP hot upgrade, no restart — Elixir-only changes, see belowDev port 4050 (config/dev.exs); prod reads PORT (default 4050) and PHX_HOST.
There is no database. No Ecto, no repo, no persistence. A game lives entirely in the LiveView socket of the one browser playing it; Phoenix.PubSub is started by the generator but nothing uses it. Reloading the page starts a new game.
The game code is split so rules never depend on rendering:
GameHub.Games.CoCaNgua.Board— pure rules engine (releases on 1/6, extra turn on 6, triple-six forfeit, exact-landing finish, captures, safe squares, own- and enemy-horse blocking). Returns{:ok, board, result}/{:error, reason}. Its@moduledocis the authoritative rules list — change rules here, not in the LiveView.GameHub.Games.CoCaNgua.Layout— maps a colour + position to 1-indexed{row, col}on the 15×15 CSS grid. Pure geometry; the only place board coordinates are decided.GameHubWeb.CoCaNgua.SetupLive— pick 2–4 players and names, thenpush_navigateto the play route with everything in query params (no server-side session).GameHubWeb.CoCaNgua.GameLive— holds the%Board{}in assigns plus ahistorylist for undo, and renders it. Rendering/animation only; delegate every decision toBoard.GameHubWeb.HomeLive— the marketing landing page. The@gamesmodule attribute drives the game cards: a game withstatus: :soonrenders a "Sắp ra mắt" card with no link. When a new game ships, flip it tostatus: :readyand setpath:— that is the whole wiring, plus a route inrouter.ex. Cờ vua, Cờ tướng and Cờ vây are currently advertised but not built.
docs/design/co_ca_ngua_toybox_brief.md is the source of truth for anything visual (warm cream "Toy Box" look, chunky glossy 3D toy art, no casino/dark/neon). Re-read it before making a visual decision.
Styling is Tailwind v4 only, layered in assets/css/app.css:
@themeholds the palette and radii, so they exist as real utilities —text-ink,text-ink-soft,bg-cream,border-cream-deep,text-toy-red,bg-toy-green/20,rounded-toy-lg. Use utilities in templates; do not add inlinestyle=attributes (both landing and setup screens are at zero).@layer componentsholds the reusable multi-property patterns:toy-btn(+--red/--blue/--neutral),toy-card(+--link/--soon),toy-chip,toy-badge,toy-medallion,toy-input,landing-band,player-panel. Reuse these instead of re-deriving the same six utilities. Plain CSS, never@apply.@layer utilitiesholds theanimate-*classes; keyframes live at the end of the file.- Custom token names are prefixed (
--radius-toy-*) so they never shadow Tailwind's own scale — an earlier--radius-lg: 28pxsilently brokerounded-lgeverywhere.--toy-red,--cream-deep, … remain as@layer basealiases only because the game screen still references them viavar().
No emoji as UI icons — no screen uses them any more, and platform emoji would break the art direction anyway. Use the shared <.toy_icon name="dice" class="h-6 w-6" /> from CoreComponents, backed by the generated set in priv/static/images/ui/. New icons must match that art direction: generate them with the chatgpt-image skill on a flat #1E50FF background and cut them out with its slicer (--chroma blue).
The brand mark is the owl-with-a-book mascot, authored as vector art in priv/static/images/ui/logo.svg (so it stays crisp in a 16px favicon) and used in the header, footer and closing CTA. Dice and chess pieces are deliberately not brand marks — they read as gambling rather than as a children's learning app; the die only ever appears where it means the game's actual die. To change the mark, edit logo.svg, rasterise it on a transparent background, then run python3 tools/build_brand.py <raster.png> . — that regenerates favicon.ico, every priv/static/images/icons/*.png, and logo.png (convert to .webp).
Images are WebP under priv/static/images/{assets,ui,landing}/. priv/static/sw.js precaches the app shell: when adding images that must be available offline, add them to PRECACHE_URLS and bump CACHE_NAME, otherwise clients keep serving the old shell. Only the app.js / app.css bundles are supported — no external CDN src/href in layouts.
Deploy with make deploy (full extract + restart). A hot upgrade (make deploy-hot) keeps live LiveView sockets, but cannot ship asset changes: Phoenix.Endpoint reads cache_static_manifest once at boot, and a relup that only does :load_module never re-inits the endpoint — so the HTML keeps linking the digest from the last restart, and digested assets go out immutable, max-age=31536000. Reach for --hot only when the diff is pure Elixir and dropping a game mid-play would actually hurt.
The hot-upgrade machinery is still wired up and must stay consistent:
- The
.githooks/pre-commithook auto-bumps the patch version inmix.exsand rewrites the versions inappup.exon every commit tomain. Versions in the two files must always match. appup.exdescribes how to get from the previous version to this one.GameLiveholds game state, but:load_moduleis sufficient for plain UI/rules tweaks; only use{:update, Mod, {:advanced, []}}when a release changes the shape ofsocket.assignsand needscode_change/3to migrate live sessions.- See
deploy/HOT_UPGRADE.mdand the header comment indeploy.sh.
Engine tests live in test/game_hub/games/co_ca_ngua/. simulation_test.exs plays full random games to completion and independently re-derives the expected outcome of every roll from raw position arithmetic before comparing it with the engine — when changing rules in Board, expect to update that re-derivation too. Use Board.roll_with/2 (explicit die) for deterministic tests; Board.roll/1 is random.