The server only sent the queue size to the player who just joined, and the
client dropped the count entirely (emitMM ignored s.players). So two friends
queuing together never saw each other, even though the server does seat 2+
waiting humans together within ~25s.
- Server: BroadcastQueueLocked() pushes the current queue size to EVERY waiting
player on join/cancel (not just the joiner).
- Client: thread the count through emitMM → MatchmakingState.waiting.
- MatchmakingScreen shows "N players in queue" (mm.inQueue) when ≥2 humans wait,
so friends can tell they're queued together before bots fill the empty seats.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Production-readiness pass — remove mock-in-prod and harden the server:
- leaderboard: new DB-backed LeaderboardService + /api/leaderboard (ranked by
rating, 30s cache, bounded scan); client now calls it instead of mock fake data.
- online count: client uses real /api/stats/online (dropped the fabricated ≥50 floor).
- boot guards (Production): refuse to start if Sms:ApiKey is missing (OTP would
run in dev mode = fixed code for any phone) or Iab:AllowUnverified is true
(forged tokens could mint coins).
- payments: ZarinPal + IAB HttpClients get 15s timeouts; ZarinPal/FlatPay gateway
failures are now logged instead of silently swallowed.
- OTP: periodic prune of expired codes + stale rate-limit logs (was an unbounded
in-memory leak over a long-running process).
- DB: EnableRetryOnFailure for Postgres (transient-fault resilience).
- docker-compose: ZarinPal sandbox now defaults to false (real payments).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the uploaded profile photo only appeared in a few places (profile,
top bar, leaderboard, public profile); chat, friends, game table, match intro,
post-match roster and private rooms showed the emoji avatar only.
- carry avatarImage end-to-end:
- server DTOs: FriendDto, SeatPlayerDto, RoomPlayerDto, MatchmakeRequest +
Player/SeatSlot/PSeat; ResolveProfile now returns avatarImage; FriendDtoFor
fills it from the profile.
- client types: Friend, RoomSeat.player, MatchmakingState.players,
ServerSeatPlayer, SeatPlayer (adds avatarId + avatarImage).
- signalr-service: send my avatarImage on StartMatchmaking/CreatePrivateRoom;
carry it through mapRoom.
- game-store: applyServerState + newOnlineMatch + offline match now populate
avatarId/avatarImage (seat 0 uses your own profile photo).
- render every avatar through the shared <Avatar> component (image → emoji
fallback): ChatScreen, FriendsScreen (requests/friends/chats), GameTable
seats, MatchIntroOverlay, MatchPlayersList, RoomScreen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: the server sends matchFound then immediately broadcasts the first
state, but the client only subscribes to state inside enterServerMatch, which
runs a React effect later — so the ordered "state" message is dispatched while
there are no subscribers and is dropped. The server then waits for the human
hakem's trump choice that can never come → permanent freeze on the green felt.
- signalr-service: cache lastState; replay it to a late onState subscriber on a
microtask (after enterServerMatch resets its store); clear the cache on every
fresh-match entry (startMatchmaking / createRoom / acceptInvite) so a finished
game's final state is never replayed into a new match.
- safety net: if no state lands within 2.5s of matchFound, the client invokes
the new Resync hub method; server re-sends the current state to that player.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- full table of 4 humans still starts instantly at any time
- at the 15s checkpoint, 2+ humans start together (bots fill empty seats)
- a lone player now waits until a precise 25s deadline, then AI fills and starts
- lower the client "connection stuck" hint to 40s to match the shorter wait
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- server: a lone player in the online-league queue now keeps waiting (re-checking
every 15s) up to 75s so an online opponent has a real chance to join; the moment
a 2nd human queues they're matched together, and a full 4 still forms instantly.
Add PlayNow hub method to force-start with bots on demand.
- client: matchmaking screen shows a "شروع با ربات / Start with bots" button after
a few seconds so the player can skip the wait; waiting copy updated; raise the
"connection stuck" hint threshold to 90s so it no longer fires during normal waits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- server: remove pro instant-start so all players queue for up to 15s,
giving real players a chance to seat together before bot-fill
- post-match: render the 4 seats as a horizontal strip so every player
is visible at once without scrolling
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZarinPal only accepts callbacks on pay.flatrender.ir, so bargevasat
pays through the shared broker and is credited via a signed webhook.
- FlatPayService: broker client (HMAC-signed /v1/pay/request) + webhook
signature verification + in-memory idempotency guard.
- Program.cs: /api/coins/pay/request prefers the broker when configured
(FlatPay__ApiKey/Secret set), else the legacy direct ZarinPal path;
new public POST /api/coins/pay/webhook verifies the HMAC and credits
coins from the echoed metadata (idempotent).
- appsettings + docker-compose: FlatPay config (empty ⇒ legacy path).
- web: recognise the broker's ?status=Paid return + re-refresh profile
(coins are credited server-side via webhook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: a designated test phone (default 09120000000 / code 453115,
overridable via Sms__TestPhone/Sms__TestCode) skips real SMS and always
verifies — for Google Play / Bazaar / Myket reviewers. Give them these creds.
- Matchmaking UX: tapping a league now navigates to the matchmaking screen
BEFORE awaiting the SignalR handshake, so the button can't freeze. Added a
watchdog hint after 28s ("connection took too long, cancel & retry") so it
never spins forever when the hub doesn't connect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Private rooms were 100% client-simulated (the "friend" auto-accepted then bots
filled invited seats). Now they're server-authoritative over SignalR:
Server (GameManager.PrivateRooms + GameHub):
- Room registry with create/invite/accept/decline/addBot/clearSeat/start/leave.
- Invite pushes a `roomInvite` to that user (Clients.User); the seat stays
"invited" (a pending guest with their real profile, resolved server-side) — it
is NEVER replaced by a bot.
- StartPrivate refuses while any invite is pending; only EMPTY seats fill with
bots. Then it spins up a live GameRoom and matchFound → both devices enter.
- Host leave / disconnect closes the room (roomClosed); members free their seat.
Client:
- signalr-service implements the room methods over the hub (+ room/roomInvite/
roomClosed events, room mapping, onRoomInvite); mock keeps offline no-ops.
- online-store accept/declineInvite; RoomScreen blocks "Start" while an invite
is pending and auto-enters the live game on matchFound (host + friend).
- New global InviteModal (accept/decline) + i18n (invite.*, room.waitAccept).
Addresses: (1) no bot replacement, (2) game waits for acceptance, (3) invited
friend shown as a pending guest with their name/avatar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sound.ts: restored startMusic (was a no-op stub) playing the selected track
through musicGain (in-game mute still applies); default track switched to
"playful" (per user). Music auto-starts on init when enabled.
- Profile → Audio: re-added a music on/off toggle (so players can disable it
outside the game too); SFX toggle unchanged.
- Economy tune: starting coins 1000 → 2000 (mock defaultProfile + server
ProfileDto) so new players start a bit richer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old reward formula rounded (40+6g)/50 on the raw goal, so adjacent
milestones could pay the same (e.g. "1 win" and "5 wins" both 50) and the curve
was lumpy — not a standard escalating-reward ladder. Reward now scales by tier
index: 50, 150, 250 … capped at 1500, strictly increasing per milestone.
Mirrored client (gamification.ts) + server (Gamification.cs) so live grants match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Shop: when short on coins the detail sheet now shows "{n} more coins" + a
"Get coins" CTA that opens the buy-coins page (was a dead disabled button).
- Chat: pin/unpin conversations (max 3, persisted to localStorage); pinned float
to the top with a gold pin. i18n chat.pin/unpin/pinLimit.
- Surrender: server now rate-limits forfeit asks at a human teammate
(45s per-user cooldown) so it can't be spammed. (Bot teammate still ends
immediately; teammate confirm dialog already existed.)
- OTP login hardening: Kavenegar send now parses the API status from the body
(HTTP 200 can still be a failure) + logs it + 12s timeout; client auth fetch
gets a 20s AbortController timeout so a lost response surfaces an error
instead of freezing on "sending…".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth / security
- Rate-limit real SMS OTP sends (dev mode unlimited): 60s resend cooldown,
5 per phone/hour, 300/hour global backstop. OtpService.CheckAndRecordRate;
POST /api/auth/otp/request returns 429 {error,retryAfter}; AuthScreen shows
auth.rateLimited. Knobs in appsettings Sms (Sms__* env).
Private rooms (invite)
- Cancel-invite button on pending seats; friend picker shows presence
(online/offline/in-game, sorted online-first) and flags in-game players.
- Mock invite stays pending ~3.5s and a cancel truly stops the auto-accept
(was a bug that re-seated cancelled invites).
In-game UI
- Scoreboard is compact + shrink-safe (no overflow on narrow screens).
- Played trick cards land dead-center (were ~2px off the corner anchor).
Plus the in-flight typing-indicator work (GameHub, ChatScreen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: generates a 5-digit code, stores it (in-memory, 120s TTL, max 5
tries, single-use), and sends it via Kavenegar verify/lookup
(template "hokmotp", %token = code). Normalizes +98/98 → 09xxxxxxxxx.
- /api/auth/otp/request + /verify now use it. No SMS_API_KEY ⇒ dev mode
(accepts a fixed code, returns devCode for local testing).
- Config: Sms section (appsettings) + Sms__* compose mapping + SMS_* in the
ENV_FILE template.
Security: sanitized deploy/ENV_FILE.example back to placeholders (it had picked
up real secrets) and added /deploy/ENV_FILE.local to .gitignore as the real
master copy (never committed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Myket's server-to-server validation is POST
/api/partners/applications/{pkg}/purchases/products/{sku}/verify with the
purchase token in the JSON body ({"tokenId": ...}) + X-Access-Token header —
not a GET with the token in the path. purchaseState 0 = valid.
Ref: https://myket.ir/kb/pages/server-to-server-payment-validation-api/
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pack ids now equal the Bazaar/Myket SKUs (Coin5K / Coin12K / Coin28K /
Coin65K) in both the server source-of-truth (ProfileService.Packs) and the
mock, so the IAB credit path (Id == ProductId) grants the right coins.
Coin totals + prices already matched the registered products.
- storeBilling.getStore(): when running inside the Capacitor Android shell and
no explicit NEXT_PUBLIC_STORE flavor is set, default to "bazaar" so the APK
uses Cafe Bazaar IAB (the web build stays on the ZarinPal gateway). Myket's
native bridge still overrides when present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- storeBilling.ts and IabService PackageName defaulted to com.bargevasat.hokm,
but the real app id is com.bargevasat.app (capacitor + android applicationId).
The mismatch would break Bazaar deep-link purchases and server validation.
- Add IabOptions.BazaarRsaPublicKey to hold the Bazaar in-app billing RSA public
key (documented; for the Poolakey local-signature flow, unused by the current
deep-link + server pardakht verification).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fullscreen on mobile:
- Android (Capacitor): MainActivity now runs edge-to-edge and hides the status +
navigation bars (immersive, transient-on-swipe), re-asserted on focus.
- PWA: manifest display -> "fullscreen" with display_override fallback chain;
viewport gains viewport-fit: cover for proper safe-area/edge-to-edge handling.
Moderation auto-hide:
- ProfileService.ReportUser now de-dupes nudity reports per reporter and, once
NudityHideThreshold (3) distinct players flag a target's avatar as nudity,
auto-removes their custom photo (reverts to default avatar). Counted from the
ledger, so still no schema change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
is unrelated and unchanged.
Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
other) in the public-profile modal, and a flag button in the chat header
(reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both the mock and the .NET server already waited then bot-filled, but used a
random 12-18s window. Make it exactly 15s on both sides so the rule is clear:
wait 15s for real online players to join, then replace any unfilled seats with
bots and start.
- client: new MATCH_QUEUE_WAIT_MS = 15000 in gamification.ts; mock beginSearch
uses it instead of randInt(12000,18000).
- server: GameManager QueueWaitMs = 15000 (was randomized 12-18s per ticket).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New searchable city picker (src/lib/iran-cities.ts, ~60 Iranian cities,
fa/en search) shown as a gold reward card at the top of the profile Basic tab.
- First time a non-empty city is set, the player earns 500 coins (CITY_REWARD),
granted server-authoritatively. Collapses to a compact summary afterwards with
a "change city" option (no re-reward).
- Frontend: UserProfile.city + cityRewardClaimed; mock-service grants on first
set; session/service updateProfile accept `city`; celebratory toast + sfx.
- Backend (.NET): ProfileDto.City/CityRewardClaimed (JSON blob → no migration);
ProfileService.Update grants +500 once and writes a "city" ledger entry.
- i18n: city.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No more earned-only (rank/wins) cosmetics — every avatar, card back/front,
reaction & sticker pack now has a coin price. Rank/wins/achievement become
purchase requirements (coin · coin+rank · coin+rank+achievement), enforced
client (mock + ShopScreen lock label) and server (ProfileService.ItemGate,
keyed by kind:id). Ownership = default + purchased only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docker compose build interpolates the whole file, so the ${JWT_KEY:?} guard
failed the build step when ENV_FILE lacked JWT_KEY. Default it empty (${JWT_KEY:-})
so build/db steps succeed, and enforce the secret at runtime instead: the server
throws on boot in Production if Jwt:Key is missing/dev/<32 chars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds ~100 new purchasable gifts that are LOCKED until a level/rating gate is met,
then buyable with coins — value scales with the gate:
- 45 gift avatars (types.ts), 35 gift titles + 20 gift card backs (gamification.ts),
all reusing existing renderers. Tier (1-5) encoded in the id (-t<n>-).
- Gate model: GIFT_TIERS (shared) → reqLevel/reqRating on AvatarDef/TitleDef/
CardBackDef + ShopItem. Tiers: t1 free, t2 Lv10, t3 Lv20, t4 Lv35, t5 Rating1700.
- Shop UI: locked cards dim + show the requirement (Lock + "Level 20"), buy
disabled until met; mock buyItem enforces it offline.
- Server enforces generically — ProfileService parses the tier from the id and
checks the player's level/rating (no 100-entry mirror). Mirrors GIFT_TIERS.
- i18n shop.reqLevel/reqRating (fa+en).
Verified: tsc + sim + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- One running game per player: server rejects a 2nd matchmake while in a live
room (re-syncs the existing game); client guards Home vs-computer + Lobby
random/create — resumes the running match + notifies instead of starting another
(game-store hasActiveMatch()).
- Background music is now selectable: santoor (سنتی, calm Persian loop) and
playful (bouncy UNO-like) — sound.ts TRACKS + setMusicTrack (persisted),
sound-store musicTrack, picker in Profile → Audio. i18n added.
- Production config for bargevasat.ir (prepare-only; no live deploy):
appsettings.Production.example (CORS + ZarinPal + IAB to the domain),
docker-compose.caddy.yml + Caddyfile (auto-HTTPS reverse proxy
bargevasat.ir→web, api.bargevasat.ir→server), ENV_FILE PRODUCTION block,
PRODUCTION.md go-live + Cafe Bazaar publish/IAB checklist. Fixed IAB package
name to match Capacitor appId (com.bargevasat.app).
Verified: tsc + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Daily reward now routes through the global CelebrationOverlay: new "daily"
variant + coins count-up; claiming closes the daily modal and fires
celebrate({variant:"daily", coins}). Unifies the "you earned X" moment.
- Premium (pro) gold chat is now visible to the OTHER player: ChatMessage gains
senderPro; server resolves each participant's plan once (SocialService.IsPro)
and stamps it on ChatMessageDto; ChatScreen styles incoming bubbles with
.premium-chat when senderPro. Mock marks ~half its friends pro so it's visible
offline too.
Verified: tsc + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MatchIntroOverlay: UNO-style pre-game reveal — the 4 seats animate into the
table (with "?" placeholders until each player's data streams in for live
matches), a 3-2-1-GO countdown, then the table shows. Wired via game-store
matchIntroPending/consumeIntro, rendered online-only in GameScreen.
- Fix: intro.found / intro.getReady / intro.go existed only in the Persian dict;
added the English strings (would have shown raw keys to EN users).
- Checkpoint of the in-progress UI/social batch (CoinsPill, shop titles section,
friend-request rate limit, etc.) — all green.
Verified: tsc + next build + scripts/sim.ts + dotnet build server/Hokm.slnx all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forfeit penalty reworked (client + server gamification, in sync):
- Surrendering team loses DOUBLE the entry coins; winner takes the stake.
- Forfeiter earns NO XP. No kot is applied or mentioned anymore.
- MatchSummary/Dto carry a `forfeit` flag; GameRoom.FinalizeForfeit →
ApplyRewardsAsync(team) with Forfeit=true (dropped the kot path).
- Forfeit confirm dialogs now alert the real penalty (double coins, no XP).
End-of-game roster: SeatPlayerDto/ServerSeatPlayer + game-store SeatPlayer gain
userId/isBot. New <MatchPlayersList> lists everyone at the table on the final
screen (PostMatchRewardsModal + AI MatchOverlay) with a tactile "Add" button to
send a friend request to real (non-bot, non-self) players ("Sent" after).
Verified: tsc + sim + dotnet + next build clean; stack rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New global celebration system: celebration-store (queue) + CelebrationOverlay
(animated: count-up XP, filling bar, level-up pop, achievement cards; plays
levelUp/award sounds; tap or auto-dismiss). Rendered in page.tsx.
- Shop: every purchase now celebrates — XP packs animate XP gain + level-up,
cosmetics show a "purchased!" pop. Newly-unlocked achievements (diffed from
the profile before/after) animate too.
- XP purchases now actually evaluate achievements: gamification.evaluateAchievements
(client) + Gamification.EvaluateAchievements (server, called in ShopBuy xp path)
unlock level milestones + grant their coins.
Verified live: buying XP took L1→L5, unlocked level_5 server-side and credited its
reward. tsc + dotnet + next build clean; images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HTTPS Nexus serves an incomplete cert chain that container trust stores
reject (NU1301 PartialChain / UNABLE_TO_GET_ISSUER), failing CI restore/install.
- NuGet has no strict-ssl flag → point CI + Dockerfile + compose at the plain-HTTP
Nexus (http://171.22.25.73:8081, allowInsecureConnections) — no TLS, no cert check.
- npm: add --strict-ssl=false to the CI web-check install (Dockerfile already had it);
Docker npm registry default also moved to the HTTP Nexus.
- ENV_FILE.example documents NUGET_INDEX/NPM_REGISTRY overrides.
Local dev (Windows trusts the cert) + image base pulls (Docker trusts it) are
unaffected — only in-container package feeds switch to HTTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- XP packs in the store (coin-priced, intentionally expensive): xp1 200/5k,
xp2 600/12k, xp3 1500/25k. Consumable (grant XP, can level up) — server
ShopBuy handles kind "xp" via an authoritative XpPacks map + Gamification.GrantXp;
mock mirrors. New shop section + shop.xp/xpHint i18n.
- Every game grants XP and the WINNER earns 2x: matchXp is now
base*(won?2:1)*leagueFactor (was a flat +80 win bonus). Mirrored server-side.
- Premium (pro) perks: 1.5x XP multiplier (applied in applyMatchResult /
ApplyMatch by plan), plus animated shimmering gold chat bubbles for your own
messages (premium-chat CSS; ChatScreen gates on plan).
Verified: tsc + next + dotnet build clean; sim passes; live server — buying xp2
took L1→L3 and deducted 12k coins under the new curve. Images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosmetics — many new variants, the rarer ones gated behind higher ranks:
- Card backs: +midnight/jade/onyx (buy) + crimson/aurora/obsidian/imperial
(earned by wins/rating up to Master). Card fronts: +sunset/velvet/onyx (buy)
+ goldleaf/crystal/imperial (earned).
- Titles: +marksman, untouchable, sweeper, ruler, platinum_star, diamond_ace,
immortal, the_one (gated by kots/streak/shutouts/hakem/rating/level/wins),
mirrored on the server so live games grant them.
- Avatars: list expanded + rank/wins-earned tier (robot/wizard/ninja/king/
genie/crown) via new ownedAvatarIds(); profile picker shows earned ones,
shop sells the priced ones.
- Stickers: new Persian-text stamp pack (کوت! / دمت گرم / باریکلا / آخه؟) plus a
rank-earned Victory pack (بردیم!/حکم) — new inline-SVG art.
Leveling: XP per level now grows (100*l + 15*l²) so each level is harder; higher
leagues grant more XP (×1.5 at 500 stake, ×2 at 1000) so you progress by playing
up. Hard cap at level 100. Mirrored in server Gamification (XpForLevel/MatchXp/
AddXp). Sim now tops out lower (level 20 vs 35 over 500 matches) as intended.
Verified: tsc + next build + dotnet build clean; sim passes; images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Achievements: generator-driven, now 100+ across 7 categories (added Rulership)
mirrored client + server with identical ids/goals/coins. New tracked stats:
hakemRounds (be the hakem — incl. "7× Hakem"), roundsWon, plus losses metric.
Custom achievement-only sticker packs (Rulership 👑, Firestorm 🔥) with new
inline-SVG art (crown-gold, seven-zip, streak-fire), unlocked by hakem_7 /
streak_10. Server GameRoom tallies hakem rounds per seat + rounds won per team;
client tallies the same for vs-computer/private games (dealId-deduped).
Forfeit (surrender): a player can request forfeit; if the teammate is a bot it
auto-confirms, otherwise the human teammate gets a confirm/decline prompt
(20s timeout). Result: forfeiting with ≥1 round won = normal loss; 0 rounds = Kot.
Wired client↔server over the hub (RequestForfeit/ConfirmForfeit/DeclineForfeit
+ "forfeit" event); offline/vs-computer ends immediately in the store. Flag
button + confirm dialogs in the table.
Online count: never shows below 50 — live service floors the real count with a
drifting believable number (mock base lowered to ~50–170).
Matchmaking: real players get a longer priority window (9s) before bots fill;
bots now occasionally react after winning a trick (humanize).
Coins: starter pack is 95,000 Toman (50k coins); packs rescaled up (server + mock).
Verified: dotnet build + tsc + next build clean; sim unlocks 57 achievements/500
matches; live server: starter=95000, a 7-hakem win unlocks hakem_7 + wins_1 with
hakemRounds/roundsWon persisted. Images rebuilt on :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Achievements (client + server mirror, metric-driven so the list is one source):
- 37 achievements across 6 categories (Victories, Kot, Streaks, Levels, Ranks,
Veterancy) incl. 7–0 sweeps, kot milestones (1/5/10/25/50/100), win streaks
(3/5/10/15), level milestones every 5 (5..50), rank floors, games/tricks.
- New AchievementsScreen with category tabs, progress bars, coin + sticker-unlock
badges, and unlocked/locked states; summary header (unlocked count + coins).
- Some achievements unlock sticker packs: Seven–Zip→Hokm, 25 Kots→Taunts,
100 Wins→Persian (ownedStickerPackIds now also honors profile.unlocked).
- Prestige titles added: Expert, Professional, Captain, Leader (+ existing).
- Tracks new stat shutoutWins; MatchSummary.shutout (7–0). Profile shows a
6-item preview + "view all" link.
Leagues: 3 ranked entry tiers — Starter (100, lvl1), Pro (500, lvl10),
Expert (1000, lvl20). Higher league stakes more, so wins/losses swing bigger;
kot bonus now scales to the stake (40%). OnlineLobby shows league cards with
level gating.
Profile photo upload gated to level 25 (client button + server Update guard).
Win animation: PostMatchRewardsModal now shows an animated coins-won count-up
hero on a win.
Verified: dotnet build + tsc + next build clean; sim unlocks 26 achievements
over 500 matches; live server grants first_win/first_kot/shutout_1 and pays
2050 coins on an expert-league shutout+kot win. Images rebuilt on :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issues found bringing the stack up locally and fixed:
- Server was loopback-only inside the container (appsettings "Urls=localhost"
wins over ASPNETCORE_URLS) → published port returned "empty reply". Force the
bind with command-line args: ENTRYPOINT dotnet Hokm.Server.dll --urls 0.0.0.0:5005.
- Web image: npm install crashed on alpine ("Exit handler never called"); root
cause was UNABLE_TO_GET_ISSUER_CERT_LOCALLY — the Nexus mirror serves a partial
chain that Node's CA bundle can't complete. Use npm ci + strict-ssl=false.
- .NET restore hit the same partial chain (NU1301 PartialChain). Both registries
are now build ARGs (NUGET_INDEX / NPM_REGISTRY) defaulting to the HTTPS mirror
(CI runner trusts it); local .env overrides to the plain-HTTP Nexus
(http://171.22.25.73:8081) which has no TLS. NuGet feed is generated inline with
allowInsecureConnections so .NET 10 accepts the HTTP source.
Verified on local Docker (Postgres-backed): db+server+web all healthy; API + web
reachable from host on 1505/1500; auth → profile (1000 coins) → friend add/accept
(bidirectional) → chat (unread) all 200; rows persisted in Postgres
(Profiles=2, Friends=2, Messages=1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror:
- CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules).
- CI web-check: npm install + tsc --noEmit + next build (static export).
- deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build,
bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate,
no bare compose down), health-wait each, prune.
Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual
dev on 3000/5005: web :1500 (nginx static) -> server :1505 (.NET) -> db :1510
(postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via
nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static
export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback.
Config: server CORS is now config-driven (Cors__Origins) so the deployed web
origin is allowed without code edits. deploy/ENV_FILE.example documents the
Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations.
Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time
Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable
packages. Build is offline and ~3x faster; 7 woff2 emitted into out/.
Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean;
next build clean with self-hosted fonts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Social: EF-backed friends graph + chat (SocialService/SocialModels);
REST endpoints (friends add/accept/decline/remove/list/requests,
chat conversations/messages/send) with real-time hub events
(friendRequest/social/chat). GameManager tracks online users for presence.
- Client SignalrService: friends + chat now hit the server and react to
hub events (refetch + emit); no longer delegated to the mock.
- IAB: /api/coins/iab/verify endpoint + IabVerifyReq for Cafe Bazaar/Myket
(token verification is a documented TODO pending store accounts/SKUs).
- Persistence: EF Core Design package + DesignTimeDbContextFactory (Postgres),
Program auto-migrate/EnsureCreated, appsettings.Production.json.example
with Supabase connection + live ZarinPal template.
Verified end-to-end (two users, SQLite dev): request -> accept ->
bidirectional friends, chat send with per-user fromMe, unread count +
read-on-fetch. Server + client builds clean (dotnet build, tsc, next build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ZarinpalService (request/verify) + /api/coins/pay/request (JWT) and
/api/coins/pay/callback (verify → credit via ProfileService.BuyCoins → redirect
back with ?pay=success); merchant id from config (sandbox default)
- Client buyCoins (live) returns the StartPay redirect URL; BuyCoinsScreen
redirects; page.tsx handles the ?pay return (notify + refresh)
- Verified: sandbox request returns a real StartPay URL
- Documented Cafe Bazaar (Poolakey) / Myket IAB as the required store payment path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Server:
- daily (/api/daily, /api/daily/claim) + shop (/api/shop/buy) + ChargeEntry
- GameRoom (via IServiceScopeFactory) deducts ranked entry at match start and
applies match rewards at match-over, broadcasting profile + reward over the hub
- tested: daily, shop (owned-guard), ranked entry deduction pushed over hub
Client:
- SignalrService routes profile/coins/plan/daily/shop/match to the server (Bearer);
onProfile/onReward hub events; guest/offline fall back to local
- session-store syncs profile from hub; game-store serverReward; GameScreen shows
live ranked reward from hub (no double submit), submits client-run games
- single source of truth in live mode (no economy divergence)
Postgres-ready via config (Provider=postgres); EnsureCreated for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- EF Core (SQLite dev / Postgres prod via config); ProfileRow JSON blob +
LedgerRow audit; EnsureCreated at startup
- C# Gamification port (ranks/elo/coins/xp/achievements/titles) → server
computes match rewards; ProfileService (get/update/plan/buyCoins/applyMatch)
- JWT endpoints: profile GET/PUT, plan, coins packs/buy, match/result;
auth upserts the profile
- Tested end-to-end (buy + ranked win+kot persisted & server-computed)
- Client still mock-backed for now (wiring is the next step)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Name + tagline («بازی حکم آنلاین») across i18n (app.title/subtitle),
layout metadata, PWA manifest, app icon, package name, server health
- Gameplay term «حکم» unchanged; repo/folder stay hokm/HokmPlay
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OnlineService.getOnlineCount(); mock random-walks a believable number,
SignalrService reads GET /api/stats/online (server tracks hub connections)
- Home screen badge with pulsing dot, polls every 8s, localized digits
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- server/ monorepo: Hokm.Engine (C# port of TS engine+AI, validated by sim),
Hokm.Server (SignalR GameHub, in-memory matchmaking/rooms, server-side turn
timers + bot fill + disconnect handling, per-seat state broadcast), Hokm.Sim
- JWT dev auth (OTP 1234 + email); CORS for the Next client; /hub/game
- NuGet restored from mirrors (Soroush Nexus + Liara); NuGetAudit off
- README + .NET .gitignore; static class Engine renamed Rules (namespace clash)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>