feat: add rotate private table

This commit is contained in:
2026-06-19 23:19:34 +03:30
parent cf043e2a19
commit 60494946d6
3 changed files with 93 additions and 23 deletions
+82 -20
View File
@@ -48,9 +48,18 @@ type pendingTable struct {
code string code string
host int64 host int64
clients []*Client clients []*Client
hands int // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷)
rot int // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲)
started bool // پس از فشردن «شروع» توسط میزبان started bool // پس از فشردن «شروع» توسط میزبان
} }
// nonHostSeat جایگاهِ مطلقِ j-اُمین بازیکنِ غیرِمیزبان را با چرخشِ rot برمی‌گرداند.
// میزبان همیشه جایگاه ۰ است؛ سه جایگاهِ دیگر {۱،۲،۳} با چرخش جابه‌جا می‌شوند.
func nonHostSeat(j, rot int) int {
seats := [3]int{1, 2, 3}
return seats[((j+rot)%3+3)%3]
}
// Hub هماهنگ‌کننده مرکزی: اتصال‌ها، صف matchmaking و مسیریابی پیام‌ها. // Hub هماهنگ‌کننده مرکزی: اتصال‌ها، صف matchmaking و مسیریابی پیام‌ها.
// تمام state آن فقط توسط goroutine Run تغییر می‌کند (single-writer، بدون قفل). // تمام state آن فقط توسط goroutine Run تغییر می‌کند (single-writer، بدون قفل).
// پس از ساخت یک میز، هاب دیگر به seatInfo دست نمی‌زند (مالک آن goroutine میز است). // پس از ساخت یک میز، هاب دیگر به seatInfo دست نمی‌زند (مالک آن goroutine میز است).
@@ -179,7 +188,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) h.startTable(c, in.msg.Hands)
case "rotate_table":
h.rotateTable(c, in.msg.Dir)
case "leave_table": case "leave_table":
h.leaveTable(c) h.leaveTable(c)
} }
@@ -234,11 +245,12 @@ func (h *Hub) joinTable(c *Client, code string) {
} }
// startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان). // startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان).
func (h *Hub) startTable(c *Client) { func (h *Hub) startTable(c *Client, hands 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.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 {
@@ -262,35 +274,60 @@ func (h *Hub) onStartTable(code string) {
delete(h.tables, code) delete(h.tables, code)
var seats [4]*seatInfo var seats [4]*seatInfo
n := 0 humans := 0
for _, cl := range t.clients { for i, cl := range t.clients {
if n >= 4 || !h.clients[cl] { if i >= 4 || !h.clients[cl] {
continue // قطع‌شده‌ها نادیده گرفته می‌شوند continue // قطع‌شده‌ها نادیده گرفته می‌شوند (جایشان را بات می‌گیرد)
} }
seats[n] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true} seat := t.seatOf(i)
n++ seats[seat] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true}
humans++
} }
if n == 0 { if humans == 0 {
return // همه خارج شدند return // همه خارج شدند
} }
for i := n; i < 4; i++ { for s := 0; s < 4; s++ {
if seats[s] == nil {
h.botSeq++ h.botSeq++
seats[i] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)} seats[s] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)}
}
} }
h.roomSeq++ h.roomSeq++
room := newRoom(fmt.Sprintf("p%d", h.roomSeq), seats, h) room := newRoom(fmt.Sprintf("p%d", h.roomSeq), seats, h)
room.tier = defaultTier // قواعدِ مبتدی برای دورهمی room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی)
room.private = true room.private = true
for i := 0; i < n; i++ { room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان
cl := seats[i].client for s := 0; s < 4; s++ {
if seats[s].isBot {
continue
}
cl := seats[s].client
cl.room = room cl.room = room
cl.seat = i cl.seat = s
cl.tableCode = "" cl.tableCode = ""
h.locations[cl.UserID] = location{room: room, seat: i} h.locations[cl.UserID] = location{room: room, seat: s}
} }
go room.run() go room.run()
slog.Info("private room started", "room", room.ID, "code", code, "humans", n) slog.Info("private room started", "room", room.ID, "code", code, "humans", humans)
}
// rotateTable جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز می‌چرخاند (فقط میزبان).
// میزبان همیشه جایگاهِ اولش را حفظ می‌کند؛ چرخش تیم‌بندی را تغییر می‌دهد.
func (h *Hub) rotateTable(c *Client, dir int) {
t := h.tables[c.tableCode]
if t == nil || t.host != c.UserID || t.started {
return
}
if len(t.clients) < 2 {
return // جز میزبان کسی نیست ⇒ چیزی برای چرخش نمانده
}
d := 1
if dir < 0 {
d = -1
}
t.rot = ((t.rot+d)%3 + 3) % 3
h.broadcastLobby(t)
} }
// leaveTable خروجِ داوطلبانه از اتاق انتظار. // leaveTable خروجِ داوطلبانه از اتاق انتظار.
@@ -331,17 +368,42 @@ func (h *Hub) removeFromTable(t *pendingTable, c *Client) {
func (h *Hub) broadcastLobby(t *pendingTable) { func (h *Hub) broadcastLobby(t *pendingTable) {
players := make([]lobbyPlayer, len(t.clients)) players := make([]lobbyPlayer, len(t.clients))
for i, cl := range t.clients { for i, cl := range t.clients {
players[i] = lobbyPlayer{Name: cl.Name, Host: cl.UserID == t.host} players[i] = lobbyPlayer{
Name: cl.Name,
Host: cl.UserID == t.host,
Seat: t.seatOf(i),
} }
for _, cl := range t.clients { }
for i, cl := range t.clients {
remaining, unlimited := h.settler.PrivateTableInfo(cl.UserID) remaining, unlimited := h.settler.PrivateTableInfo(cl.UserID)
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, Remaining: remaining, Unlimited: unlimited, Host: cl.UserID == t.host, YouSeat: t.seatOf(i),
Remaining: remaining, Unlimited: unlimited,
})) }))
} }
} }
// seatOf جایگاهِ مطلقِ کلاینتِ شماره i در فهرستِ میز را برمی‌گرداند (میزبان=۰).
func (t *pendingTable) seatOf(i int) int {
if i == 0 {
return 0
}
return nonHostSeat(i-1, t.rot)
}
// clampHands تعداد دست را به یکی از مقادیرِ مجاز (۳/۵/۷) محدود می‌کند.
func clampHands(h int) int {
switch h {
case 5:
return 5
case 7:
return 7
default:
return 3
}
}
// enqueue کلاینت را پس از کسر ورودی به صفِ tier افزوده و میزهای کامل را می‌سازد. // enqueue کلاینت را پس از کسر ورودی به صفِ tier افزوده و میزهای کامل را می‌سازد.
func (h *Hub) enqueue(c *Client, tier string) { func (h *Hub) enqueue(c *Client, tier string) {
if c.room != nil { if c.room != nil {
+4
View File
@@ -10,12 +10,15 @@ type inboundMsg struct {
Suit string `json:"suit"` // برای choose_trump: hearts|spades|diamonds|clubs Suit string `json:"suit"` // برای choose_trump: hearts|spades|diamonds|clubs
Card string `json:"card"` // برای play_card: مثل "AS" Card string `json:"card"` // برای play_card: مثل "AS"
Code string `json:"code"` // برای join_table: شماره میز خصوصی Code string `json:"code"` // برای join_table: شماره میز خصوصی
Hands int `json:"hands"` // برای start_table: تعداد دستِ بازیِ خصوصی (۳/۵/۷)
Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاه‌ها (+۱/−۱)
} }
// lobbyPlayer یک بازیکن در اتاق انتظارِ میز خصوصی. // lobbyPlayer یک بازیکن در اتاق انتظارِ میز خصوصی.
type lobbyPlayer struct { type lobbyPlayer struct {
Name string `json:"name"` Name string `json:"name"`
Host bool `json:"host"` Host bool `json:"host"`
Seat int `json:"seat"` // جایگاهِ مطلق (۰..۳)
} }
// tableLobbyMsg وضعیت اتاق انتظارِ میز خصوصی (دورهمی). // tableLobbyMsg وضعیت اتاق انتظارِ میز خصوصی (دورهمی).
@@ -24,6 +27,7 @@ type tableLobbyMsg struct {
Code string `json:"code"` Code string `json:"code"`
Players []lobbyPlayer `json:"players"` Players []lobbyPlayer `json:"players"`
Host bool `json:"host"` // آیا گیرنده میزبان است Host bool `json:"host"` // آیا گیرنده میزبان است
YouSeat int `json:"you_seat"` // جایگاهِ مطلقِ گیرنده (برای چیدمانِ صحنه)
Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده
Unlimited bool `json:"unlimited"` Unlimited bool `json:"unlimited"`
} }
+4
View File
@@ -48,6 +48,7 @@ type Room struct {
hub *Hub hub *Hub
tier string tier string
private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود) private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود)
handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier)
settler Settler settler Settler
seats [4]*seatInfo seats [4]*seatInfo
game *game.Game game *game.Game
@@ -110,6 +111,9 @@ func (r *Room) run() {
func (r *Room) start() { func (r *Room) start() {
// تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین می‌شود. // تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین می‌شود.
target := r.settler.TargetHands(r.tier) target := r.settler.TargetHands(r.tier)
if r.handsOverride > 0 {
target = r.handsOverride // میز خصوصی: تعداد دستِ انتخابیِ میزبان
}
if target <= 0 { if target <= 0 {
target = targetScore target = targetScore
} }