feat: add messages feature
This commit is contained in:
+1
-1
@@ -173,7 +173,7 @@ func (h *Hub) handle(in inbound) {
|
||||
}
|
||||
h.enqueue(c, tier)
|
||||
}
|
||||
case "choose_trump", "play_card", "reshuffle", "leave":
|
||||
case "choose_trump", "play_card", "reshuffle", "chat", "leave":
|
||||
if c.room != nil {
|
||||
if in.msg.Type == "leave" {
|
||||
delete(h.locations, c.UserID) // پس از خروج دائمی، reconnect نشود
|
||||
|
||||
@@ -12,6 +12,16 @@ type inboundMsg struct {
|
||||
Code string `json:"code"` // برای join_table: شماره میز خصوصی
|
||||
Hands int `json:"hands"` // برای start_table: تعداد دستِ بازیِ خصوصی (۳/۵/۷)
|
||||
Dir int `json:"dir"` // برای rotate_table: جهتِ چرخشِ جایگاهها (+۱/−۱)
|
||||
Text string `json:"text"` // برای chat: متنِ پیامِ آماده
|
||||
Emoji string `json:"emoji"` // برای chat: شناسه/کاراکترِ شکلک
|
||||
}
|
||||
|
||||
// chatMsg پیامِ چت/شکلکِ یک بازیکن که برای همهی میز پخش میشود.
|
||||
type chatMsg struct {
|
||||
Type string `json:"type"` // "chat"
|
||||
Seat int `json:"seat"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
}
|
||||
|
||||
// lobbyPlayer یک بازیکن در اتاق انتظارِ میز خصوصی.
|
||||
|
||||
@@ -65,6 +65,7 @@ type Room struct {
|
||||
dealDelay time.Duration // پنجرهی پخشِ بقیهی کارتها (پس از انتخابِ حکم)
|
||||
// graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمیشود (هماهنگ با انیمیشنهای کلاینت).
|
||||
graceUntil time.Time
|
||||
chatAt [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم)
|
||||
}
|
||||
|
||||
func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room {
|
||||
@@ -250,6 +251,8 @@ func (r *Room) handleInput(act roomAction) {
|
||||
switch act.msg.Type {
|
||||
case "leave":
|
||||
r.handleLeave(act.seat)
|
||||
case "chat":
|
||||
r.handleChat(act.seat, act.msg.Text, act.msg.Emoji)
|
||||
case "choose_trump":
|
||||
suit, err := game.ParseSuit(act.msg.Suit)
|
||||
if err != nil {
|
||||
@@ -544,6 +547,28 @@ func (r *Room) broadcast(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleChat یک پیام/شکلکِ بازیکن را با کنترلِ نرخ (ضدِ اسپم) و طولِ مجاز برای
|
||||
// همهی میز پخش میکند تا کنارِ آواتارِ فرستنده نمایش داده شود.
|
||||
func (r *Room) handleChat(seat int, text, emoji string) {
|
||||
now := time.Now()
|
||||
if now.Sub(r.chatAt[seat]) < 1200*time.Millisecond {
|
||||
return // فاصلهی کمتر از ~۱.۲ ثانیه ⇒ نادیده (ضدِ اسپم)
|
||||
}
|
||||
clip := func(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s
|
||||
}
|
||||
text, emoji = clip(text, 60), clip(emoji, 16)
|
||||
if text == "" && emoji == "" {
|
||||
return
|
||||
}
|
||||
r.chatAt[seat] = now
|
||||
r.broadcast(mustJSON(chatMsg{Type: "chat", Seat: seat, Text: text, Emoji: emoji}))
|
||||
}
|
||||
|
||||
func (r *Room) sendError(seat int, msg string) {
|
||||
if c := r.seats[seat].client; c != nil {
|
||||
c.trySend(mustJSON(errorMsg{Type: "error", Message: msg}))
|
||||
|
||||
@@ -30,6 +30,8 @@ type anyMsg struct {
|
||||
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 واقعی با ۴ ربات اجرا میکند.
|
||||
@@ -585,6 +587,68 @@ func TestRefundOnAbort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user