Files
2026-06-15 20:54:05 +03:30

152 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# حکم‌شو — بک‌اند (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 از سکه بی‌خبر می‌ماند).
## دیتابیس
**SQLite** (درایور خالص Go، بدون cgo) — مناسبِ سرور کوچک (۱GB RAM / ۱ هسته): پروسه‌ی جدا و رمِ اضافه ندارد، حالتِ بازی در حافظه است و نوشتن فقط هنگام تسویه/خرید/پاداش رخ می‌دهد. برای بکاپ روی یک سرور تک، **Litestream** پیشنهاد می‌شود. کد با `database/sql` نوشته شده تا مهاجرت به Postgres در آینده کم‌دردسر باشد. migrationها با جدول `schema_migrations` فقط یک‌بار اجرا می‌شوند.
## پنل ادمین (HTML سرور‌ساید)
در مسیر **`/admin`**، داخل همان باینری Go (بدون پروسه/بیلدِ جدا)، پشت **Basic Auth** (`ADMIN_PANEL_USER`/`ADMIN_PANEL_PASS`):
- **داشبورد**: تعداد کاربران، خریدها، بازی‌ها، مجموع سکه.
- **فروشگاه**: ویرایش زنده‌ی کاتالوگ (بسته‌های سکه/بلیط، اسکین کارت، بوستر، انواع میز). با ذخیره، کشِ کاتالوگ تازه می‌شود و **هم API و هم تسویه‌ی بازی فوراً** مقادیر جدید را می‌گیرند.
- **کاربران**: جستجو، مشاهده‌ی موجودی/سطح، و کم/زیادکردن سکه (ثبت در `wallet_tx`).
کاتالوگ فروشگاه از جدول‌های دیتابیس خوانده می‌شود (مهاجرت `003_catalog.sql` با مقادیر اولیه seed می‌کند) و در حافظه کش می‌شود.
## ساخت باینری / 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) رجوع کنید.