664 lines
18 KiB
Go
664 lines
18 KiB
Go
package ws
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"hakemsho/internal/game"
|
|
)
|
|
|
|
// anyMsg تمام فیلدهای ممکن پیامهای سرور را برای تست جمع میکند.
|
|
type anyMsg struct {
|
|
Type string `json:"type"`
|
|
Seat int `json:"seat"`
|
|
YourSeat int `json:"your_seat"`
|
|
Phase string `json:"phase"`
|
|
Hakem int `json:"hakem"`
|
|
Turn int `json:"turn"`
|
|
YourHand []string `json:"your_hand"`
|
|
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
|
|
Text string `json:"text"` // chat
|
|
Emoji string `json:"emoji"` // chat
|
|
}
|
|
|
|
// TestFourPlayersFullGame یک بازی کامل را روی WebSocket واقعی با ۴ ربات اجرا میکند.
|
|
func TestFourPlayersFullGame(t *testing.T) {
|
|
hub := testHub() // مهلتهای کوتاه (شامل trickHold) برای سرعت تست
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx), nil)
|
|
if err != nil {
|
|
t.Errorf("dial %d: %v", idx, err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
// ورود به صف
|
|
if err := conn.WriteJSON(map[string]string{"type": "join_queue", "mode": "normal"}); err != nil {
|
|
t.Errorf("join %d: %v", idx, err)
|
|
return
|
|
}
|
|
|
|
seat := -1
|
|
for {
|
|
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
_, raw, err := conn.ReadMessage()
|
|
if err != nil {
|
|
t.Errorf("read %d (seat %d): %v", idx, seat, err)
|
|
return
|
|
}
|
|
var m anyMsg
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Errorf("unmarshal %d: %v", idx, err)
|
|
return
|
|
}
|
|
switch m.Type {
|
|
case "matched":
|
|
seat = m.Seat
|
|
case "state":
|
|
act(conn, &m)
|
|
case "game_over":
|
|
return
|
|
case "error":
|
|
t.Errorf("client %d got error: %s", idx, raw)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() { wg.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(25 * time.Second):
|
|
t.Fatal("game did not finish in time")
|
|
}
|
|
}
|
|
|
|
// act ربات سمتکلاینت: حکم میزند یا کارت مجاز بازی میکند.
|
|
// در فاز انتخاب حکم، حاکم دقیقاً یک state دریافت میکند پس یکبار حکم میزند (هر هَند).
|
|
func act(conn *websocket.Conn, m *anyMsg) {
|
|
switch m.Phase {
|
|
case "choose_trump":
|
|
if m.YourSeat == m.Hakem && len(m.YourHand) > 0 {
|
|
c, _ := game.ParseCard(m.YourHand[0])
|
|
_ = conn.WriteJSON(map[string]string{"type": "choose_trump", "suit": c.Suit.String()})
|
|
}
|
|
case "playing":
|
|
if m.TrickDone || m.Turn != m.YourSeat {
|
|
return
|
|
}
|
|
card := legalCard(m.YourHand, m.LeadSuit)
|
|
_ = conn.WriteJSON(map[string]string{"type": "play_card", "card": card})
|
|
}
|
|
}
|
|
|
|
// testHub یک هاب با مهلتهای کوتاه برای تستها میسازد.
|
|
func testHub() *Hub {
|
|
hub := NewHub(func(token string) (int64, string, error) {
|
|
id, err := strconv.ParseInt(token, 10, 64)
|
|
return id, "p" + token, err
|
|
})
|
|
hub.turnTimeout = 8 * time.Millisecond
|
|
hub.botDelay = 8 * time.Millisecond
|
|
hub.matchWait = 300 * time.Millisecond
|
|
hub.trickHold = 5 * time.Millisecond
|
|
hub.firstHandDelay = 0
|
|
hub.dealDelay = 0
|
|
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) {
|
|
hub := testHub()
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
conns := make([]*websocket.Conn, 4)
|
|
for i := 0; i < 4; i++ {
|
|
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial %d: %v", i, err)
|
|
}
|
|
conns[i] = c
|
|
if err := c.WriteJSON(map[string]string{"type": "join_queue"}); err != nil {
|
|
t.Fatalf("join %d: %v", i, err)
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
conns[0].Close()
|
|
|
|
gotDisconnect := false
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
_ = conns[1].SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, raw, err := conns[1].ReadMessage()
|
|
if err != nil {
|
|
break
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "player_disconnected" {
|
|
gotDisconnect = true
|
|
break
|
|
}
|
|
}
|
|
for _, c := range conns[1:] {
|
|
c.Close()
|
|
}
|
|
if !gotDisconnect {
|
|
t.Fatal("expected player_disconnected after a player disconnected")
|
|
}
|
|
}
|
|
|
|
// TestBotFill: اگر کمتر از ۴ انسان در صف بماند، میز با بات کامل و بازی شروع میشود.
|
|
func TestBotFill(t *testing.T) {
|
|
hub := testHub()
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
// فقط ۲ انسان وصل میشوند.
|
|
conns := make([]*websocket.Conn, 2)
|
|
for i := 0; i < 2; i++ {
|
|
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial %d: %v", i, err)
|
|
}
|
|
defer c.Close()
|
|
conns[i] = c
|
|
_ = c.WriteJSON(map[string]string{"type": "join_queue"})
|
|
}
|
|
|
|
// بازیکن ۰ باید matched با ۴ بازیکن (۲ بات) دریافت کند.
|
|
_ = conns[0].SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
bots := -1
|
|
for {
|
|
_, raw, err := conns[0].ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read matched: %v", err)
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "matched" {
|
|
if len(m.Players) != 4 {
|
|
t.Fatalf("expected 4 players, got %d", len(m.Players))
|
|
}
|
|
bots = 0
|
|
for _, p := range m.Players {
|
|
if p.Bot {
|
|
bots++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if bots != 2 {
|
|
t.Fatalf("expected 2 bots, got %d", bots)
|
|
}
|
|
}
|
|
|
|
// TestReconnect: بازیکن قطعشده میتواند با همان توکن به جایگاه قبلی بازگردد.
|
|
func TestReconnect(t *testing.T) {
|
|
hub := testHub()
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
conns := make([]*websocket.Conn, 4)
|
|
for i := 0; i < 4; i++ {
|
|
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial %d: %v", i, err)
|
|
}
|
|
conns[i] = c
|
|
_ = c.WriteJSON(map[string]string{"type": "join_queue"})
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// بازیکن ۰ قطع و سپس با همان توکن دوباره وصل میشود.
|
|
conns[0].Close()
|
|
time.Sleep(100 * time.Millisecond)
|
|
rc, _, err := websocket.DefaultDialer.Dial(wsURL+"?token=0", nil)
|
|
if err != nil {
|
|
t.Fatalf("reconnect dial: %v", err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
// باید matched با seat=0 و سپس state دریافت کند.
|
|
gotMatched, gotState := false, false
|
|
_ = rc.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for !gotState {
|
|
_, raw, err := rc.ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read after reconnect: %v", err)
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
switch m.Type {
|
|
case "matched":
|
|
if m.Seat != 0 {
|
|
t.Fatalf("reconnect to wrong seat: %d", m.Seat)
|
|
}
|
|
gotMatched = true
|
|
case "state":
|
|
gotState = true
|
|
}
|
|
}
|
|
if !gotMatched || !gotState {
|
|
t.Fatal("reconnected client did not receive matched+state")
|
|
}
|
|
for _, c := range conns[1:] {
|
|
c.Close()
|
|
}
|
|
}
|
|
|
|
// TestTurnTimeoutAutoPlays: حتی اگر هیچ بازیکنی حرکت نکند، تایمر نوبت کل بازی
|
|
// را خودکار پیش میبرد تا game_over (مسیر مشترک با حرکت بات/قطعشده).
|
|
func TestTurnTimeoutAutoPlays(t *testing.T) {
|
|
hub := testHub()
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx), nil)
|
|
if err != nil {
|
|
t.Errorf("dial %d: %v", idx, err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.WriteJSON(map[string]string{"type": "join_queue"})
|
|
// عمداً هیچ حرکتی نمیزنیم؛ فقط منتظر game_over میمانیم.
|
|
for {
|
|
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
_, raw, err := conn.ReadMessage()
|
|
if err != nil {
|
|
t.Errorf("client %d read: %v", idx, err)
|
|
return
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "game_over" {
|
|
return
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() { wg.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(20 * time.Second):
|
|
t.Fatal("turn-timeout auto-play did not finish the game")
|
|
}
|
|
}
|
|
|
|
// recordingSettler فراخوانیهای تسویه را برای بررسی در تست میشمارد.
|
|
type recordingSettler struct {
|
|
mu sync.Mutex
|
|
charges int
|
|
refunds int
|
|
awards int
|
|
records int
|
|
hands int
|
|
cuts int
|
|
results int
|
|
}
|
|
|
|
func (s *recordingSettler) ChargeEntry(int64, string) error {
|
|
s.mu.Lock()
|
|
s.charges++
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (s *recordingSettler) Refund(int64, string) {
|
|
s.mu.Lock()
|
|
s.refunds++
|
|
s.mu.Unlock()
|
|
}
|
|
func (s *recordingSettler) AwardWinner(int64, string) {
|
|
s.mu.Lock()
|
|
s.awards++
|
|
s.mu.Unlock()
|
|
}
|
|
func (s *recordingSettler) AwardRank(int64, string, bool) {}
|
|
func (s *recordingSettler) RecordGame(string, string, int, bool) {
|
|
s.mu.Lock()
|
|
s.records++
|
|
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) PlayerProfile(int64) (int64, string) { return 0, "" }
|
|
func (s *recordingSettler) ChargeTicket(int64) error { return nil }
|
|
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) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.charges, s.refunds, s.awards, s.records
|
|
}
|
|
|
|
// TestSettlementOnGameOver: ۴ انسانِ بیکار ⇒ ۴ کسر ورودی، بازی خودکار تا پایان،
|
|
// ۲ جایزهی برنده و یک ثبت بازی، بدون بازگشت.
|
|
func TestSettlementOnGameOver(t *testing.T) {
|
|
hub := testHub()
|
|
settler := &recordingSettler{}
|
|
hub.SetSettler(settler)
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx+1), nil)
|
|
if err != nil {
|
|
t.Errorf("dial %d: %v", idx, err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.WriteJSON(map[string]string{"type": "join_queue", "tier": "beginner"})
|
|
for {
|
|
_ = conn.SetReadDeadline(time.Now().Add(6 * time.Second))
|
|
_, raw, err := conn.ReadMessage()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "game_over" {
|
|
return
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
done := make(chan struct{})
|
|
go func() { wg.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(20 * time.Second):
|
|
t.Fatal("game did not finish")
|
|
}
|
|
time.Sleep(100 * time.Millisecond) // فرصت پردازش settle در goroutine میز
|
|
|
|
c, r, a, rec := settler.snapshot()
|
|
if c != 4 {
|
|
t.Errorf("charges = %d, want 4", c)
|
|
}
|
|
if a != 2 {
|
|
t.Errorf("awards = %d, want 2 (winning team humans)", a)
|
|
}
|
|
if rec != 1 {
|
|
t.Errorf("records = %d, want 1", rec)
|
|
}
|
|
if r != 0 {
|
|
t.Errorf("refunds = %d, want 0 on normal finish", r)
|
|
}
|
|
}
|
|
|
|
// TestRefundOnAbort: اگر همهی انسانها وسط بازی قطع شوند، ورودی بازگردانده میشود.
|
|
func TestRefundOnAbort(t *testing.T) {
|
|
hub := testHub()
|
|
settler := &recordingSettler{}
|
|
hub.SetSettler(settler)
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
conns := make([]*websocket.Conn, 4)
|
|
for i := 0; i < 4; i++ {
|
|
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i+1), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial %d: %v", i, err)
|
|
}
|
|
conns[i] = c
|
|
_ = c.WriteJSON(map[string]string{"type": "join_queue", "tier": "beginner"})
|
|
}
|
|
time.Sleep(150 * time.Millisecond) // میز تشکیل شود
|
|
for _, c := range conns {
|
|
c.Close()
|
|
}
|
|
time.Sleep(400 * time.Millisecond) // قطع همه ⇒ abort + refund
|
|
|
|
charges, refunds, _, _ := settler.snapshot()
|
|
if charges != 4 {
|
|
t.Errorf("charges = %d, want 4", charges)
|
|
}
|
|
if refunds != 4 {
|
|
t.Errorf("refunds = %d, want 4 on abort", refunds)
|
|
}
|
|
}
|
|
|
|
// TestChatBroadcast: پیامِ چتِ یک بازیکن باید برای همهی میز (شاملِ گیرندهی دیگر)
|
|
// با جایگاهِ فرستنده و متن پخش شود.
|
|
func TestChatBroadcast(t *testing.T) {
|
|
hub := testHub()
|
|
srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
|
|
conns := make([]*websocket.Conn, 4)
|
|
seats := make([]int, 4)
|
|
for i := 0; i < 4; i++ {
|
|
c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial %d: %v", i, err)
|
|
}
|
|
defer c.Close()
|
|
conns[i] = c
|
|
_ = c.WriteJSON(map[string]string{"type": "join_queue"})
|
|
}
|
|
|
|
// منتظرِ matched برای دانستنِ جایگاهِ فرستنده.
|
|
for i := 0; i < 4; i++ {
|
|
_ = conns[i].SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for {
|
|
_, raw, err := conns[i].ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read matched %d: %v", i, err)
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "matched" {
|
|
seats[i] = m.Seat
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// فرستنده: بازیکن ۰ یک پیام میفرستد.
|
|
_ = conns[0].WriteJSON(map[string]string{"type": "chat", "text": "سلام"})
|
|
|
|
// گیرنده: بازیکن ۱ باید پیامِ chat با جایگاهِ بازیکن ۰ و متنِ درست بگیرد.
|
|
_ = conns[1].SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
got := false
|
|
for !got {
|
|
_, raw, err := conns[1].ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read chat: %v", err)
|
|
}
|
|
var m anyMsg
|
|
_ = json.Unmarshal(raw, &m)
|
|
if m.Type == "chat" {
|
|
if m.Seat != seats[0] {
|
|
t.Fatalf("chat seat = %d, want %d", m.Seat, seats[0])
|
|
}
|
|
if m.Text != "سلام" {
|
|
t.Fatalf("chat text = %q, want سلام", m.Text)
|
|
}
|
|
got = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// legalCard یک کارت مجاز انتخاب میکند (follow suit در صورت امکان).
|
|
func legalCard(hand []string, leadSuit string) string {
|
|
if leadSuit != "" {
|
|
for _, cs := range hand {
|
|
c, err := game.ParseCard(cs)
|
|
if err == nil && c.Suit.String() == leadSuit {
|
|
return cs
|
|
}
|
|
}
|
|
}
|
|
return hand[0]
|
|
}
|