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 } type Hub struct { Clients map[string]*Client Register chan *Client Unregister chan *Client GameMgr *game.GameManager mu sync.RWMutex } 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, } } func (h *Hub) Run() { for { select { case c := <-h.Register: h.mu.Lock() 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) close(c.Send) } h.mu.Unlock() if c.RoomID != "" { h.GameMgr.LeaveRoom(c.RoomID, c.ID) h.broadcastRoomState(c.RoomID) } } } } func (c *Client) ReadPump() { defer func() { c.Hub.Unregister <- c c.Conn.Close() }() 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 { _, data, err := c.Conn.ReadMessage() if err != nil { break } c.handleMessage(data) } } func (c *Client) WritePump() { ticker := time.NewTicker(30 * time.Second) defer func() { ticker.Stop() c.Conn.Close() }() for { select { 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.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: c.Hub.broadcastToRoom(c.RoomID, msg) } } 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: 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: 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: default: } } } } 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, } hub.mu.Lock() hub.Clients[client.ID] = client hub.mu.Unlock() go client.WritePump() go client.ReadPump() time.Sleep(50 * time.Millisecond) hub.broadcastRoomState(roomID) }