mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-05-14 04:58:38 +00:00
9de0f97752
The "Start Game" button in the waiting room is gone — as soon as a second player joins a PVP room, the server flips to Playing and both clients transition into the game in lockstep. Server: - Proto: remove GameStartingRequest message + oneof field (now unused). - state/home.go handleJoinRoom: after a successful join that fills the room, call assignColors, set Status=Playing + CurrentTurn=Black, close StartCh, broadcast GameStartingResponse, return StateGamePvp for the joining player. If still only one player, return StateWaiting as before. - state/waiting.go: collapse ownerWait + joinerWait into a single unified loop. Both roles just select on StartCh (auto-start signal) and CmdCh (for ClientExit). No more GameStartingRequest handling or PlayerCount checks. - network/dispatch.go: drop Request_GameStarting from the stateful routing list. Client: - connection-service.js: remove sendGameStarting() helper. - menu-ui-rooms.js showWaiting: drop the isOwner parameter + Start Game button + owner/joiner branching. Both views show the same passive 'Waiting for opponent...' message that flips to 'Opponent joined: <name> - starting game...' when the second player arrives. - menu-scene.js: update _onRoomCreateSuccess + _onRoomJoinSuccess to call the simpler showWaiting(ownerNickname, playerCount, onLeave) signature. Tests: - Remove TestWaitingOwnerGameStartingRequiresFullRoom (obsolete). - Rewrite TestWaitingOwnerStartsWhenFull into TestWaitingOwner TransitionsOnStartCh: simulate handleJoinRoom closing StartCh, verify owner transitions to StateGamePvp. - Add TestFlow_JoinAutoStartsGame: end-to-end drive where owner is in waitingState, joiner sends JoinRoomRequest from home, and both goroutines must land in StateGamePvp with colors assigned and GameStartingResponse broadcast. Regen Go + JS proto stubs. go vet + go test ./... green. npm --prefix client run build green.
67 lines
2.2 KiB
Go
67 lines
2.2 KiB
Go
package network
|
|
|
|
import (
|
|
"github.com/tiennm99/gomoku/server/lobby"
|
|
"github.com/tiennm99/gomoku/server/pkg/log"
|
|
"github.com/tiennm99/gomoku/server/protocol"
|
|
)
|
|
|
|
// Dispatch routes an incoming Request to the appropriate handler.
|
|
//
|
|
// Stateless requests (heartbeat, get_rooms, set_nickname, set_client_info)
|
|
// are handled inline on the reader goroutine — they must return fast.
|
|
//
|
|
// Stateful requests (create_room, join_room, game_move, client_exit, etc.)
|
|
// are pushed onto player.CmdCh for the state machine goroutine to consume
|
|
// in order.
|
|
//
|
|
// Overflow policy: if CmdCh is full, the request is logged and dropped rather
|
|
// than blocking the reader goroutine (prevents backpressure deadlocks).
|
|
func Dispatch(player *lobby.Player, req *protocol.Request) {
|
|
switch req.Payload.(type) {
|
|
// --- Stateless handlers (inline) ---
|
|
case *protocol.Request_Heartbeat:
|
|
handleHeartbeat(player, req)
|
|
|
|
case *protocol.Request_SetNickname:
|
|
handleSetNickname(player, req)
|
|
|
|
case *protocol.Request_GetRooms:
|
|
handleGetRooms(player, req)
|
|
|
|
case *protocol.Request_SetClientInfo:
|
|
handleSetClientInfo(player, req)
|
|
|
|
// --- Stateful handlers (pushed to cmdCh for state machine) ---
|
|
// ClientExit is stateful: it must leave the current room and transition
|
|
// the state machine back to home, keeping the WS alive.
|
|
case *protocol.Request_CreateRoom,
|
|
*protocol.Request_CreatePveRoom,
|
|
*protocol.Request_JoinRoom,
|
|
*protocol.Request_GameMove,
|
|
*protocol.Request_GameReset,
|
|
*protocol.Request_WatchGame,
|
|
*protocol.Request_WatchGameExit,
|
|
*protocol.Request_ClientExit:
|
|
pushToCmdCh(player, req)
|
|
|
|
default:
|
|
log.Errorf("[dispatch] player %d: unhandled request type %T, dropping\n", player.ID, req.Payload)
|
|
}
|
|
}
|
|
|
|
// pushToCmdCh enqueues req on player.CmdCh without blocking.
|
|
// If the channel is full or nil, the request is dropped with a warning.
|
|
func pushToCmdCh(player *lobby.Player, req *protocol.Request) {
|
|
if player.CmdCh == nil {
|
|
log.Errorf("[dispatch] player %d: CmdCh is nil, dropping %T\n", player.ID, req.Payload)
|
|
return
|
|
}
|
|
select {
|
|
case player.CmdCh <- req:
|
|
default:
|
|
log.Errorf("[dispatch] player %d: CmdCh full (cap %d), dropping %T\n",
|
|
player.ID, cap(player.CmdCh), req.Payload)
|
|
}
|
|
}
|