mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-05-29 20:23:17 +00:00
43ac04ac79
Replace legacy AskForString/AskForPacket state machine with channel-based runner. Each state reads *protocol.Request from player.CmdCh. Adds PVP waiting (owner/joiner role split with StartCh signal), gamePvp (GameOverCh for cross-goroutine game-end sync), gamePve (AI alternates with human, AI-first when human is White), and gameover (reset/rematch support).
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
package network
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/tiennm99/gomoku/server/database"
|
|
"github.com/tiennm99/gomoku/server/pkg/log"
|
|
"github.com/tiennm99/gomoku/server/protocol"
|
|
"github.com/tiennm99/gomoku/server/state"
|
|
)
|
|
|
|
const (
|
|
// sendChSize is the number of outbound responses that can be buffered
|
|
// per player before back-pressure kicks in and Send() returns an error.
|
|
sendChSize = 32
|
|
|
|
// cmdChSize is the number of stateful requests queued for the state machine.
|
|
// Phase-06 rewrites the state machine; for now it remains the legacy loop.
|
|
cmdChSize = 16
|
|
)
|
|
|
|
// upgrader accepts any origin for development. Phase-11 hardens this with an allowlist.
|
|
var upgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
}
|
|
|
|
// Server is the WebSocket-only game server.
|
|
// It binds a single HTTP endpoint: /gomoku
|
|
type Server struct {
|
|
addr string
|
|
}
|
|
|
|
// NewServer creates a Server that will listen on addr (e.g. ":1999").
|
|
func NewServer(addr string) *Server {
|
|
return &Server{addr: addr}
|
|
}
|
|
|
|
// Serve registers the /gomoku handler and blocks on ListenAndServe.
|
|
func (s *Server) Serve() error {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/gomoku", s.handleWS)
|
|
log.Infof("[server] WebSocket server listening on %s/gomoku\n", s.addr)
|
|
return http.ListenAndServe(s.addr, mux)
|
|
}
|
|
|
|
// handleWS upgrades the HTTP connection to WebSocket, registers the player,
|
|
// wires I/O channels, then spawns reader + writer goroutines.
|
|
func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
log.Errorf("[server] WS upgrade failed: %v\n", err)
|
|
return
|
|
}
|
|
|
|
// Register player with an empty nickname (set later via SetNicknameRequest).
|
|
player := database.RegisterPlayer("")
|
|
player.SendCh = make(chan *protocol.Response, sendChSize)
|
|
player.CmdCh = make(chan *protocol.Request, cmdChSize)
|
|
player.LastHeartbeat = time.Now()
|
|
|
|
log.Infof("[server] player %d connected from %s\n", player.ID, r.RemoteAddr)
|
|
|
|
// Send ClientConnectResponse so the client knows its assigned ID.
|
|
_ = player.Send(&protocol.Response{
|
|
Payload: &protocol.Response_ClientConnect{
|
|
ClientConnect: &protocol.ClientConnectResponse{
|
|
ClientId: int32(player.ID),
|
|
},
|
|
},
|
|
})
|
|
|
|
// Send initial nickname prompt (invalid_length = 0 signals "prompt mode").
|
|
_ = player.Send(&protocol.Response{
|
|
Payload: &protocol.Response_NicknameSet{
|
|
NicknameSet: &protocol.NicknameSetResponse{InvalidLength: 0},
|
|
},
|
|
})
|
|
|
|
wr := newWriter(conn, player.SendCh)
|
|
|
|
// Writer goroutine: serialises all WS writes.
|
|
go wr.run()
|
|
|
|
// State machine goroutine: reads typed *protocol.Request from player.CmdCh.
|
|
// Stateful requests are routed here by Dispatch; stateless ones handled inline.
|
|
go state.Run(player)
|
|
|
|
// Reader loop: blocks until WS close or error, then cleans up.
|
|
readLoop(conn, player)
|
|
}
|