This commit is contained in:
2026-06-15 16:53:25 +03:30
commit 390ef96e7e
34 changed files with 4463 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# حکم‌شو — بک‌اند (Go)
سرور بازی حکم. فازهای انجام‌شده: **اسکلت + احراز هویت OTP (کاوه‌نگار) + JWT**، **موتور حکم + لایه WebSocket (matchmaking + اجرای میز به‌صورت goroutine)**، **مقاوم‌سازی realtime (reconnect + timeout نوبت + بات)** و **اقتصاد و فروشگاه (کیف‌پول، اسکین کارت، سکه روزانه، تبلیغ rewarded، خرید IAP، تسویه‌ی داخل بازی)**.
## اجرا (محلی)
```bash
cp .env.example .env # و مقادیر را پر کنید
# یا مستقیم با env:
ADMIN_MOBILE=09120000000 ADMIN_OTP=11111 go run ./cmd/server
```
سرور روی `:8080` بالا می‌آید.
> به‌خاطر محدودیت شبکه برای دانلود ماژول‌ها، اگر لازم شد از میرور استفاده کنید:
> `go env -w GOPROXY=https://proxy.golang.org,direct` (همین الان کار کرد) یا یک میرور داخلی معتبر.
## متغیرهای محیطی
| متغیر | پیش‌فرض | توضیح |
|-------|---------|-------|
| `ADDR` | `:8080` | آدرس گوش‌دادن |
| `DB_PATH` | `hakemsho.db` | مسیر فایل SQLite |
| `JWT_SECRET` | `change-me…` | کلید امضای JWT (در پروداکشن عوض شود) |
| `KAVE_API_KEY` | — | کلید کاوه‌نگار |
| `KAVE_TEMPLATE` | `loginotp` | تمپلیت verify |
| `ADMIN_MOBILE` / `ADMIN_OTP` | — | ورود تستی بدون SMS |
## endpointها (فاز ۱)
```
GET /health
POST /api/auth/login-otp { mobile, fcm_token? } → { message: "otp sent" }
POST /api/auth/check-otp { mobile, token } → { user, token }
GET /api/me (Bearer JWT) → { user }
```
- throttle: حداکثر **۳ بار در دقیقه** روی `login-otp` به ازای هر شماره.
- کد OTP **۵ رقمی**، اعتبار **۱۵ دقیقه**، پس از مصرف حذف می‌شود.
- کاوه‌نگار از `verify/lookup.json` با تمپلیت `loginotp` استفاده می‌کند (مطابق پروژه approagency).
## WebSocket بازی (فاز ۲)
اتصال: `GET /ws?token=<JWT>` (یا هدر `Authorization: Bearer <JWT>`).
جریان کلی: اتصال → `join_queue` → وقتی ۴ نفر در صف شد میز ساخته می‌شود → `matched` + `state` → حاکم `choose_trump` → بازیکنان به‌نوبت `play_card``hand_over` / `game_over`.
پیام‌های **کلاینت → سرور** (JSON):
```json
{ "type": "join_queue", "mode": "normal" }
{ "type": "choose_trump", "suit": "hearts" } // فقط حاکم
{ "type": "play_card", "card": "AS" }
{ "type": "leave" }
```
پیام‌های **سرور → کلاینت**:
```json
{ "type": "matched", "room": "r1", "seat": 0, "players": [...] }
{ "type": "state", "phase": "playing", "your_seat": 0, "hakem": 0, "turn": 1,
"trump": "hearts", "your_hand": ["AS","10H"], "hand_counts": [13,13,13,13],
"trick": [{"seat":0,"card":"AS"}], "lead_suit": "spades",
"tricks_won": [0,0], "scores": [0,0], "target_score": 7, "players": [...] }
{ "type": "hand_over", "winner_team": 0, "kot": false, "points": 1, "scores": [1,0] }
{ "type": "game_over", "winner_team": 0, "scores": [7,5] }
{ "type": "player_disconnected", "seat": 2 } // قطع موقت؛ جایگاه برای بازگشت باز است
{ "type": "player_reconnected", "seat": 2 }
{ "type": "player_left", "seat": 2 } // خروج دائمی؛ جایگاه به بات تبدیل شد
{ "type": "error", "message": "..." }
```
در پیام `state`، هر بازیکن در `players` فیلدهای `bot` و `connected` دارد تا UI وضعیت میز را نشان دهد.
## مقاوم‌سازی realtime (فاز ۵)
- **timeout نوبت**: اگر بازیکن در مهلت نوبت (پیش‌فرض ۲۰s) حرکت نکند، سرور یک حرکت مجاز خودکار می‌زند (انتخاب حکمِ پرتعدادترین خال؛ پایین‌ترین کارتِ مجاز). همین مسیر، بات‌ها و بازیکنانِ قطع‌شده را هم اداره می‌کند (با تأخیر کوتاه‌تر).
- **reconnect**: قطع اتصال میز را خاتمه نمی‌دهد؛ جایگاه باز می‌ماند و خودکار بازی می‌شود. کاربر با همان توکن دوباره وصل می‌شود (هاب نگاشت `userID → میز/جایگاه` نگه می‌دارد) و `matched`+`state` می‌گیرد. اگر هیچ انسانِ متصلی در میز نماند، میز بسته می‌شود.
- **بات / پرکردن میز**: اگر تا مهلت matchmaking (پیش‌فرض ۱۲s) چهار انسان جمع نشد، جایگاه‌های خالی با بات پر می‌شوند تا بازی شروع شود. `leave` هم جایگاه را به بات تبدیل می‌کند تا بقیه ادامه دهند.
- **ایمنی همزمانی**: `seatInfo` فقط متعلق به goroutine میز است؛ هاب پس از ساخت میز هرگز به آن دست نمی‌زند و همه‌چیز از طریق action/endInfo رد و بدل می‌شود. تایمرها با شمارنده‌ی نسل (generation) از اجرای کهنه مصون‌اند. همه‌ی تست‌ها با `-race` سبزند.
نکات معماری:
- هر **میز یک goroutine** است و تنها نویسنده‌ی state بازی (الگوی actor، بدون قفل).
- **هاب** تنها هماهنگ‌کننده است (اتصال‌ها، صف، مسیریابی) و تنها نویسنده‌ی state خودش.
- نمای هر بازیکن فقط **کارت‌های خودش** را دارد؛ از بقیه فقط «تعداد کارت».
- منبع حقیقت سرور است: نوبت، follow-suit و مالکیت کارت سمت سرور اعتبارسنجی می‌شوند.
- تست‌ها: بازی کامل ۴ نفره روی WebSocket واقعی و سناریوی قطع‌اتصال، هر دو با `-race`.
## اقتصاد و فروشگاه
endpointها (همه پشت JWT):
```
GET /api/wallet → سکه، بلیط، XP، سطح، جام، VIP، کارت انتخابی
GET /api/shop → کاتالوگ + کارت‌های متعلق به کاربر + کارت انتخابی
POST /api/shop/buy-card { card_id } خرید اسکین کارت با سکه
POST /api/shop/select-card { card_id } انتخاب اسکین
POST /api/shop/purchase { store, kind, product_id, token } تأیید خرید IAP
POST /api/rewards/daily سکه روزانه (هر ۲۴ ساعت)
POST /api/rewards/ad { token } سکه رایگان پس از تبلیغ rewarded
```
مدل اقتصادی (کاتالوگ در [internal/economy/catalog.go](internal/economy/catalog.go)، برگرفته از اپ مرجع):
- **بسته‌های سکه** (پول واقعی، ۶ سطح با بونوس و VIP هدیه)، **بسته‌های بلیط**، **اسکین کارت** (با سکه)، **بوستر XP**، **انواع میز** (ورودی/جایزه/XP/جام).
- **سکه روزانه** و **سکه رایگان ۵۰تایی** فقط با دیدن کامل تبلیغ (تأیید سمت‌سرور؛ ضد تکرار با token یکتا + سقف روزانه).
- **VIP**: ۱۰٪ سکه‌ی بیشتر در هر خرید.
پرداخت و تبلیغ پشت اینترفیس‌اند (`AdVerifier`, `IAPVerifier` در [internal/economy/verify.go](internal/economy/verify.go)):
- تبلیغ: **تپسل** (آداپتر آماده) · IAP: **کافه‌بازار و مایکت** (آداپتر آماده).
- فعلاً تأییدکننده‌ی **توسعه** (`Dev*Verifier`) وصل است؛ با تنظیم creds به آداپتر واقعی سوییچ می‌شود.
### تسویه‌ی داخل بازی
`join_queue` یک `tier` می‌گیرد (مثل `beginner`/`pro`). هاب هنگام ورود به صف **ورودی** را کسر می‌کند؛ در پایان بازی **جایزه/XP/جام** به برندگانِ انسان واریز و بازی در `game_history` ثبت می‌شود. اگر بازی پیش از پایان لغو شود، ورودی **بازگردانده** می‌شود (به‌جز کسی که داوطلبانه `leave` کرده). تسویه پشت اینترفیس `ws.Settler` است (لایه ws از سکه بی‌خبر می‌ماند).
## ساخت باینری / Docker
```bash
CGO_ENABLED=0 go build -ldflags="-s -w" -o server ./cmd/server # باینری استاتیک
docker build -t hakemsho-backend . # image مینیمال (distroless)
```
## ساختار
```
cmd/server نقطه ورود و wiring
internal/config خواندن env
internal/store SQLite + migrations (embed)
internal/user مدل و ریپوی کاربر
internal/auth OTP، JWT، کاوه‌نگار، handlerها
internal/httpx پاسخ JSON و throttle
internal/game موتور حکم (کارت، قوانین، state machine) + تست‌ها
internal/ws هاب، میز (room)، کلاینت، پروتکل WebSocket، تسویه + تست‌ها
internal/economy کیف‌پول، کاتالوگ، کارت، روزانه/تبلیغ، IAP، تسویه + تست‌ها
```
## قدم بعدی
- وصل‌کردن آداپتر واقعی **تپسل** و **بازار/مایکت** با creds (الان استاب توسعه است).
- شروع **فرانت Flutter/Flame** (لاگین/OTP → لابی/فروشگاه → میز بازی).
- بهبود هوش بات (فعلاً حرکت مجاز ساده می‌زند).
- به [../BACKEND_PLAN.md](../BACKEND_PLAN.md) رجوع کنید.