feat: add private and profile
This commit is contained in:
@@ -31,9 +31,10 @@ type Client struct {
|
||||
closeOnce sync.Once
|
||||
|
||||
// توسط goroutine هاب ست میشوند (تکنویسنده) و فقط توسط آن خوانده میشوند.
|
||||
room *Room
|
||||
seat int
|
||||
tier string // نوع میزی که در صفش است (برای تسویه)
|
||||
room *Room
|
||||
seat int
|
||||
tier string // نوع میزی که در صفش است (برای تسویه)
|
||||
tableCode string // کدِ میز خصوصیِ در انتظار (پیش از شروع بازی)
|
||||
}
|
||||
|
||||
// close اتصال را یکبار بهصورت امن میبندد.
|
||||
|
||||
+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}:
|
||||
|
||||
+30
-1
@@ -4,11 +4,40 @@ import "hakemsho/internal/game"
|
||||
|
||||
// inboundMsg پیام دریافتی از کلاینت.
|
||||
type inboundMsg struct {
|
||||
Type string `json:"type"` // join_queue | choose_trump | play_card | leave
|
||||
Type string `json:"type"` // join_queue | choose_trump | play_card | leave | create_table | join_table | start_table | leave_table
|
||||
Mode string `json:"mode"` // برای join_queue
|
||||
Tier string `json:"tier"` // برای join_queue: نوع میز (beginner/pro/...)
|
||||
Suit string `json:"suit"` // برای choose_trump: hearts|spades|diamonds|clubs
|
||||
Card string `json:"card"` // برای play_card: مثل "AS"
|
||||
Code string `json:"code"` // برای join_table: شماره میز خصوصی
|
||||
}
|
||||
|
||||
// lobbyPlayer یک بازیکن در اتاق انتظارِ میز خصوصی.
|
||||
type lobbyPlayer struct {
|
||||
Name string `json:"name"`
|
||||
Host bool `json:"host"`
|
||||
}
|
||||
|
||||
// tableLobbyMsg وضعیت اتاق انتظارِ میز خصوصی (دورهمی).
|
||||
type tableLobbyMsg struct {
|
||||
Type string `json:"type"` // "table_lobby"
|
||||
Code string `json:"code"`
|
||||
Players []lobbyPlayer `json:"players"`
|
||||
Host bool `json:"host"` // آیا گیرنده میزبان است
|
||||
Remaining int `json:"remaining"` // باقیماندهی میزهای رایگانِ گیرنده
|
||||
Unlimited bool `json:"unlimited"`
|
||||
}
|
||||
|
||||
// countdownMsg شمارش معکوس پیش از شروع بازیِ خصوصی.
|
||||
type countdownMsg struct {
|
||||
Type string `json:"type"` // "countdown"
|
||||
Seconds int `json:"seconds"`
|
||||
}
|
||||
|
||||
// tableClosedMsg انحلالِ میز خصوصی پیش از شروع (میزبان خارج شد).
|
||||
type tableClosedMsg struct {
|
||||
Type string `json:"type"` // "table_closed"
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// PlayerInfo اطلاعات عمومی یک بازیکن سر میز.
|
||||
|
||||
+42
-4
@@ -47,6 +47,7 @@ type Room struct {
|
||||
ID string
|
||||
hub *Hub
|
||||
tier string
|
||||
private bool // میز دورهمی: بدون ورودی/جایزه (فقط آمار ثبت میشود)
|
||||
settler Settler
|
||||
seats [4]*seatInfo
|
||||
game *game.Game
|
||||
@@ -130,6 +131,7 @@ func (r *Room) react() {
|
||||
switch r.game.Phase {
|
||||
case game.PhaseHandOver:
|
||||
res := r.game.LastResult
|
||||
r.recordHandStats(res)
|
||||
r.broadcast(mustJSON(handOverMsg{
|
||||
Type: "hand_over", WinnerTeam: res.WinnerTeam, Kot: res.Kot,
|
||||
HakemKot: res.HakemKot, Points: res.Points, Scores: r.game.Scores,
|
||||
@@ -144,6 +146,7 @@ func (r *Room) react() {
|
||||
r.armTimer()
|
||||
case game.PhaseGameOver:
|
||||
res := r.game.LastResult
|
||||
r.recordHandStats(res)
|
||||
r.broadcast(mustJSON(gameOverMsg{
|
||||
Type: "game_over", WinnerTeam: res.WinnerTeam, Scores: r.game.Scores,
|
||||
}))
|
||||
@@ -238,7 +241,7 @@ func (r *Room) handleInput(act roomAction) {
|
||||
r.sendError(act.seat, "invalid card")
|
||||
return
|
||||
}
|
||||
if err := r.game.PlayCard(act.seat, card); err != nil {
|
||||
if err := r.playCard(act.seat, card); err != nil {
|
||||
r.sendError(act.seat, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -307,7 +310,37 @@ func (r *Room) autoPlayCard(seat int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = r.game.PlayCard(seat, choice)
|
||||
_ = r.playCard(seat, choice)
|
||||
}
|
||||
|
||||
// playCard کارت را بازی کرده و در صورت بُرِش با حکم، آن را برای آمار ثبت میکند.
|
||||
func (r *Room) playCard(seat int, card game.Card) error {
|
||||
wasFirst := len(r.game.Trick) == 0
|
||||
if err := r.game.PlayCard(seat, card); err != nil {
|
||||
return err
|
||||
}
|
||||
// بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده
|
||||
// ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است.
|
||||
if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump {
|
||||
if s := r.seats[seat]; !s.isBot && s.userID != 0 {
|
||||
r.settler.RecordCut(s.userID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordHandStats آمار هر بازیکنِ انسان را برای هَندِ پایانیافته ثبت میکند.
|
||||
func (r *Room) recordHandStats(res *game.HandResult) {
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
for seat, s := range r.seats {
|
||||
if s.isBot || s.userID == 0 {
|
||||
continue
|
||||
}
|
||||
won := game.Team(seat) == res.WinnerTeam
|
||||
r.settler.RecordHand(s.userID, won, res.Kot, seat == res.Hakem)
|
||||
}
|
||||
}
|
||||
|
||||
// handleLeave خروج دائمی یک بازیکن؛ جایگاهش به بات تبدیل میشود.
|
||||
@@ -383,15 +416,20 @@ func (r *Room) settleGameOver(res *game.HandResult) {
|
||||
if s.isBot || s.userID == 0 {
|
||||
continue
|
||||
}
|
||||
if game.Team(seat) == res.WinnerTeam {
|
||||
r.settler.AwardWinner(s.userID, r.tier)
|
||||
won := game.Team(seat) == res.WinnerTeam
|
||||
if won && !r.private {
|
||||
r.settler.AwardWinner(s.userID, r.tier) // میز دورهمی جایزهی سکه ندارد
|
||||
}
|
||||
r.settler.RecordGameResult(s.userID, won)
|
||||
}
|
||||
r.settler.RecordGame(r.ID, r.playersJSON(res.WinnerTeam), res.WinnerTeam, res.Kot)
|
||||
}
|
||||
|
||||
// refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشدهاند بازمیگرداند.
|
||||
func (r *Room) refundAll() {
|
||||
if r.private {
|
||||
return // میز دورهمی ورودی نگرفته، پس بازگشتی هم ندارد
|
||||
}
|
||||
for _, s := range r.seats {
|
||||
if s.isBot || s.left || s.userID == 0 {
|
||||
continue
|
||||
|
||||
@@ -7,6 +7,13 @@ type Settler interface {
|
||||
Refund(userID int64, tier string) // بازگرداندن ورودی در صورت لغو
|
||||
AwardWinner(userID int64, tier string) // جایزه/XP/جام به برنده
|
||||
RecordGame(room, playersJSON string, winnerTeam int, kot bool)
|
||||
// آمار پروفایل (همگی غیرحیاتی و fire-and-forget).
|
||||
RecordHand(userID int64, won, kot, asHakem bool) // پایان یک هَند
|
||||
RecordCut(userID int64) // بُرِش با حکم
|
||||
RecordGameResult(userID int64, won bool) // پایان بازی (برد/باخت)
|
||||
// میز خصوصی (دورهمی).
|
||||
ChargePrivateTable(userID int64) error // مصرفِ یک میز رایگان (خطا اگر سقف پر باشد)
|
||||
PrivateTableInfo(userID int64) (remaining int, unlimited bool)
|
||||
// TargetHands تعداد هَندِ لازم برای برد بازی در این tier (مثلاً مبتدی=۳).
|
||||
TargetHands(tier string) int
|
||||
}
|
||||
@@ -18,4 +25,9 @@ func (noopSettler) ChargeEntry(int64, string) error { return nil }
|
||||
func (noopSettler) Refund(int64, string) {}
|
||||
func (noopSettler) AwardWinner(int64, string) {}
|
||||
func (noopSettler) RecordGame(string, string, int, bool) {}
|
||||
func (noopSettler) RecordHand(int64, bool, bool, bool) {}
|
||||
func (noopSettler) RecordCut(int64) {}
|
||||
func (noopSettler) RecordGameResult(int64, bool) {}
|
||||
func (noopSettler) ChargePrivateTable(int64) error { return nil }
|
||||
func (noopSettler) PrivateTableInfo(int64) (int, bool) { return 0, true }
|
||||
func (noopSettler) TargetHands(string) int { return 7 }
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,8 @@ type anyMsg struct {
|
||||
LeadSuit string `json:"lead_suit"`
|
||||
TrickDone bool `json:"trick_done"`
|
||||
Players []PlayerInfo `json:"players"`
|
||||
Code string `json:"code"` // table_lobby
|
||||
Host bool `json:"host"` // table_lobby
|
||||
}
|
||||
|
||||
// TestFourPlayersFullGame یک بازی کامل را روی WebSocket واقعی با ۴ ربات اجرا میکند.
|
||||
@@ -119,10 +122,117 @@ func testHub() *Hub {
|
||||
hub.botDelay = 8 * time.Millisecond
|
||||
hub.matchWait = 300 * time.Millisecond
|
||||
hub.trickHold = 5 * time.Millisecond
|
||||
hub.countdownDelay = 20 * time.Millisecond
|
||||
go hub.Run()
|
||||
return hub
|
||||
}
|
||||
|
||||
// TestPrivateTableFlow: میزبان میز خصوصی میسازد، بازیکن دوم میپیوندد،
|
||||
// میزبان شروع میکند و بازی (با بات برای ۲ جای خالی) تا پایان پیش میرود.
|
||||
func TestPrivateTableFlow(t *testing.T) {
|
||||
hub := testHub()
|
||||
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
||||
defer srv.Close()
|
||||
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
||||
|
||||
dial := func(tok string) *websocket.Conn {
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+tok, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial %s: %v", tok, err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
host := dial("1")
|
||||
defer host.Close()
|
||||
if err := host.WriteJSON(map[string]string{"type": "create_table"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// میزبان باید table_lobby با کد دریافت کند.
|
||||
code := ""
|
||||
for code == "" {
|
||||
_ = host.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
_, raw, err := host.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("host read: %v", err)
|
||||
}
|
||||
var m anyMsg
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
if m.Type == "table_lobby" {
|
||||
if !m.Host {
|
||||
t.Fatal("host flag must be true for creator")
|
||||
}
|
||||
code = m.Code
|
||||
}
|
||||
}
|
||||
if len(code) != 5 {
|
||||
t.Fatalf("expected 5-digit code, got %q", code)
|
||||
}
|
||||
|
||||
joiner := dial("2")
|
||||
defer joiner.Close()
|
||||
if err := joiner.WriteJSON(map[string]string{"type": "join_table", "code": code}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// میزبان منتظر میماند تا میز ۲ نفره شود، سپس شروع میکند.
|
||||
var gotCountdown atomic.Bool
|
||||
play := func(conn *websocket.Conn, m *anyMsg) {
|
||||
if m.Type == "state" {
|
||||
act(conn, m)
|
||||
}
|
||||
}
|
||||
|
||||
done := make(chan string, 2)
|
||||
run := func(conn *websocket.Conn, isHost bool) {
|
||||
started := false
|
||||
for {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(6 * time.Second))
|
||||
_, raw, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
done <- "read err: " + err.Error()
|
||||
return
|
||||
}
|
||||
var m anyMsg
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
switch m.Type {
|
||||
case "table_lobby":
|
||||
if isHost && !started && len(m.Players) >= 2 {
|
||||
started = true
|
||||
_ = conn.WriteJSON(map[string]string{"type": "start_table"})
|
||||
}
|
||||
case "countdown":
|
||||
gotCountdown.Store(true)
|
||||
case "state":
|
||||
play(conn, &m)
|
||||
case "game_over":
|
||||
done <- "ok"
|
||||
return
|
||||
case "error":
|
||||
done <- "error: " + string(raw)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
go run(host, true)
|
||||
go run(joiner, false)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case res := <-done:
|
||||
if res != "ok" {
|
||||
t.Fatalf("client finished with: %s", res)
|
||||
}
|
||||
case <-time.After(25 * time.Second):
|
||||
t.Fatal("private game did not finish in time")
|
||||
}
|
||||
}
|
||||
if !gotCountdown.Load() {
|
||||
t.Fatal("expected countdown message before game start")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDisconnectMidGameNoPanic: قطع بازیکن وسط بازی باید بدون panic، رویداد
|
||||
// player_disconnected تولید کند و بازی با حرکت خودکار ادامه یابد.
|
||||
func TestDisconnectMidGameNoPanic(t *testing.T) {
|
||||
@@ -323,6 +433,9 @@ type recordingSettler struct {
|
||||
refunds int
|
||||
awards int
|
||||
records int
|
||||
hands int
|
||||
cuts int
|
||||
results int
|
||||
}
|
||||
|
||||
func (s *recordingSettler) ChargeEntry(int64, string) error {
|
||||
@@ -347,6 +460,25 @@ func (s *recordingSettler) RecordGame(string, string, int, bool) {
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *recordingSettler) RecordHand(int64, bool, bool, bool) {
|
||||
s.mu.Lock()
|
||||
s.hands++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
func (s *recordingSettler) RecordCut(int64) {
|
||||
s.mu.Lock()
|
||||
s.cuts++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
func (s *recordingSettler) RecordGameResult(int64, bool) {
|
||||
s.mu.Lock()
|
||||
s.results++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *recordingSettler) ChargePrivateTable(int64) error { return nil }
|
||||
func (s *recordingSettler) PrivateTableInfo(int64) (int, bool) { return 5, false }
|
||||
|
||||
// برای سرعتِ تست، بازی پس از ۲ هَند تمام میشود.
|
||||
func (s *recordingSettler) TargetHands(string) int { return 2 }
|
||||
func (s *recordingSettler) snapshot() (c, r, a, rec int) {
|
||||
|
||||
Reference in New Issue
Block a user