diff --git a/internal/ws/hub.go b/internal/ws/hub.go index 5e81633..e8cda21 100644 --- a/internal/ws/hub.go +++ b/internal/ws/hub.go @@ -12,10 +12,11 @@ import ( // مقادیر پیش‌فرض مقاوم‌سازی (قابل بازنویسی در تست). const ( - defaultTurnTimeout = 20 * time.Second // مهلت نوبت بازیکنِ انسان - defaultBotDelay = 800 * time.Millisecond // تأخیر حرکت بات/قطع‌شده (حس طبیعی + فرصت بازگشت) - defaultMatchWait = 12 * time.Second // مهلت پر شدن میز با انسان پیش از افزودن بات - defaultTrickHold = 1200 * time.Millisecond // مدت نمایش دستِ کامل پیش از جمع‌آوری + defaultTurnTimeout = 30 * time.Second // مهلت نوبت بازیکنِ انسان + defaultBotDelay = 800 * time.Millisecond // تأخیر حرکت بات/قطع‌شده (حس طبیعی + فرصت بازگشت) + defaultMatchWait = 6 * time.Second // مهلت پر شدن میز با انسان پیش از افزودن بات + defaultTrickHold = 1200 * time.Millisecond // مدت نمایش دستِ کامل پیش از جمع‌آوری + defaultFirstHandDelay = 3000 * time.Millisecond // تأخیر اولین حرکتِ بات تا پایانِ اینترو+بُر زدنِ کلاینت ) // AuthFunc توکن را تأیید کرده و (شناسه کاربر، نام نمایشی) را برمی‌گرداند. @@ -69,6 +70,7 @@ type Hub struct { botDelay time.Duration matchWait time.Duration trickHold time.Duration + firstHandDelay time.Duration // تأخیر اولین حرکتِ بات (هماهنگ با انیمیشنِ بُر زدنِ کلاینت) countdownDelay time.Duration // مهلت شمارش معکوسِ میز خصوصی پیش از شروع register chan *Client @@ -98,6 +100,7 @@ func NewHub(auth AuthFunc) *Hub { botDelay: defaultBotDelay, matchWait: defaultMatchWait, trickHold: defaultTrickHold, + firstHandDelay: defaultFirstHandDelay, countdownDelay: 3 * time.Second, register: make(chan *Client), unregister: make(chan *Client), diff --git a/internal/ws/protocol.go b/internal/ws/protocol.go index fdee721..ab57c90 100644 --- a/internal/ws/protocol.go +++ b/internal/ws/protocol.go @@ -76,10 +76,11 @@ type stateMsg struct { HandCounts [4]int `json:"hand_counts"` Trick []trickView `json:"trick"` LeadSuit string `json:"lead_suit,omitempty"` - TricksWon [2]int `json:"tricks_won"` - Scores [2]int `json:"scores"` - TargetScore int `json:"target_score"` - Players []PlayerInfo `json:"players"` + TricksWon [2]int `json:"tricks_won"` + Scores [2]int `json:"scores"` + TargetScore int `json:"target_score"` + TurnTimeoutMs int `json:"turn_timeout_ms"` // مهلتِ نوبتِ انسان (برای شمارنده‌ی کلاینت) + Players []PlayerInfo `json:"players"` } // handOverMsg نتیجه یک هَند. @@ -118,7 +119,7 @@ type playerStatusMsg struct { } // buildState نمای بازی را برای جایگاه seat می‌سازد (کارت بقیه مخفی می‌ماند). -func buildState(g *game.Game, roomID string, seat int, players []PlayerInfo) stateMsg { +func buildState(g *game.Game, roomID string, seat int, players []PlayerInfo, turnTimeoutMs int) stateMsg { hand := g.Hand(seat) handStr := make([]string, len(hand)) for i, c := range hand { diff --git a/internal/ws/room.go b/internal/ws/room.go index da09841..111d8fb 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -54,22 +54,25 @@ type Room struct { actions chan roomAction ended bool turnGen int - timer *time.Timer // تایمر فعال (نوبت یا جمع‌آوری)؛ فقط goroutine میز به آن دست می‌زند - turnTimeout time.Duration - botDelay time.Duration - trickHold time.Duration + timer *time.Timer // تایمر فعال (نوبت یا جمع‌آوری)؛ فقط goroutine میز به آن دست می‌زند + turnTimeout time.Duration + botDelay time.Duration + trickHold time.Duration + firstHandDelay time.Duration + handStart time.Time // زمانِ شروعِ هَندِ جاری (برای پنجره‌ی بُر زدن) } func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room { return &Room{ - ID: id, - hub: hub, - settler: hub.settler, - seats: seats, - actions: make(chan roomAction, 64), - turnTimeout: hub.turnTimeout, - botDelay: hub.botDelay, - trickHold: hub.trickHold, + ID: id, + hub: hub, + settler: hub.settler, + seats: seats, + actions: make(chan roomAction, 64), + turnTimeout: hub.turnTimeout, + botDelay: hub.botDelay, + trickHold: hub.trickHold, + firstHandDelay: hub.firstHandDelay, } } @@ -108,6 +111,7 @@ func (r *Room) start() { target = targetScore } r.game = game.NewGame(target, game.FirstHakem()) + r.handStart = time.Now() for seat, s := range r.seats { if s.client != nil { s.client.trySend(mustJSON(r.matchedFor(seat))) @@ -142,6 +146,7 @@ func (r *Room) react() { r.endRoom() return } + r.handStart = time.Now() // شروعِ هَندِ جدید ⇒ پنجره‌ی بُر زدن r.broadcastState() r.armTimer() case game.PhaseGameOver: @@ -206,6 +211,11 @@ func (r *Room) armTimer() { delay := r.turnTimeout if r.seats[actor].auto() { delay = r.botDelay + // در پنجره‌ی ابتدای هَند (انیمیشنِ بُر زدنِ کلاینت)، حرکتِ خودکار را عقب + // می‌اندازیم تا کارت‌ها وسطِ بُر زدن روی میز نیفتند. نوبتِ انسان دست‌نخورده می‌ماند. + if grace := r.firstHandDelay - time.Since(r.handStart); grace > delay { + delay = grace + } } r.timer = time.AfterFunc(delay, func() { select { @@ -386,7 +396,7 @@ func (r *Room) handleReconnect(seat int, client *Client) { r.broadcast(mustJSON(playerStatusMsg{Type: "player_reconnected", Seat: seat})) // وضعیت کامل را برای بازیکن بازگشته بفرست client.trySend(mustJSON(r.matchedFor(seat))) - client.trySend(mustJSON(buildState(r.game, r.ID, seat, r.playersInfo()))) + client.trySend(mustJSON(buildState(r.game, r.ID, seat, r.playersInfo(), int(r.turnTimeout.Milliseconds())))) r.armTimer() // مهلت نوبت برای انسانِ بازگشته دوباره بلند می‌شود } @@ -476,7 +486,7 @@ func (r *Room) broadcastState() { players := r.playersInfo() for seat, s := range r.seats { if s.client != nil { - s.client.trySend(mustJSON(buildState(r.game, r.ID, seat, players))) + s.client.trySend(mustJSON(buildState(r.game, r.ID, seat, players, int(r.turnTimeout.Milliseconds())))) } } } diff --git a/internal/ws/ws_test.go b/internal/ws/ws_test.go index 5d816d4..c2a83a5 100644 --- a/internal/ws/ws_test.go +++ b/internal/ws/ws_test.go @@ -122,6 +122,7 @@ func testHub() *Hub { hub.botDelay = 8 * time.Millisecond hub.matchWait = 300 * time.Millisecond hub.trickHold = 5 * time.Millisecond + hub.firstHandDelay = 0 hub.countdownDelay = 20 * time.Millisecond go hub.Run() return hub