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
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++ {
for s := 0; s < 4; s++ {
if seats[s] == nil {
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++
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 {
+4
View File
@@ -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"`
}
+4
View File
@@ -48,6 +48,7 @@ type Room struct {
hub *Hub
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
}