feat: add coin for private table

This commit is contained in:
2026-07-04 12:45:07 +03:30
parent 413fca68cc
commit 63716576ca
6 changed files with 104 additions and 8 deletions
+18
View File
@@ -87,6 +87,24 @@ func (g gameSettler) ChargeTicket(userID int64) error {
return g.eco.Adjust(context.Background(), userID, economy.CurrencyTicket, -1, "reshuffle", "") 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 یک میز خصوصیِ رایگان را برای کاربر مصرف می‌کند. // ChargePrivateTable یک میز خصوصیِ رایگان را برای کاربر مصرف می‌کند.
func (g gameSettler) ChargePrivateTable(userID int64) error { func (g gameSettler) ChargePrivateTable(userID int64) error {
return g.eco.ConsumePrivateTable(context.Background(), userID) return g.eco.ConsumePrivateTable(context.Background(), userID)
+26 -3
View File
@@ -50,6 +50,7 @@ type pendingTable struct {
clients []*Client clients []*Client
hands int // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷) hands int // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷)
rot int // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲) rot int // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲)
stake int // شرطِ سکه‌ایِ هر بازیکن (۰ یعنی رایگان)
started bool // پس از فشردن «شروع» توسط میزبان started bool // پس از فشردن «شروع» توسط میزبان
} }
@@ -188,7 +189,9 @@ func (h *Hub) handle(in inbound) {
case "join_table": case "join_table":
h.joinTable(c, in.msg.Code) h.joinTable(c, in.msg.Code)
case "start_table": 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": case "rotate_table":
h.rotateTable(c, in.msg.Dir) h.rotateTable(c, in.msg.Dir)
case "leave_table": case "leave_table":
@@ -245,12 +248,15 @@ func (h *Hub) joinTable(c *Client, code string) {
} }
// startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان). // startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان).
func (h *Hub) startTable(c *Client, hands int) { func (h *Hub) startTable(c *Client, hands, stake int) {
t := h.tables[c.tableCode] t := h.tables[c.tableCode]
if t == nil || t.host != c.UserID || t.started { if t == nil || t.host != c.UserID || t.started {
return return
} }
t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷) t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷)
if stake >= 0 {
t.stake = stake // آخرین شرطِ سکه‌ای (اگر همراهِ شروع فرستاده شد)
}
t.started = true t.started = true
cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3}) cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3})
for _, cl := range t.clients { for _, cl := range t.clients {
@@ -298,6 +304,7 @@ func (h *Hub) onStartTable(code string) {
room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی) room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی)
room.private = true room.private = true
room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان
room.stake = int64(t.stake) // شرطِ سکه‌ایِ هر بازیکن
for s := 0; s < 4; s++ { for s := 0; s < 4; s++ {
if seats[s].isBot { if seats[s].isBot {
continue continue
@@ -312,6 +319,22 @@ func (h *Hub) onStartTable(code string) {
slog.Info("private room started", "room", room.ID, "code", code, "humans", humans) 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 جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز می‌چرخاند (فقط میزبان). // rotateTable جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز می‌چرخاند (فقط میزبان).
// میزبان همیشه جایگاهِ اولش را حفظ می‌کند؛ چرخش تیم‌بندی را تغییر می‌دهد. // میزبان همیشه جایگاهِ اولش را حفظ می‌کند؛ چرخش تیم‌بندی را تغییر می‌دهد.
func (h *Hub) rotateTable(c *Client, dir int) { func (h *Hub) rotateTable(c *Client, dir int) {
@@ -379,7 +402,7 @@ func (h *Hub) broadcastLobby(t *pendingTable) {
cl.trySend(mustJSON(tableLobbyMsg{ cl.trySend(mustJSON(tableLobbyMsg{
Type: "table_lobby", Code: t.code, Players: players, Type: "table_lobby", Code: t.code, Players: players,
Host: cl.UserID == t.host, YouSeat: t.seatOf(i), Host: cl.UserID == t.host, YouSeat: t.seatOf(i),
Remaining: remaining, Unlimited: unlimited, Remaining: remaining, Unlimited: unlimited, Stake: t.stake,
})) }))
} }
} }
+2
View File
@@ -14,6 +14,7 @@ type inboundMsg struct {
Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاه‌ها (+۱/−۱) Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاه‌ها (+۱/−۱)
Text string `json:"text"` // برای chat: متنِ پیامِ آماده Text string `json:"text"` // برای chat: متنِ پیامِ آماده
Emoji string `json:"emoji"` // برای chat: شناسه/کاراکترِ شکلک Emoji string `json:"emoji"` // برای chat: شناسه/کاراکترِ شکلک
Stake int `json:"stake"` // برای set_stake/start_table: شرطِ سکه‌ایِ میز
} }
// chatMsg پیامِ چت/شکلکِ یک بازیکن که برای همه‌ی میز پخش می‌شود. // chatMsg پیامِ چت/شکلکِ یک بازیکن که برای همه‌ی میز پخش می‌شود.
@@ -40,6 +41,7 @@ type tableLobbyMsg struct {
YouSeat int `json:"you_seat"` // جایگاهِ مطلقِ گیرنده (برای چیدمانِ صحنه) YouSeat int `json:"you_seat"` // جایگاهِ مطلقِ گیرنده (برای چیدمانِ صحنه)
Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده
Unlimited bool `json:"unlimited"` Unlimited bool `json:"unlimited"`
Stake int `json:"stake"` // شرطِ سکه‌ایِ فعلیِ میز (تنظیمِ میزبان)
} }
// countdownMsg شمارش معکوس پیش از شروع بازیِ خصوصی. // countdownMsg شمارش معکوس پیش از شروع بازیِ خصوصی.
+49 -4
View File
@@ -49,8 +49,11 @@ type Room struct {
ID string ID string
hub *Hub hub *Hub
tier string tier string
private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود) private bool // میز دورهمی: بدون ورودی/جایزه‌ی tier (فقط آمار و شرطِ سکه)
handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier) handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier)
stake int64 // شرطِ سکه‌ایِ هر بازیکن در میز خصوصی (۰ یعنی رایگان)
pot int64 // مجموعِ سکه‌های جمع‌آوری‌شده برای برنده
paid [4]bool // جایگاه‌هایی که شرط را پرداخته‌اند (برای بازگشت هنگام لغو)
settler Settler settler Settler
seats [4]*seatInfo seats [4]*seatInfo
game *game.Game game *game.Game
@@ -122,6 +125,19 @@ func (r *Room) start() {
} }
r.game = game.NewGame(target, game.FirstHakem()) r.game = game.NewGame(target, game.FirstHakem())
r.graceUntil = time.Now().Add(r.firstHandDelay) // بُر زدن + پخشِ ۵ کارت 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 { for _, s := range r.seats {
if s.isBot || s.userID == 0 { if s.isBot || s.userID == 0 {
@@ -462,26 +478,55 @@ func (r *Room) endRoom() {
// settleGameOver جایزه را به برندگانِ انسان می‌دهد و بازی را ثبت می‌کند. // settleGameOver جایزه را به برندگانِ انسان می‌دهد و بازی را ثبت می‌کند.
func (r *Room) settleGameOver(res *game.HandResult) { func (r *Room) settleGameOver(res *game.HandResult) {
// برندگانِ انسان (برای تقسیمِ potِ میزِ خصوصی).
var winners []int64
for seat, s := range r.seats { for seat, s := range r.seats {
if s.isBot || s.userID == 0 { if s.isBot || s.userID == 0 {
continue continue
} }
won := game.Team(seat) == res.WinnerTeam won := game.Team(seat) == res.WinnerTeam
if won && !r.private { if won {
r.settler.AwardWinner(s.userID, r.tier) // میز دورهمی جایزه‌ی سکه ندارد winners = append(winners, s.userID)
if !r.private {
r.settler.AwardWinner(s.userID, r.tier)
}
} }
if !r.private { if !r.private {
r.settler.AwardRank(s.userID, r.tier, won) // امتیازِ رتبه (اگر میزِ رتبه‌بندی باشد) r.settler.AwardRank(s.userID, r.tier, won) // امتیازِ رتبه (اگر میزِ رتبه‌بندی باشد)
} }
r.settler.RecordGameResult(s.userID, 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) r.settler.RecordGame(r.ID, r.playersJSON(res.WinnerTeam), res.WinnerTeam, res.Kot)
} }
// refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشده‌اند بازمی‌گرداند. // refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشده‌اند بازمی‌گرداند.
func (r *Room) refundAll() { func (r *Room) refundAll() {
if r.private { 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 { for _, s := range r.seats {
if s.isBot || s.left || s.userID == 0 { if s.isBot || s.left || s.userID == 0 {
+6
View File
@@ -16,6 +16,10 @@ type Settler interface {
PlayerProfile(userID int64) (coins int64, rankTier string) PlayerProfile(userID int64) (coins int64, rankTier string)
// ChargeTicket یک بلیت برای بُرِ مجدد در فازِ انتخابِ حکم مصرف می‌کند. // ChargeTicket یک بلیت برای بُرِ مجدد در فازِ انتخابِ حکم مصرف می‌کند.
ChargeTicket(userID int64) error 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 // مصرفِ یک میز رایگان (خطا اگر سقف پر باشد) ChargePrivateTable(userID int64) error // مصرفِ یک میز رایگان (خطا اگر سقف پر باشد)
PrivateTableInfo(userID int64) (remaining int, unlimited bool) PrivateTableInfo(userID int64) (remaining int, unlimited bool)
@@ -36,6 +40,8 @@ func (noopSettler) RecordCut(int64) {}
func (noopSettler) RecordGameResult(int64, bool) {} func (noopSettler) RecordGameResult(int64, bool) {}
func (noopSettler) PlayerProfile(int64) (int64, string) { return 0, "" } func (noopSettler) PlayerProfile(int64) (int64, string) { return 0, "" }
func (noopSettler) ChargeTicket(int64) error { return nil } 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) ChargePrivateTable(int64) error { return nil }
func (noopSettler) PrivateTableInfo(int64) (int, bool) { return 0, true } func (noopSettler) PrivateTableInfo(int64) (int, bool) { return 0, true }
func (noopSettler) TargetHands(string) int { return 7 } func (noopSettler) TargetHands(string) int { return 7 }
+2
View File
@@ -483,6 +483,8 @@ func (s *recordingSettler) RecordGameResult(int64, bool) {
func (s *recordingSettler) PlayerProfile(int64) (int64, string) { return 0, "" } func (s *recordingSettler) PlayerProfile(int64) (int64, string) { return 0, "" }
func (s *recordingSettler) ChargeTicket(int64) error { return nil } 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) ChargePrivateTable(int64) error { return nil }
func (s *recordingSettler) PrivateTableInfo(int64) (int, bool) { return 5, false } func (s *recordingSettler) PrivateTableInfo(int64) (int, bool) { return 5, false }