From 60494946d640197999b5fbd08a8767a5e88f57cb Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Fri, 19 Jun 2026 23:19:34 +0330 Subject: [PATCH] feat: add rotate private table --- internal/ws/hub.go | 104 ++++++++++++++++++++++++++++++++-------- internal/ws/protocol.go | 4 ++ internal/ws/room.go | 8 +++- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/internal/ws/hub.go b/internal/ws/hub.go index 707a2d5..06b10d6 100644 --- a/internal/ws/hub.go +++ b/internal/ws/hub.go @@ -48,9 +48,18 @@ type pendingTable struct { code string host int64 clients []*Client + hands int // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷) + rot int // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲) 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 و مسیریابی پیام‌ها. // تمام state آن فقط توسط goroutine Run تغییر می‌کند (single-writer، بدون قفل). // پس از ساخت یک میز، هاب دیگر به seatInfo دست نمی‌زند (مالک آن goroutine میز است). @@ -179,7 +188,9 @@ func (h *Hub) handle(in inbound) { case "join_table": h.joinTable(c, in.msg.Code) case "start_table": - h.startTable(c) + h.startTable(c, in.msg.Hands) + case "rotate_table": + h.rotateTable(c, in.msg.Dir) case "leave_table": h.leaveTable(c) } @@ -234,11 +245,12 @@ func (h *Hub) joinTable(c *Client, code string) { } // startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان). -func (h *Hub) startTable(c *Client) { +func (h *Hub) startTable(c *Client, hands int) { t := h.tables[c.tableCode] if t == nil || t.host != c.UserID || t.started { return } + t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷) t.started = true cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3}) for _, cl := range t.clients { @@ -262,35 +274,60 @@ func (h *Hub) onStartTable(code string) { delete(h.tables, code) var seats [4]*seatInfo - n := 0 - for _, cl := range t.clients { - if n >= 4 || !h.clients[cl] { - continue // قطع‌شده‌ها نادیده گرفته می‌شوند + humans := 0 + for i, cl := range t.clients { + if i >= 4 || !h.clients[cl] { + continue // قطع‌شده‌ها نادیده گرفته می‌شوند (جایشان را بات می‌گیرد) } - seats[n] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true} - n++ + seat := t.seatOf(i) + seats[seat] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true} + humans++ } - if n == 0 { + if humans == 0 { return // همه خارج شدند } - for i := n; i < 4; i++ { - h.botSeq++ - seats[i] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)} + for s := 0; s < 4; s++ { + if seats[s] == nil { + h.botSeq++ + seats[s] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)} + } } h.roomSeq++ room := newRoom(fmt.Sprintf("p%d", h.roomSeq), seats, h) - room.tier = defaultTier // قواعدِ مبتدی برای دورهمی + room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی) room.private = true - for i := 0; i < n; i++ { - cl := seats[i].client + room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان + for s := 0; s < 4; s++ { + if seats[s].isBot { + continue + } + cl := seats[s].client cl.room = room - cl.seat = i + cl.seat = s cl.tableCode = "" - h.locations[cl.UserID] = location{room: room, seat: i} + h.locations[cl.UserID] = location{room: room, seat: s} } 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 خروجِ داوطلبانه از اتاق انتظار. @@ -331,17 +368,42 @@ func (h *Hub) removeFromTable(t *pendingTable, c *Client) { func (h *Hub) broadcastLobby(t *pendingTable) { players := make([]lobbyPlayer, len(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) cl.trySend(mustJSON(tableLobbyMsg{ 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 افزوده و میزهای کامل را می‌سازد. func (h *Hub) enqueue(c *Client, tier string) { if c.room != nil { diff --git a/internal/ws/protocol.go b/internal/ws/protocol.go index ab57c90..d382108 100644 --- a/internal/ws/protocol.go +++ b/internal/ws/protocol.go @@ -10,12 +10,15 @@ type inboundMsg struct { Suit string `json:"suit"` // برای choose_trump: hearts|spades|diamonds|clubs Card string `json:"card"` // برای play_card: مثل "AS" Code string `json:"code"` // برای join_table: شماره میز خصوصی + Hands int `json:"hands"` // برای start_table: تعداد دستِ بازیِ خصوصی (۳/۵/۷) + Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاه‌ها (+۱/−۱) } // lobbyPlayer یک بازیکن در اتاق انتظارِ میز خصوصی. type lobbyPlayer struct { Name string `json:"name"` Host bool `json:"host"` + Seat int `json:"seat"` // جایگاهِ مطلق (۰..۳) } // tableLobbyMsg وضعیت اتاق انتظارِ میز خصوصی (دورهمی). @@ -24,6 +27,7 @@ type tableLobbyMsg struct { Code string `json:"code"` Players []lobbyPlayer `json:"players"` Host bool `json:"host"` // آیا گیرنده میزبان است + YouSeat int `json:"you_seat"` // جایگاهِ مطلقِ گیرنده (برای چیدمانِ صحنه) Remaining int `json:"remaining"` // باقیمانده‌ی میزهای رایگانِ گیرنده Unlimited bool `json:"unlimited"` } diff --git a/internal/ws/room.go b/internal/ws/room.go index 6ce0e3a..34e31c8 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -46,8 +46,9 @@ func (s *seatInfo) auto() bool { return s.isBot || !s.connected } type Room struct { ID string hub *Hub - tier string - private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود) + tier string + private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت می‌شود) + handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier) settler Settler seats [4]*seatInfo game *game.Game @@ -110,6 +111,9 @@ func (r *Room) run() { func (r *Room) start() { // تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین می‌شود. target := r.settler.TargetHands(r.tier) + if r.handsOverride > 0 { + target = r.handsOverride // میز خصوصی: تعداد دستِ انتخابیِ میزبان + } if target <= 0 { target = targetScore }