حکمشو — بکاند (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_card → hand_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 از سکه بیخبر میماند).
ساخت باینری / 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 رجوع کنید.