- **Frontend:** Next 16 (App Router, `output:"export"` → static), React 19, Tailwind v4, Framer Motion, Zustand. RTL Persian default + English; custom i18n in `src/lib/i18n.tsx` (NOT next-intl) — **every string must be added to BOTH the `fa` and `en` dicts**.
- **Engine:** pure-TS Hokm engine/AI in `src/lib/hokm/`; mirrored as C# (`server/src/Hokm.Engine`, static class **`Rules`** — not `Engine`, to avoid namespace clash). Validated by `scripts/sim.ts` (TS) and `server/tools/Hokm.Sim` (C#).
- **Service seam:** all networking goes through `OnlineService` (`src/lib/online/service.ts`). Two impls: `MockOnlineService` (offline) and `SignalrService` (live). `getService()` picks via `NEXT_PUBLIC_USE_SERVER==="1"`.
- **Backend:** .NET 10 ASP.NET Core + SignalR (`server/src/Hokm.Server`), EF Core (SQLite dev / Npgsql Postgres prod), JWT. Hub `/hub/game`; REST under `/api/*`. Profile stored as a JSON blob (`ProfileRow`) + coin `Ledger`. In-memory matchmaking/rooms in `GameManager`/`GameRoom`.
- **⚠️ CRITICAL — keep gamification in sync:** `src/lib/online/gamification.ts` (client) and `server/src/Hokm.Server/Profiles/Gamification.cs` (server) implement the SAME rules (rating/Elo, coins, XP, achievements, titles). **In live mode the server is authoritative** — ranked games run server-side and push `reward`/`profile` over the hub; client-run (vs-computer/private) games submit a `MatchSummary` to `POST /api/match/result`. If you change a rule, change BOTH files identically (ids/goals/coins/metrics/formulas).
- Online multiplayer over SignalR: matchmaking (pro skips queue, bots fill after wait), live server-run ranked games, server-authoritative entry/rewards. **Matchmaking waits ~15s (randomized 12–18s) for humans, then bots fill** (`GameManager.NextQueueWaitMs`; mock mirrors it in `beginSearch`). MatchmakingScreen shows the elapsed timer + a bot-fill hint.
- **Per-league turn time** (think faster in higher leagues): Starter/vs-AI/private → **15s**, Pro (stake ≥500) → **10s**, Expert (stake ≥1000) → **7s**. Single source: `turnMsForStake(stake, speed?)` in `gamification.ts`; the live server mirrors it in `GameRoom.TurnMs`. The turn-timer bar reads it from `matchMeta.stake`.
- **Speed (Blitz) mode** — CLIENT-ONLY (vs-AI + private rooms; ranked stays standard). Flat **5s** turn clock (`SPEED_TURN_MS`), races to **5** points (`SPEED_TARGET_SCORE`), and ~½ pacing on animations/pauses (the `fast()` scaler in `game-store.scheduleAuto`). Threaded via `matchMeta.speed` + `GameSettings.speed`/`OnlineMatchConfig.speed`. Toggle on Home's vs-Computer card; a `SpeedBadge` (⚡) shows on the table HUD. No server change needed — private rooms are client-driven even in live mode, and ranked is intentionally excluded.
- **Card backs are pattern-distinct** (not just recoloured): each `CardBackDef` carries a `pattern` (`stripes/argyle/grid/dots/rays/scales/crosshatch/royal/filigree/gem`) + optional `motif` glyph. Rendering lives in `src/lib/cardBack.ts` (`cardBackVisual`/`cardBackMotif`/`backVisualFromDef`), used by `PlayingCard`, the shop preview, and the profile picker so all three match.
- **Purchasable titles:** `TitleDef.price` makes a title buyable; shop **Titles** section (`ShopItemKind` now includes `"title"`, server `ShopBuy` handles `title → OwnedTitles`, mock mirrors it). The equipped title shows **under your name on the table** (`SeatPlayer.title` → `SeatAvatar`, seat 0 from your profile in every mode incl. live via `applyServerState`) and in the **Discover/find list** (`PlayerSummary.title`, server `ToSummary`). Localize via `titleById(id)`. New themed packs: **کلکل/banter** (kolkol, tikeh, shakkak, raghib), **Persian trends/praise** (trends, tashvigh), **court cards** (khanevadeh: تکخال/آس دل/شاه خشت/بیبی گشنیز), moods (ehsasat). Banter uses the `Stamp` helper (rounded badge + Persian phrase); court cards use `CourtCard`. Profile **photo upload gated at level ≥ 25**. New sticker packs are **client-only** (server `ShopBuy` is generic) — add art to `Sticker.tsx` + an entry in `STICKER_PACKS`.
- **Daily rewards** (boosted): `[300, 500, 750, 1000, 1500, 2500, 7500]` — **must stay in sync** between client `gamification.ts DAILY_REWARDS` and server `ProfileService.DailyRewards` (server is authoritative for the claim). Day 7 is the gold "special" tier.
- **Social:** friends + chat **server-persisted** (`Social/SocialService`, REST + hub `friendRequest`/`social`/`chat`); friend remove needs confirm. **Premium chat = animated gold bubbles**. **Friend-request rate limit = 10 / rolling hour** per user (server `SocialService.TryRecordRequest`, static in-memory; mirrored in the mock). Both `addFriend(query)` and `addFriendById(userId)` funnel through it.
- **Public profiles:** tap any player in the **leaderboard / friends list / end-of-game roster** → `PublicProfileModal` (global, `ui-store.viewProfile(id)`) shows their identity, stats, and **achievement board** + a rate-limited **Add-friend** button. Server `GET /api/profile/{id}/public` → `PublicProfileDto` (no coins/phone/email); mock synthesizes deterministic stats seeded from the id. Client `OnlineService.getPublicProfile(id)` / `addFriendById(id)`.
- **Social hub:** `FriendsScreen` is now tabbed — **Friends / Discover / Messages**. **Discover** = find-friends search (debounced) + suggested players, each row taps to the public profile and has a rate-limited Add button. **Messages** = conversation inbox (`listConversations`, unread badges, relative time) → opens `ChatScreen`. New: `OnlineService.searchPlayers(q)` / `suggestedPlayers()` → `PlayerSummary[]`; server `GET /api/players/search?q=` + `/api/players/suggested` (`SocialService.SearchPlayers`/`Suggested`, online-first, excludes friends/self). Mock synthesizes results.
- **Profile gender + social links:** `UserProfile.gender` (`""|male|female|other`, shown as ♂/♀/⚧ in Discover + public profile + edited in `ProfileScreen`'s `SocialSettings`), `socials` (instagram/telegram/x/youtube handles or URLs, rendered as tappable chips), and `socialsVisibility` (**public / friends / hidden**). Helpers in `src/lib/social.ts` (`GENDER_META`, `SOCIAL_PLATFORMS`, `socialUrl`, `hasSocials`). **Privacy is server-enforced:**`SocialService.GetPublicProfile` only includes `Socials` when `public`, or `friends` && the viewer is a friend, or it's you; `hidden` → never. `PlayerSummary.gender` carried in discovery. Server fields on `ProfileDto` (`Gender`/`Socials`/`SocialsVisibility`); `ProfileService.Update` parses them; `updateProfile` patch widened (client interface + session-store + mock + signalr).
- **Forfeit:** request + teammate-confirm (server `GameRoom` forfeit flow); penalty = **lose 2× coins + 0 XP** (NO kot, and never mention kot); confirm dialog alerts the penalty.
- **End-of-game roster:** `MatchPlayersList` on the final screen (reward modal + AI match-over) lists everyone; **Add-friend** button for real non-bot players (seat `userId` threaded from server).
- **Celebrations:** `celebration-store` + `CelebrationOverlay` — animated XP count-up, level-up pop, achievement unlock; fires from shop purchases (XP/cosmetic) and unlocks. Reusable via `celebrate({...})`. Achievement rows (here + `PostMatchRewardsModal`) now reveal the **sticker pack** an achievement unlocks (`stickerPackForAchievement`).
- **Persistent level + XP bar:** `LevelXpBar` (avatar + Lv + progress, taps to profile) shows on Home (`TopBar`) and atop every inner screen (`ScreenHeader`, `showXp` default on) so level/XP is always visible.
- **Buy-coins gateway** opens in a **new tab** (`window.open(_blank)`, same-tab fallback if popup-blocked) so a slow/blocked ZarinPal page can't dead-end the SPA; balance refreshes on window focus. (Fixes the old `window.location.href` "page couldn't load" crash.)
- **UX/UI:** "Persian luxury" palette (navy/teal/gold, glass) + **UNO-style tactile UX** rolled out to Home (hero Play), Shop (detail sheets), Lobby, Matchmaking, Profile, Leaderboard. Primitives in `globals.css`: `.press-3d` (tactile press), `.safe-top/.safe-bottom/.safe-x` (notch), `.hud-shadow`, `.premium-chat`, **`.tap`** (44px min hit area). Online count floored at **≥50**. Match stays alive on exit (minimize/resume + ResumeGameBar). **No fake/periodic notifications** (removed as spam).
- **Accessibility pass:** global **`:focus-visible`** gold ring (keyboard/controller/switch nav — no pointer required); **reduced-motion** honored app-wide via a `@media (prefers-reduced-motion)` block (kills decorative CSS loops) **and**`<MotionConfig reducedMotion="user">` in `page.tsx` (tames all Framer Motion). Cramped 32px icon buttons (Friends accept/decline/msg/remove, Chat back/send, Room invite/bot/clear) bumped to **44px**. Empty states (Friends/Chat/Notifications) and loading **skeletons** (Leaderboard/Shop) added.
- **Production prep (bargevasat.ir):** `docker-compose.caddy.yml` + `Caddyfile` (auto-HTTPS: `bargevasat.ir`→web, `api.bargevasat.ir`→server), prod `appsettings`/`ENV_FILE` blocks pointing at the domain, and **`PRODUCTION.md`** (go-live + Cafe Bazaar publish/IAB checklist). Prepare-only — deploy/DNS is manual.
- **One running game per player:** server `GameManager.StartMatchmaking` re-syncs an existing live room instead of starting a 2nd; client (`game-store.hasActiveMatch()`) guards Home vs-computer + Lobby random/create → resumes the running match + notifies. Clears on forfeit/finish.
-`.gitea/workflows/ci-cd.yml`: **api-build** (dotnet build slnx + Hokm.Sim), **web-check** (tsc + next build), **deploy** (self-hosted; pg_dump backup → rollback tag → build → stop+rm+up `--no-deps` → health-wait → prune). Set the **`ENV_FILE`** repo secret (see `deploy/ENV_FILE.example`).
- **NuGet/npm go through the Nexus mirror over PLAIN HTTP** `http://171.22.25.73:8081/repository/{nuget,npm}-group/` — the HTTPS mirror serves a **partial cert chain** that container trust stores reject (NU1301 PartialChain / npm UNABLE_TO_GET_ISSUER). npm also uses `--strict-ssl=false`; NuGet HTTP source needs `allowInsecureConnections="true"`. Local Windows dev + Docker base-image pulls work over HTTPS (Windows trust store has the intermediate) — only in-container package feeds use HTTP.
- **Fonts are self-hosted** (`@fontsource-variable/vazirmatn` + `plus-jakarta-sans`) — `next/font/google` fetches Google at build time and FAILS on the Iran runner. Do not reintroduce `next/font/google`.
- Memory: localhost can be VPN-hijacked (EonVPN) → reach local services via LAN IP if needed.
---
## 5. TODO / next
1.**Generate real EF migrations** (`dotnet ef migrations add Init`, DesignTimeDbContextFactory targets Postgres) + point at live **Supabase**; today the server uses `EnsureCreated()` (auto-switches to `Migrate()` once migrations exist).
2.**Game-table UNO restyle — DONE** (bolder suit-aware cards + `xl` size, pulsing playable-card glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain).
3. Store **IAB** — **scaffolded** (`server/.../Payments/IabService.cs`, config-driven via `Iab__*`). **Cafe Bazaar** path is end-to-end pure-web: client deep-links `bazaar://in_app?...&sku=...&redirect_url=...` (`src/lib/storeBilling.ts`), Bazaar returns `?purchaseToken=`, `page.tsx` captures it → `verifyIab` → server OAuth-refresh→validate→credit. **Myket** verification is wired server-side, but its purchase trigger needs a **native Capacitor bridge** (`window.MyketBilling.purchase(sku)` → `{purchaseToken,productId}`) — see §6. **Remaining:** fill `IAB_*` creds (Bazaar client id/secret/refresh token, Myket access token) in `ENV_FILE`, confirm the exact Bazaar/Myket validate endpoints against your panels, write the Myket native plugin, and set per-build `NEXT_PUBLIC_STORE`=`bazaar|myket` + `NEXT_PUBLIC_APP_PACKAGE`. SKU == coin-pack id (`p1`–`p4`). `IAB_ALLOW_UNVERIFIED=true` credits without verifying — **dev only**.
5. Optional polish — **DONE**: premium gold chat is visible to the recipient (`ChatMessage.senderPro`, server `SocialService.IsPro` stamps it; mock marks ~half its friends pro); daily reward routes through the global `CelebrationOverlay` (`"daily"` variant + coins count-up).
6.**Match intro / "players joining" loading screen — DONE** (`MatchIntroOverlay`: seats slide in with "?" placeholders until live data arrives, 3-2-1-GO countdown; online-only via `matchIntroPending`/`consumeIntro`).
- **Can't use the headless preview** to verify visuals (it pauses animations/screenshots) — verify via builds + ask the user for screenshots. UI changes have been shipped "by the numbers".
- Server binds **0.0.0.0** in Docker via `ENTRYPOINT … --urls http://0.0.0.0:5005` (appsettings `Urls=localhost` wins over env, so command-line args are used).
- After schema changes in SQLite dev with `EnsureCreated()`, delete `server/src/Hokm.Server/hokm.db*` to recreate.
- Background `dotnet run &` from a Git-Bash shell dies when the shell exits; use a tracked background runner.
- Commit messages end with `Co-Authored-By: Claude …`. Both `messages/`-style i18n strings live in `src/lib/i18n.tsx` (fa+en).
`storeBilling.getStore()` returns `"myket"` when `available` is true; the client then calls `purchase(sku)` and POSTs the token to `/api/coins/iab/verify`. Until the plugin exists, Myket purchases report "unavailable" and fall back to the web gateway. Cafe Bazaar needs **no** native code (deep-link only).