2026-06-24 10:47:54 +03:30
2026-06-24 10:47:54 +03:30
2026-06-24 10:47:54 +03:30
2026-06-15 20:54:05 +03:30
2026-06-15 16:53:25 +03:30
2026-06-24 10:47:54 +03:30
2026-06-15 16:53:25 +03:30
2026-06-15 16:53:25 +03:30
2026-06-15 20:54:05 +03:30

حکم‌شو — بک‌اند (Go)

سرور بازی حکم. فازهای انجام‌شده: اسکلت + احراز هویت OTP (کاوه‌نگار) + JWT، موتور حکم + لایه WebSocket (matchmaking + اجرای میز به‌صورت goroutine)، مقاوم‌سازی realtime (reconnect + timeout نوبت + بات) و اقتصاد و فروشگاه (کیف‌پول، اسکین کارت، سکه روزانه، تبلیغ rewarded، خرید IAP، تسویه‌ی داخل بازی).

اجرا (محلی)

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_cardhand_over / game_over.

پیام‌های کلاینت → سرور (JSON):

{ "type": "join_queue", "mode": "normal" }
{ "type": "choose_trump", "suit": "hearts" }   // فقط حاکم
{ "type": "play_card", "card": "AS" }
{ "type": "leave" }

پیام‌های سرور → کلاینت:

{ "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، برگرفته از اپ مرجع):

  • بسته‌های سکه (پول واقعی، ۶ سطح با بونوس و VIP هدیه)، بسته‌های بلیط، اسکین کارت (با سکه)، بوستر XP، انواع میز (ورودی/جایزه/XP/جام).
  • سکه روزانه و سکه رایگان ۵۰تایی فقط با دیدن کامل تبلیغ (تأیید سمت‌سرور؛ ضد تکرار با token یکتا + سقف روزانه).
  • VIP: ۱۰٪ سکه‌ی بیشتر در هر خرید.

پرداخت و تبلیغ پشت اینترفیس‌اند (AdVerifier, IAPVerifier در 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

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 رجوع کنید.
S
Description
No description provided
Readme
17 MiB
Languages
Go 92.7%
HTML 6.6%
Shell 0.5%
Dockerfile 0.2%