package ws import ( "doudizhu-server/internal/game" "doudizhu-server/internal/models" "encoding/json" "log" "net/http" "strings" "sync" "time" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } type Client struct { ID string RoomID string Conn *websocket.Conn Send chan []byte Hub *Hub done chan struct{} doneOnce sync.Once } type Hub struct { Clients map[string]*Client Register chan *Client Unregister chan *Client GameMgr *game.GameManager mu sync.RWMutex stopChan chan struct{} stopOnce sync.Once } func NewHub(gameMgr *game.GameManager) *Hub { return &Hub{ Clients: make(map[string]*Client), Register: make(chan *Client, 64), Unregister: make(chan *Client, 64), GameMgr: gameMgr, stopChan: make(chan struct{}), } } func (h *Hub) Run() { for { select { case c := <-h.Register: h.mu.Lock() if old, exists := h.Clients[c.ID]; exists { old.close() delete(h.Clients, old.ID) } h.Clients[c.ID] = c h.mu.Unlock() case c := <-h.Unregister: h.mu.Lock() if _, ok := h.Clients[c.ID]; ok { delete(h.Clients, c.ID) c.close() } h.mu.Unlock() if c.RoomID != "" { h.GameMgr.MarkPlayerOffline(c.RoomID, c.ID) h.broadcastRoomState(c.RoomID) } case <-h.stopChan: h.mu.Lock() for _, c := range h.Clients { c.close() } h.Clients = make(map[string]*Client) h.mu.Unlock() return } } } func (h *Hub) Stop() { h.stopOnce.Do(func() { close(h.stopChan) }) } func (c *Client) close() { c.doneOnce.Do(func() { close(c.done) }) } func (c *Client) ReadPump() { defer func() { c.Hub.Unregister <- c }() c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) for { select { case <-c.done: return default: _, data, err := c.Conn.ReadMessage() if err != nil { return } c.handleMessage(data) } } } func (c *Client) WritePump() { ticker := time.NewTicker(30 * time.Second) defer func() { ticker.Stop() c.Conn.Close() }() for { select { case <-c.done: c.Conn.WriteMessage(websocket.CloseMessage, nil) return case msg, ok := <-c.Send: c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if !ok { c.Conn.WriteMessage(websocket.CloseMessage, nil) return } if err := c.Conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } case <-ticker.C: c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } } func (c *Client) handleMessage(data []byte) { var msg models.Message if err := json.Unmarshal(data, &msg); err != nil { return } msg.Timestamp = time.Now().Unix() switch msg.Type { case models.MsgTypeReady: ready := true if m, ok := msg.Data.(map[string]interface{}); ok { if r, ok := m["ready"].(bool); ok { ready = r } } c.Hub.GameMgr.SetReady(c.RoomID, c.ID, ready) c.Hub.broadcastRoomState(c.RoomID) c.tryStartGame() case models.MsgTypePlay: cards := c.parseCards(msg.Data) if len(cards) == 0 { c.sendError("invalid cards") return } if err := c.Hub.GameMgr.PlayCards(c.RoomID, c.ID, cards); err != nil { c.sendError(err.Error()) return } room, _ := c.Hub.GameMgr.GetRoom(c.RoomID) if room != nil && room.State == models.RoomStateFinished { c.Hub.broadcastFinalState(c.RoomID, room) c.Hub.broadcastToRoom(c.RoomID, models.Message{ Type: models.MsgTypeGameOver, Data: map[string]string{"winnerId": room.LastWinner}, }) } else { c.Hub.broadcastRoomState(c.RoomID) } case models.MsgTypePass: if err := c.Hub.GameMgr.Pass(c.RoomID, c.ID); err != nil { c.sendError(err.Error()) return } c.Hub.broadcastRoomState(c.RoomID) case models.MsgTypeChat: chatMsg, ok := msg.Data.(string) if !ok || chatMsg == "" { return } playerName := c.Hub.GameMgr.GetPlayerName(c.RoomID, c.ID) c.Hub.broadcastToRoom(c.RoomID, models.Message{ Type: models.MsgTypeChat, Data: map[string]string{ "playerId": c.ID, "playerName": playerName, "message": chatMsg, }, Timestamp: time.Now().Unix(), }) } } func (c *Client) tryStartGame() { room, err := c.Hub.GameMgr.GetRoom(c.RoomID) if err != nil || len(room.Players) < 2 { return } for _, p := range room.Players { if !p.IsReady { return } } if err := c.Hub.GameMgr.StartGame(c.RoomID); err == nil { c.Hub.broadcastRoomState(c.RoomID) } } func (c *Client) parseCards(data interface{}) []models.Card { cards := make([]models.Card, 0) m, ok := data.(map[string]interface{}) if !ok { return cards } arr, ok := m["cards"].([]interface{}) if !ok { return cards } for _, item := range arr { if cm, ok := item.(map[string]interface{}); ok { card := models.Card{} if suit, ok := cm["suit"].(float64); ok { card.Suit = models.Suit(int(suit)) } if value, ok := cm["value"].(float64); ok { card.Value = int(value) } cards = append(cards, card) } } return cards } func (c *Client) sendError(msg string) { data, _ := json.Marshal(models.Message{ Type: models.MsgTypeError, Data: msg, Timestamp: time.Now().Unix(), }) select { case c.Send <- data: case <-c.done: default: } } func (h *Hub) broadcastToRoom(roomID string, msg models.Message) { data, _ := json.Marshal(msg) h.mu.RLock() defer h.mu.RUnlock() for _, c := range h.Clients { if c.RoomID == roomID { select { case c.Send <- data: case <-c.done: default: } } } } func (h *Hub) broadcastRoomState(roomID string) { h.mu.RLock() clients := make([]*Client, 0) for _, c := range h.Clients { if c.RoomID == roomID { clients = append(clients, c) } } h.mu.RUnlock() for _, c := range clients { state := h.GameMgr.GetRoomState(roomID, c.ID) if state != nil { data, _ := json.Marshal(models.Message{ Type: models.MsgTypeState, Data: state, Timestamp: time.Now().Unix(), }) select { case c.Send <- data: case <-c.done: default: } } } } func (h *Hub) broadcastFinalState(roomID string, room *models.Room) { h.mu.RLock() clients := make([]*Client, 0) for _, c := range h.Clients { if c.RoomID == roomID { clients = append(clients, c) } } h.mu.RUnlock() for _, c := range clients { state := h.GameMgr.GetRoomState(roomID, c.ID) if state != nil { state.State = models.RoomStatePlaying data, _ := json.Marshal(models.Message{ Type: models.MsgTypeState, Data: state, Timestamp: time.Now().Unix(), }) select { case c.Send <- data: case <-c.done: default: } } } } func (h *Hub) BroadcastRoomLeft(roomID, playerID string) { h.mu.RLock() clients := make([]*Client, 0) for _, c := range h.Clients { if c.RoomID == roomID { clients = append(clients, c) } } h.mu.RUnlock() data, _ := json.Marshal(models.Message{ Type: models.MsgTypeLeave, PlayerID: playerID, Timestamp: time.Now().Unix(), }) for _, c := range clients { select { case c.Send <- data: case <-c.done: default: } } h.broadcastRoomState(roomID) } func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { roomID := strings.ToLower(r.URL.Query().Get("roomId")) playerID := r.URL.Query().Get("playerId") if roomID == "" || playerID == "" { http.Error(w, "missing params", http.StatusBadRequest) return } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("ws upgrade error:", err) return } client := &Client{ ID: playerID, RoomID: roomID, Conn: conn, Send: make(chan []byte, 256), Hub: hub, done: make(chan struct{}), } hub.GameMgr.MarkPlayerOnline(roomID, playerID) hub.Register <- client time.Sleep(50 * time.Millisecond) go client.WritePump() go client.ReadPump() hub.broadcastRoomState(roomID) }