cb27a16dc1
Game table & play - UNO-style restyle: suit-aware bolder cards (+xl size), pulsing playable glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain. - Per-league turn time via turnMsForStake: 15s starter/AI, 10s pro, 7s expert; mirrored server-side in GameRoom.TurnMs. - Speed (Blitz) mode for vs-AI/private: 5s turns, race to 5, ~halved pacing. - Matchmaking waits ~15s (randomized 12-18s) then fills bots; elapsed timer + hint. Rewards / gifts - Richer post-match modal (floating coins, XP bar), celebration overlay reveals the unlocked sticker pack, boosted daily rewards (client+server synced), themed 7-day daily with special day-7. Social - Public profile modal (identity, stats, achievement board) from leaderboard / friends / discover / end-of-game roster; rate-limited add-friend (10/hour). - Social hub: Friends / Discover (player search + suggestions) / Messages inbox. - Profile gender (shown in finder/profile) + social links with public/friends/ hidden visibility, enforced server-side. Cosmetics - Distinct card backs: per-design pattern families (stripes/argyle/grid/dots/ rays/scales/crosshatch/royal/filigree/gem) + luxury motifs (lib/cardBack.ts), consistent on table/shop/profile; +Peacock/Rose-Gold backs. - Purchasable titles (shop Titles section); title shown under the seat on the table and in discover/public profile. - 10 new sticker packs (banter/kol-kol, Persian trends, court cards, moods). - Persistent level+XP bar on Home and every inner screen. Payments - Buy-coins gateway opens in a new tab (no SPA dead-end) + focus refresh. - Store IAB scaffolding: Cafe Bazaar deep-link purchase + redirect-token capture, Myket native-bridge contract, server-side IabService.Verify for both stores, config-driven via Iab__* env. POST /api/coins/iab/verify (JWT). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
17 KiB
Barg-e Vasat (برگ وسط) — Project Handoff
Persian Hokm card game for the Iran market. Web/PWA + Android (Capacitor), vs-AI and online multiplayer, with a full gamification economy. This doc is the single source of truth for any agent/session continuing the project.
1. Repo, remotes, run
- Code root:
D:\Projects\hokm(Next.js app at root; .NET backend underserver/). - Git remote
origin= Gitea:https://git.soroushasadi.com/soroushdes/HokmPlay.git, branchmain. (No GitHub remote.) Pushingorigin maintriggers CI/CD (Gitea Actions). - Brand: name «برگ وسط» / "Barg-e Vasat" (insider Hokm slang — middle card when weak). Don't rename to "Hokm"; folder/repo stay hokm/HokmPlay.
Run locally (dev)
# frontend (root) — http://localhost:3000
npm run dev
# backend (.NET 10 + SignalR) — http://localhost:5005
cd server && HOKM_USE_SQLITE=1 dotnet run --project src/Hokm.Server/Hokm.Server.csproj
.env.local for live mode: NEXT_PUBLIC_USE_SERVER=1, NEXT_PUBLIC_SERVER_URL=http://localhost:5005.
Without NEXT_PUBLIC_USE_SERVER=1 the app runs fully offline against the in-memory mock service.
Run the Docker stack (local, prod-like) — what the user actually tests
Ports are in 1500–1600 so they coexist with npm run dev/dotnet run:
- web
http://localhost:1500(nginx serving the static export) → api:1505(.NET) →:1510postgres.
cd D:\Projects\hokm
copy deploy\ENV_FILE.example .env # set JWT_KEY, POSTGRES_PASSWORD; registries default to HTTP Nexus
docker compose build server web
docker compose up -d
Gotcha: after rebuilding the web image, docker compose up -d sometimes says "Running" and keeps the old image. Force it:
docker stop hokm-web && docker rm hokm-web && docker compose up -d --no-deps web
Build / verify (run before committing)
rm -rf .next && npx tsc --noEmit # types (rm .next first — stale .next/dev breaks typedRoutes)
npx tsx scripts/sim.ts # engine + gamification self-test (200 matches + 500 rewards)
dotnet build server/Hokm.slnx -c Release # server
npm run build # next static export
2. Architecture (the important bits)
- Frontend: Next 16 (App Router,
output:"export"→ static), React 19, Tailwind v4, Framer Motion, Zustand. RTL Persian default + English; custom i18n insrc/lib/i18n.tsx(NOT next-intl) — every string must be added to BOTH thefaandendicts. - Engine: pure-TS Hokm engine/AI in
src/lib/hokm/; mirrored as C# (server/src/Hokm.Engine, static classRules— notEngine, to avoid namespace clash). Validated byscripts/sim.ts(TS) andserver/tools/Hokm.Sim(C#). - Service seam: all networking goes through
OnlineService(src/lib/online/service.ts). Two impls:MockOnlineService(offline) andSignalrService(live).getService()picks viaNEXT_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) + coinLedger. In-memory matchmaking/rooms inGameManager/GameRoom. - ⚠️ CRITICAL — keep gamification in sync:
src/lib/online/gamification.ts(client) andserver/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 pushreward/profileover the hub; client-run (vs-computer/private) games submit aMatchSummarytoPOST /api/match/result. If you change a rule, change BOTH files identically (ids/goals/coins/metrics/formulas). - Zustand stores:
ui-store(History-API screen routing via hash),session-store(auth + profile, subscribes hubonProfile),online-store(friends/chat/leaderboard/matchmaking),game-store(the table driver —mode: ai|online,live, turn timer, minimize/resume, forfeit),sound-store,notification-store,celebration-store.
3. Feature status (DONE)
- Full offline vs-AI game (engine, AI, turn timer + auto-play, disconnect/reconnect sim).
- 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 inbeginSearch). 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?)ingamification.ts; the live server mirrors it inGameRoom.TurnMs. The turn-timer bar reads it frommatchMeta.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 (thefast()scaler ingame-store.scheduleAuto). Threaded viamatchMeta.speed+GameSettings.speed/OnlineMatchConfig.speed. Toggle on Home's vs-Computer card; aSpeedBadge(⚡) shows on the table HUD. No server change needed — private rooms are client-driven even in live mode, and ranked is intentionally excluded. - Economy: coins; ranked entry = stake (win +stake [+kot 40], lose −stake); free vs-computer/private rooms. Buy-coins via ZarinPal sandbox (merchant
299685fb-cadf-4dfc-98e2-d4af5d81528d, config-driven). Coin packs: starter 50k/95,000﷼, … Stores (Bazaar/Myket) must use their IAB (/api/coins/iab/verifyscaffolded; token verification TODO). - XP/levels: every game grants XP, winner ×2; premium (pro) ×1.5; max level 100; curve
100*lvl + 15*lvl². Store sells XP packs (xp1 +200/5k, xp2 +600/12k, xp3 +1500/25k coins; consumable; unlocks level achievements). - Achievements: ~100, metric-driven generator (categories: victory/kot/streak/hakem/level/rank/veteran), incl. "7× hakem", "7–0 sweep". Dedicated AchievementsScreen (tabbed) + Profile summary. Some unlock sticker packs.
- Cosmetics: avatars, titles (incl. expert/professional/captain/leader ladder), card front+back, reaction packs, 16 sticker packs (all custom inline-SVG art in
Sticker.tsx). ✨ Luxury tier (premium giftable items): luxury avatars (🦢🎩💎💰🏆 + 💠 rank-gated), luxury card backs (Diamond/Black Gold/Platinum/Peacock/Rose-Gold) + fronts (Diamond/Black Gold), luxury titles (Hokm Sultan/Emperor/Grandmaster). Shop tags items priced ≥2000 with a gold «ویژه/Luxury» badge + ring. - Card backs are pattern-distinct (not just recoloured): each
CardBackDefcarries apattern(stripes/argyle/grid/dots/rays/scales/crosshatch/royal/filigree/gem) + optionalmotifglyph. Rendering lives insrc/lib/cardBack.ts(cardBackVisual/cardBackMotif/backVisualFromDef), used byPlayingCard, the shop preview, and the profile picker so all three match. - Purchasable titles:
TitleDef.pricemakes a title buyable; shop Titles section (ShopItemKindnow includes"title", serverShopBuyhandlestitle → 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 viaapplyServerState) and in the Discover/find list (PlayerSummary.title, serverToSummary). Localize viatitleById(id). New themed packs: کلکل/banter (kolkol, tikeh, shakkak, raghib), Persian trends/praise (trends, tashvigh), court cards (khanevadeh: تکخال/آس دل/شاه خشت/بیبی گشنیز), moods (ehsasat). Banter uses theStamphelper (rounded badge + Persian phrase); court cards useCourtCard. Profile photo upload gated at level ≥ 25. New sticker packs are client-only (serverShopBuyis generic) — add art toSticker.tsx+ an entry inSTICKER_PACKS. - Daily rewards (boosted):
[300, 500, 750, 1000, 1500, 2500, 7500]— must stay in sync between clientgamification.ts DAILY_REWARDSand serverProfileService.DailyRewards(server is authoritative for the claim). Day 7 is the gold "special" tier. - Social: friends + chat server-persisted (
Social/SocialService, REST + hubfriendRequest/social/chat); friend remove needs confirm. Premium chat = animated gold bubbles. Friend-request rate limit = 10 / rolling hour per user (serverSocialService.TryRecordRequest, static in-memory; mirrored in the mock). BothaddFriend(query)andaddFriendById(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. ServerGET /api/profile/{id}/public→PublicProfileDto(no coins/phone/email); mock synthesizes deterministic stats seeded from the id. ClientOnlineService.getPublicProfile(id)/addFriendById(id). - Social hub:
FriendsScreenis 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) → opensChatScreen. New:OnlineService.searchPlayers(q)/suggestedPlayers()→PlayerSummary[]; serverGET /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 inProfileScreen'sSocialSettings),socials(instagram/telegram/x/youtube handles or URLs, rendered as tappable chips), andsocialsVisibility(public / friends / hidden). Helpers insrc/lib/social.ts(GENDER_META,SOCIAL_PLATFORMS,socialUrl,hasSocials). Privacy is server-enforced:SocialService.GetPublicProfileonly includesSocialswhenpublic, orfriends&& the viewer is a friend, or it's you;hidden→ never.PlayerSummary.gendercarried in discovery. Server fields onProfileDto(Gender/Socials/SocialsVisibility);ProfileService.Updateparses them;updateProfilepatch widened (client interface + session-store + mock + signalr). - Forfeit: request + teammate-confirm (server
GameRoomforfeit flow); penalty = lose 2× coins + 0 XP (NO kot, and never mention kot); confirm dialog alerts the penalty. - End-of-game roster:
MatchPlayersListon the final screen (reward modal + AI match-over) lists everyone; Add-friend button for real non-bot players (seatuserIdthreaded from server). - Celebrations:
celebration-store+CelebrationOverlay— animated XP count-up, level-up pop, achievement unlock; fires from shop purchases (XP/cosmetic) and unlocks. Reusable viacelebrate({...}). 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,showXpdefault 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 oldwindow.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-visiblegold 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">inpage.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. - Capacitor Android APK builds (Myket maven mirror at root
https://maven.myket.ir; init script pins buildTools 36 + JDK17). SeeANDROID.md. - CI/CD (Gitea Actions + Nexus mirror) + Docker stack. See
DEPLOY.md.
4. CI/CD + mirrors (Iran constraints)
.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 theENV_FILErepo secret (seedeploy/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 needsallowInsecureConnections="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/googlefetches Google at build time and FAILS on the Iran runner. Do not reintroducenext/font/google. - Memory: localhost can be VPN-hijacked (EonVPN) → reach local services via LAN IP if needed.
5. TODO / next
- Generate real EF migrations (
dotnet ef migrations add Init, DesignTimeDbContextFactory targets Postgres) + point at live Supabase; today the server usesEnsureCreated()(auto-switches toMigrate()once migrations exist). - Game-table UNO restyle — DONE (bolder suit-aware cards +
xlsize, pulsing playable-card glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain). - Store IAB — scaffolded (
server/.../Payments/IabService.cs, config-driven viaIab__*). Cafe Bazaar path is end-to-end pure-web: client deep-linksbazaar://in_app?...&sku=...&redirect_url=...(src/lib/storeBilling.ts), Bazaar returns?purchaseToken=,page.tsxcaptures 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: fillIAB_*creds (Bazaar client id/secret/refresh token, Myket access token) inENV_FILE, confirm the exact Bazaar/Myket validate endpoints against your panels, write the Myket native plugin, and set per-buildNEXT_PUBLIC_STORE=bazaar|myket+NEXT_PUBLIC_APP_PACKAGE. SKU == coin-pack id (p1–p4).IAB_ALLOW_UNVERIFIED=truecredits without verifying — dev only. - Iranian push provider for closed-app notifications (FCM/APNs blocked); in-app + real-time notifications already work.
- Optional: colored-chat visibility to OTHER players (needs sender-plan on chat messages); route daily-reward through the celebration overlay.
6. Working notes / gotchas
- 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(appsettingsUrls=localhostwins over env, so command-line args are used). - After schema changes in SQLite dev with
EnsureCreated(), deleteserver/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 …. Bothmessages/-style i18n strings live insrc/lib/i18n.tsx(fa+en). - Myket native bridge contract (for the Capacitor plugin to inject on
window):window.MyketBilling = { available: true, purchase(sku): Promise<{purchaseToken, productId}>, consume?(token): Promise<void> }.storeBilling.getStore()returns"myket"whenavailableis true; the client then callspurchase(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).