# حکم‌شو — بک‌اند (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) رجوع کنید.
