Imported from phucnx277/sam (
AGENTS.md). Install upstream withnpx skills add phucnx277/sam. Copyright stays with the author.
AGENTS.md
React 19 + Vite 7 PWA for playing "Sam", a Vietnamese card game. Multiplayer has no backend server — all shared state lives in Ably LiveObjects. UI text is localized (Vietnamese/English) via src/logic/i18n.ts + src/hooks/useI18n.ts.
Commands
npm run dev— Vite dev server (--host, exposes on LAN).npm run build—tsc -b && vite build. This is the typecheck; there is no separate typecheck script.npm run lint— ESLint flat config (eslint.config.js).npm run generate-pwa-assets— regenerate PWA icons/splash screens frompublic/logo.svg(config:pwa-assets.config.ts). Run only when the logo changes; output is committed underpublic/.- There is no test framework and no test files. Do not invent
npm test.
Critical gotchas
- The pre-commit hook mutates every commit (
.husky/pre-commit): it runsnpm run build, then./bump-version.sh(bumps the patch version inpackage.jsonand runsnpm install), thengit add -u. So commits are slow, fail if the build fails, and always change the version.git add -ustages modified tracked files only — explicitlygit addany new files before committing. - When committing only docs/specs (no code changes), use
git commit --no-verifyto skip the hook so it does not bump the version or run a needless build. vite.config.tsrunsgit rev-parse --short HEADat config load and injects__APP_VERSION__fromprocess.env.npm_package_version. Use the npm scripts, notvitedirectly, and build inside the git repo.- Path aliases: use
@hooks/*and@logic/*(declared in bothvite.config.tsandtsconfig.app.json). A bare@/*alias exists in Vite only and will failtsc— don't use it. - Tailwind is v4 via the
@tailwindcss/viteplugin. There is notailwind.config.js; global CSS starts with@import "tailwindcss";insrc/index.css. - Global ambient types (
Table,Game,Player,Card,GameState,PlayerAction, …) live insrc/type.d.ts— use them without importing.
Architecture
src/logic/*— pure, framework-free game rules. Functions take/returnTable/Game; no I/O. Most game behavior (turns, tigers, chip settlement) is here and inlogic/game.ts'sActionDef.src/hooks/*— Zustand stores (module-level singletons viacreate()) that wire logic to React and Ably.useAppDatais the Ably client;useLocalGame/useLocalPlayerpersist tolocalStorageundersam.*keys.- i18n:
src/locales/*holds thevi/endictionaries (viis the key source of truth;enmust satisfy the same key set).logic/i18n.tsis framework-free (types,translate,detectLocale, a module-levelt()used by the logic layer);hooks/useI18n.tsowns the locale, persists it undersam.locale, and syncs<html lang>+ the logic-layer locale. - Realtime data model: a root Ably LiveMap on channel
sam.lobbywith keytables; each table is a nested LiveMap whose values are JSON-stringified and parsed with helpers inlogic/util.ts(stringifyValues,parseTable). Read by subscribing to the LiveMap, write viachannel.objects.batch. src/components/*— grouped by flow:Credentials(player + Ably API key setup) →Tables→GamePlayer.Lobby.tsxswitches between them based on init state.- Playing cards render through the vendored custom elements
<card-item>/<card-list>fromsrc/lib/elements.cardmeister.min.js(loaded once insrc/main.tsx); don't reintroduce it elsewhere.
Runtime setup
- The app requires an Ably API key, pasted by the user on first run. It is base64-encoded and stored in
localStorageassam.apiKey(encodeApiKey/decodeApiKeyinlogic/util.ts) because raw keys contain characters some browsers mishandle. It can also be passed via?apiKey=(andtblId/tblPwfor table links);InitAppData.tsxstrips these from the URL after reading. - PWA uses
registerType: "autoUpdate"; the service worker is registered insrc/main.tsx. @vercel/analyticsis wired inApp.tsx.