diff --git a/cmd/server/settler.go b/cmd/server/settler.go index 77eaebb..be39253 100644 --- a/cmd/server/settler.go +++ b/cmd/server/settler.go @@ -87,6 +87,24 @@ func (g gameSettler) ChargeTicket(userID int64) error { return g.eco.Adjust(context.Background(), userID, economy.CurrencyTicket, -1, "reshuffle", "") } +// ChargeCoins سکهٔ شرطِ میزِ خصوصی را کسر می‌کند (خطا اگر موجودی کافی نباشد). +func (g gameSettler) ChargeCoins(userID int64, amount int64, ref string) error { + if amount <= 0 { + return nil + } + return g.eco.Adjust(context.Background(), userID, economy.CurrencyCoin, -amount, "private_stake", ref) +} + +// AwardCoins سهمِ برنده از potِ میزِ خصوصی را اضافه می‌کند. +func (g gameSettler) AwardCoins(userID int64, amount int64, ref string) { + if amount <= 0 { + return + } + if err := g.eco.Adjust(context.Background(), userID, economy.CurrencyCoin, amount, "private_pot", ref); err != nil { + slog.Error("award coins", "user", userID, "err", err) + } +} + // ChargePrivateTable یک میز خصوصیِ رایگان را برای کاربر مصرف می‌کند. func (g gameSettler) ChargePrivateTable(userID int64) error { return g.eco.ConsumePrivateTable(context.Background(), userID) diff --git a/internal/ws/hub.go b/internal/ws/hub.go index 1e1f153..1248fd1 100644 --- a/internal/ws/hub.go +++ b/internal/ws/hub.go @@ -50,6 +50,7 @@ type pendingTable struct { clients []*Client hands int // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷) rot int // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲) + stake int // شرطِ سکه‌ایِ هر بازیکن (۰ یعنی رایگان) started bool // پس از فشردن «شروع» توسط میزبان } @@ -188,7 +189,9 @@ func (h *Hub) handle(in inbound) { case "join_table": h.joinTable(c, in.msg.Code) case "start_table": - h.startTable(c, in.msg.Hands) + h.startTable(c, in.msg.Hands, in.msg.Stake) + case "set_stake": + h.setStake(c, in.msg.Stake) case "rotate_table": h.rotateTable(c, in.msg.Dir) case "leave_table": @@ -245,12 +248,15 @@ func (h *Hub) joinTable(c *Client, code string) { } // startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان). -func (h *Hub) startTable(c *Client, hands int) { +func (h *Hub) startTable(c *Client, hands, stake int) { t := h.tables[c.tableCode] if t == nil || t.host != c.UserID || t.started { return } t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷) + if stake >= 0 { + t.stake = stake // آخرین شرطِ سکه‌ای (اگر همراهِ شروع فرستاده شد) + } t.started = true cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3}) for _, cl := range t.clients { @@ -298,6 +304,7 @@ func (h *Hub) onStartTable(code string) { room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی) room.private = true room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان + room.stake = int64(t.stake) // شرطِ سکه‌ایِ هر بازیکن for s := 0; s < 4; s++ { if seats[s].isBot { continue @@ -312,6 +319,22 @@ func (h *Hub) onStartTable(code string) { slog.Info("private room started", "room", room.ID, "code", code, "humans", humans) } +// setStake شرطِ سکه‌ایِ میز را تنظیم و برای همه پخش می‌کند (فقط میزبان، پیش از شروع). +func (h *Hub) setStake(c *Client, stake int) { + t := h.tables[c.tableCode] + if t == nil || t.host != c.UserID || t.started { + return + } + if stake < 0 { + stake = 0 + } + if stake > 1_000_000 { + stake = 1_000_000 + } + t.stake = stake + h.broadcastLobby(t) +} + // rotateTable جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز می‌چرخاند (فقط میزبان). // میزبان همیشه جایگاهِ اولش را حفظ می‌کند؛ چرخش تیم‌بندی را تغییر می‌دهد. func (h *Hub) rotateTable(c *Client, dir int) { @@ -379,7 +402,7 @@ func (h *Hub) broadcastLobby(t *pendingTable) { cl.trySend(mustJSON(tableLobbyMsg{ Type: "table_lobby", Code: t.code, Players: players, Host: cl.UserID == t.host, YouSeat: t.seatOf(i), - Remaining: remaining, Unlimited: unlimited, + Remaining: remaining, Unlimited: unlimited, Stake: t.stake, })) } } diff --git a/internal/ws/protocol.go b/internal/ws/protocol.go index 13d4209..92a2f52 100644 --- a/internal/ws/protocol.go +++ b/internal/ws/protocol.go @@ -14,6 +14,7 @@ type inboundMsg struct { Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاه‌ها (+۱/−۱) Text string `json:"text"` // برای chat: متنِ پیامِ آماده Emoji string `json:"emoji"` // برای chat: شناسه/کاراکترِ شکلک + Stake int `json:"stake"` // برای set_stake/start_table: شرطِ سکه‌ایِ میز } // chatMsg پیامِ چت/شکلکِ یک بازیکن که برای همه‌ی میز پخش می‌شود. @@ -40,6 +41,7 @@ type tableLobbyMsg struct { YouSeat int `json:"you_seat"` // جایگاهِ مطلقِ گیرنده (برای چیدمانِ صحنه) Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده Unlimited bool `json:"unlimited"` + Stake int `json:"stake"` // شرطِ سکه‌ایِ فعلیِ میز (تنظیمِ میزبان) } // countdownMsg شمارش معکوس پیش از شروع بازیِ خصوصی. diff --git a/internal/ws/room.go b/internal/ws/room.go index c6a1375..996ec77 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -49,8 +49,11 @@ type Room struct { ID string hub *Hub tier string - private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود) - handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier) + private bool // میز دورهمی: بدون ورودی/جایزه‌ی tier (فقط آمار و شرطِ سکه) + handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier) + stake int64 // شرطِ سکه‌ایِ هر بازیکن در میز خصوصی (۰ یعنی رایگان) + pot int64 // مجموعِ سکه‌های جمع‌آوری‌شده برای برنده + paid [4]bool // جایگاه‌هایی که شرط را پرداخته‌اند (برای بازگشت هنگام لغو) settler Settler seats [4]*seatInfo game *game.Game @@ -122,6 +125,19 @@ func (r *Room) start() { } r.game = game.NewGame(target, game.FirstHakem()) r.graceUntil = time.Now().Add(r.firstHandDelay) // بُر زدن + پخشِ ۵ کارت + // شرطِ سکه‌ایِ میزِ خصوصی: از هر بازیکنِ انسان کسر و به pot اضافه می‌شود + // (هر که موجودی نداشت، رایگان بازی می‌کند و به pot اضافه نمی‌شود). + if r.stake > 0 { + for seat, s := range r.seats { + if s.isBot || s.userID == 0 { + continue + } + if err := r.settler.ChargeCoins(s.userID, r.stake, r.ID); err == nil { + r.pot += r.stake + r.paid[seat] = true + } + } + } // اطلاعاتِ نمایشیِ بازیکنان (سکه و نشانِ رتبه) را یک‌بار هنگام شروع می‌گیریم. for _, s := range r.seats { if s.isBot || s.userID == 0 { @@ -462,26 +478,55 @@ func (r *Room) endRoom() { // settleGameOver جایزه را به برندگانِ انسان می‌دهد و بازی را ثبت می‌کند. func (r *Room) settleGameOver(res *game.HandResult) { + // برندگانِ انسان (برای تقسیمِ potِ میزِ خصوصی). + var winners []int64 for seat, s := range r.seats { if s.isBot || s.userID == 0 { continue } won := game.Team(seat) == res.WinnerTeam - if won && !r.private { - r.settler.AwardWinner(s.userID, r.tier) // میز دورهمی جایزه‌ی سکه ندارد + if won { + winners = append(winners, s.userID) + if !r.private { + r.settler.AwardWinner(s.userID, r.tier) + } } if !r.private { r.settler.AwardRank(s.userID, r.tier, won) // امتیازِ رتبه (اگر میزِ رتبه‌بندی باشد) } r.settler.RecordGameResult(s.userID, won) } + // تقسیمِ potِ شرطِ خصوصی بینِ برندگانِ انسان (باقی‌مانده به نفرِ اول). + if r.private && r.pot > 0 && len(winners) > 0 { + share := r.pot / int64(len(winners)) + rem := r.pot - share*int64(len(winners)) + for i, id := range winners { + amt := share + if i == 0 { + amt += rem + } + r.settler.AwardCoins(id, amt, r.ID) + } + r.pot = 0 + } r.settler.RecordGame(r.ID, r.playersJSON(res.WinnerTeam), res.WinnerTeam, res.Kot) } // refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشده‌اند بازمی‌گرداند. func (r *Room) refundAll() { if r.private { - return // میز دورهمی ورودی نگرفته، پس بازگشتی هم ندارد + // میزِ خصوصی: فقط شرطِ سکه‌ایِ پرداخت‌شده بازمی‌گردد (اگر شرطی بوده). + if r.stake > 0 { + for seat, s := range r.seats { + if s.isBot || s.left || s.userID == 0 || !r.paid[seat] { + continue + } + r.settler.AwardCoins(s.userID, r.stake, r.ID+":refund") + r.paid[seat] = false + } + r.pot = 0 + } + return } for _, s := range r.seats { if s.isBot || s.left || s.userID == 0 { diff --git a/internal/ws/settler.go b/internal/ws/settler.go index 45cb8c3..b8ff087 100644 --- a/internal/ws/settler.go +++ b/internal/ws/settler.go @@ -16,6 +16,10 @@ type Settler interface { PlayerProfile(userID int64) (coins int64, rankTier string) // ChargeTicket یک بلیت برای بُرِ مجدد در فازِ انتخابِ حکم مصرف می‌کند. ChargeTicket(userID int64) error + // ChargeCoins/AwardCoins برای شرطِ سکه‌ایِ میزِ خصوصی (pot). Charge در صورتِ + // کمبودِ موجودی خطا می‌دهد؛ Award سکه را به برنده اضافه می‌کند. + ChargeCoins(userID int64, amount int64, ref string) error + AwardCoins(userID int64, amount int64, ref string) // میز خصوصی (دورهمی). ChargePrivateTable(userID int64) error // مصرفِ یک میز رایگان (خطا اگر سقف پر باشد) PrivateTableInfo(userID int64) (remaining int, unlimited bool) @@ -36,6 +40,8 @@ func (noopSettler) RecordCut(int64) {} func (noopSettler) RecordGameResult(int64, bool) {} func (noopSettler) PlayerProfile(int64) (int64, string) { return 0, "" } func (noopSettler) ChargeTicket(int64) error { return nil } +func (noopSettler) ChargeCoins(int64, int64, string) error { return nil } +func (noopSettler) AwardCoins(int64, int64, string) {} func (noopSettler) ChargePrivateTable(int64) error { return nil } func (noopSettler) PrivateTableInfo(int64) (int, bool) { return 0, true } func (noopSettler) TargetHands(string) int { return 7 } diff --git a/internal/ws/ws_test.go b/internal/ws/ws_test.go index 2a057da..5660d19 100644 --- a/internal/ws/ws_test.go +++ b/internal/ws/ws_test.go @@ -483,6 +483,8 @@ func (s *recordingSettler) RecordGameResult(int64, bool) { func (s *recordingSettler) PlayerProfile(int64) (int64, string) { return 0, "" } func (s *recordingSettler) ChargeTicket(int64) error { return nil } +func (s *recordingSettler) ChargeCoins(int64, int64, string) error { return nil } +func (s *recordingSettler) AwardCoins(int64, int64, string) {} func (s *recordingSettler) ChargePrivateTable(int64) error { return nil } func (s *recordingSettler) PrivateTableInfo(int64) (int, bool) { return 5, false }