feat: add private and profile
This commit is contained in:
+200
-10
@@ -3,6 +3,7 @@ package ws
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -39,6 +40,15 @@ type endInfo struct {
|
||||
clients []*Client
|
||||
}
|
||||
|
||||
// pendingTable میز خصوصیِ در انتظارِ شروع (دورهمی).
|
||||
// فقط توسط goroutine هاب خوانده/نوشته میشود.
|
||||
type pendingTable struct {
|
||||
code string
|
||||
host int64
|
||||
clients []*Client
|
||||
started bool // پس از فشردن «شروع» توسط میزبان
|
||||
}
|
||||
|
||||
// Hub هماهنگکننده مرکزی: اتصالها، صف matchmaking و مسیریابی پیامها.
|
||||
// تمام state آن فقط توسط goroutine Run تغییر میکند (single-writer، بدون قفل).
|
||||
// پس از ساخت یک میز، هاب دیگر به seatInfo دست نمیزند (مالک آن goroutine میز است).
|
||||
@@ -47,23 +57,26 @@ type Hub struct {
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
clients map[*Client]bool
|
||||
queues map[string][]*Client // صف انتظار به ازای هر tier
|
||||
locations map[int64]location // userID → محل بازی (برای reconnect)
|
||||
queues map[string][]*Client // صف انتظار به ازای هر tier
|
||||
locations map[int64]location // userID → محل بازی (برای reconnect)
|
||||
tables map[string]*pendingTable // کد میز → میز خصوصیِ در انتظار
|
||||
roomSeq int
|
||||
botSeq int
|
||||
fillPending map[string]bool
|
||||
settler Settler
|
||||
|
||||
turnTimeout time.Duration
|
||||
botDelay time.Duration
|
||||
matchWait time.Duration
|
||||
trickHold time.Duration
|
||||
turnTimeout time.Duration
|
||||
botDelay time.Duration
|
||||
matchWait time.Duration
|
||||
trickHold time.Duration
|
||||
countdownDelay time.Duration // مهلت شمارش معکوسِ میز خصوصی پیش از شروع
|
||||
|
||||
register chan *Client
|
||||
unregister chan *Client
|
||||
inbound chan inbound
|
||||
endRoom chan endInfo
|
||||
fill chan string // tier برای پر کردن با بات
|
||||
startTbl chan string // کد میز خصوصی برای شروع (پس از شمارش معکوس)
|
||||
}
|
||||
|
||||
func NewHub(auth AuthFunc) *Hub {
|
||||
@@ -78,17 +91,20 @@ func NewHub(auth AuthFunc) *Hub {
|
||||
clients: make(map[*Client]bool),
|
||||
queues: make(map[string][]*Client),
|
||||
locations: make(map[int64]location),
|
||||
tables: make(map[string]*pendingTable),
|
||||
fillPending: make(map[string]bool),
|
||||
settler: noopSettler{},
|
||||
turnTimeout: defaultTurnTimeout,
|
||||
botDelay: defaultBotDelay,
|
||||
matchWait: defaultMatchWait,
|
||||
trickHold: defaultTrickHold,
|
||||
turnTimeout: defaultTurnTimeout,
|
||||
botDelay: defaultBotDelay,
|
||||
matchWait: defaultMatchWait,
|
||||
trickHold: defaultTrickHold,
|
||||
countdownDelay: 3 * time.Second,
|
||||
register: make(chan *Client),
|
||||
unregister: make(chan *Client),
|
||||
inbound: make(chan inbound, 64),
|
||||
endRoom: make(chan endInfo),
|
||||
fill: make(chan string, 8),
|
||||
startTbl: make(chan string, 8),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +127,8 @@ func (h *Hub) Run() {
|
||||
h.closeRoom(e)
|
||||
case tier := <-h.fill:
|
||||
h.onFill(tier)
|
||||
case code := <-h.startTbl:
|
||||
h.onStartTable(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +168,171 @@ func (h *Hub) handle(in inbound) {
|
||||
default:
|
||||
}
|
||||
}
|
||||
case "create_table":
|
||||
h.createTable(c)
|
||||
case "join_table":
|
||||
h.joinTable(c, in.msg.Code)
|
||||
case "start_table":
|
||||
h.startTable(c)
|
||||
case "leave_table":
|
||||
h.leaveTable(c)
|
||||
}
|
||||
}
|
||||
|
||||
// --- میز خصوصی (دورهمی) ---
|
||||
|
||||
// genCode یک کد ۵ رقمیِ یکتا برای میز خصوصی میسازد.
|
||||
func (h *Hub) genCode() string {
|
||||
for {
|
||||
code := fmt.Sprintf("%05d", rand.Intn(100000))
|
||||
if _, ok := h.tables[code]; !ok {
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createTable یک میز خصوصی میسازد (در صورت مجاز بودنِ سهمیه).
|
||||
func (h *Hub) createTable(c *Client) {
|
||||
if c.room != nil || c.tableCode != "" {
|
||||
return
|
||||
}
|
||||
if err := h.settler.ChargePrivateTable(c.UserID); err != nil {
|
||||
c.trySend(mustJSON(errorMsg{Type: "error", Message: "سهمیهی میزهای رایگان به پایان رسیده؛ برای میز نامحدود VIP بگیرید"}))
|
||||
return
|
||||
}
|
||||
code := h.genCode()
|
||||
t := &pendingTable{code: code, host: c.UserID, clients: []*Client{c}}
|
||||
h.tables[code] = t
|
||||
c.tableCode = code
|
||||
slog.Info("private table created", "code", code, "host", c.UserID)
|
||||
h.broadcastLobby(t)
|
||||
}
|
||||
|
||||
// joinTable کلاینت را به میز خصوصیِ موجود میافزاید.
|
||||
func (h *Hub) joinTable(c *Client, code string) {
|
||||
if c.room != nil || c.tableCode != "" {
|
||||
return
|
||||
}
|
||||
t := h.tables[code]
|
||||
if t == nil || t.started {
|
||||
c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز یافت نشد"}))
|
||||
return
|
||||
}
|
||||
if len(t.clients) >= 4 {
|
||||
c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز پر است"}))
|
||||
return
|
||||
}
|
||||
t.clients = append(t.clients, c)
|
||||
c.tableCode = code
|
||||
h.broadcastLobby(t)
|
||||
}
|
||||
|
||||
// startTable شمارش معکوس را آغاز و پس از آن بازی را شروع میکند (فقط میزبان).
|
||||
func (h *Hub) startTable(c *Client) {
|
||||
t := h.tables[c.tableCode]
|
||||
if t == nil || t.host != c.UserID || t.started {
|
||||
return
|
||||
}
|
||||
t.started = true
|
||||
cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3})
|
||||
for _, cl := range t.clients {
|
||||
cl.trySend(cd)
|
||||
}
|
||||
code := t.code
|
||||
time.AfterFunc(h.countdownDelay, func() {
|
||||
select {
|
||||
case h.startTbl <- code:
|
||||
default:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// onStartTable پس از شمارش معکوس، میز را از کلاینتهای متصل میسازد و بازی را شروع میکند.
|
||||
func (h *Hub) onStartTable(code string) {
|
||||
t := h.tables[code]
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
delete(h.tables, code)
|
||||
|
||||
var seats [4]*seatInfo
|
||||
n := 0
|
||||
for _, cl := range t.clients {
|
||||
if n >= 4 || !h.clients[cl] {
|
||||
continue // قطعشدهها نادیده گرفته میشوند
|
||||
}
|
||||
seats[n] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true}
|
||||
n++
|
||||
}
|
||||
if n == 0 {
|
||||
return // همه خارج شدند
|
||||
}
|
||||
for i := n; i < 4; i++ {
|
||||
h.botSeq++
|
||||
seats[i] = &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.private = true
|
||||
for i := 0; i < n; i++ {
|
||||
cl := seats[i].client
|
||||
cl.room = room
|
||||
cl.seat = i
|
||||
cl.tableCode = ""
|
||||
h.locations[cl.UserID] = location{room: room, seat: i}
|
||||
}
|
||||
go room.run()
|
||||
slog.Info("private room started", "room", room.ID, "code", code, "humans", n)
|
||||
}
|
||||
|
||||
// leaveTable خروجِ داوطلبانه از اتاق انتظار.
|
||||
func (h *Hub) leaveTable(c *Client) {
|
||||
t := h.tables[c.tableCode]
|
||||
c.tableCode = ""
|
||||
if t != nil {
|
||||
h.removeFromTable(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
// removeFromTable کلاینت را از میز حذف میکند؛ با خروجِ میزبان یا خالیشدن، میز منحل میشود.
|
||||
func (h *Hub) removeFromTable(t *pendingTable, c *Client) {
|
||||
idx := -1
|
||||
for i, cl := range t.clients {
|
||||
if cl == c {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
t.clients = append(t.clients[:idx], t.clients[idx+1:]...)
|
||||
if c.UserID == t.host || len(t.clients) == 0 {
|
||||
closed := mustJSON(tableClosedMsg{Type: "table_closed", Reason: "host_left"})
|
||||
for _, cl := range t.clients {
|
||||
cl.tableCode = ""
|
||||
cl.trySend(closed)
|
||||
}
|
||||
delete(h.tables, t.code)
|
||||
return
|
||||
}
|
||||
h.broadcastLobby(t)
|
||||
}
|
||||
|
||||
// broadcastLobby وضعیت اتاق انتظار را برای همهی اعضای میز ارسال میکند.
|
||||
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}
|
||||
}
|
||||
for _, 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +431,13 @@ func (h *Hub) handleDisconnect(c *Client) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// اگر در اتاق انتظارِ میز خصوصی بود، از آن حذف شود.
|
||||
if c.tableCode != "" {
|
||||
if t := h.tables[c.tableCode]; t != nil {
|
||||
h.removeFromTable(t, c)
|
||||
}
|
||||
c.tableCode = ""
|
||||
}
|
||||
if c.room != nil {
|
||||
select {
|
||||
case c.room.actions <- roomAction{kind: akDisconnect, seat: c.seat, client: c}:
|
||||
|
||||
Reference in New Issue
Block a user