Files
gomoku/server/state/game_shared.go
T
tiennm99 1d80d334e0 refactor(lobby): drop 'New' prefix from Room type and store functions
The 'New' prefix was a phase-06 naming compromise to avoid colliding
with a legacy Room type. That legacy type is gone now, so the prefix
is misleading — in Go 'NewX' conventionally reads as a constructor,
not a type.

- NewRoom type → Room
- JoinNewRoom → JoinRoom
- GetNewRoom → GetRoom
- LeaveNewRoom → LeaveRoom
- WatchNewRoom → WatchRoom
- UnwatchNewRoom → UnwatchRoom
- deleteNewRoom → deleteRoom

Also remove dead code uncovered by the pass:
- Delete lobby/events.go entirely — RoomEvent + BroadcastEvent wrapped
  a no-op placeholder, never called.
- Delete BroadcastToNewRoom no-op from lobby/store.go — kept only for
  the dead BroadcastEvent wrapper.
- Delete TestBroadcastToRoom_SkipsExcludedIDs — theater test that never
  actually called BroadcastToNewRoom, just built a manual exclude map.

go vet + go test ./... green.
2026-04-11 16:31:36 +07:00

202 lines
5.4 KiB
Go

package state
import (
"github.com/tiennm99/gomoku/server/lobby"
"github.com/tiennm99/gomoku/server/game"
"github.com/tiennm99/gomoku/server/protocol"
)
// sendRoomSnapshot sends the current room state to a newly-joined spectator.
// Order: WatchGameSuccessResponse → GameStartingResponse → one GameMoveSuccessResponse per history entry.
// Uses Snapshot() to avoid holding a lock while calling player.Send.
func sendRoomSnapshot(player *lobby.Player, room *lobby.Room) {
snap := room.Snapshot()
status := protocol.RoomStatus_WAITING
switch room.Status { // read without lock: single int read, race-safe
case lobby.RoomStatusPlaying:
status = protocol.RoomStatus_PLAYING
case lobby.RoomStatusFinished:
status = protocol.RoomStatus_FINISHED
}
_ = player.Send(&protocol.Response{
Payload: &protocol.Response_WatchGameSuccess{
WatchGameSuccess: &protocol.WatchGameSuccessResponse{
Owner: snap.PlayerNames[snap.BlackPlayerID],
Status: status,
},
},
})
blackName := snap.PlayerNames[snap.BlackPlayerID]
whiteName := snap.PlayerNames[snap.WhitePlayerID]
if blackName == "" {
blackName = "Unknown"
}
if whiteName == "" {
whiteName = "Unknown"
}
_ = player.Send(&protocol.Response{
Payload: &protocol.Response_GameStarting{
GameStarting: &protocol.GameStartingResponse{
RoomId: int32(snap.ID),
BlackPlayerId: int32(snap.BlackPlayerID),
BlackPlayerNickname: blackName,
WhitePlayerId: int32(snap.WhitePlayerID),
WhitePlayerNickname: whiteName,
BoardSize: int32(game.BoardSize),
},
},
})
for _, mv := range snap.MoveHistory {
nickname := snap.PlayerNames[mv.PlayerID]
if nickname == "" {
nickname = "Unknown"
}
_ = player.Send(buildMoveSuccessResponse(mv.Row, mv.Col, mv.Piece, mv.PlayerID, nickname))
}
}
// buildGameStartingResponse constructs a GameStartingResponse from room state.
// Acquires RLock internally for safety.
func buildGameStartingResponse(room *lobby.Room) *protocol.Response {
room.RLock()
blackID := room.BlackPlayerID
whiteID := room.WhitePlayerID
roomID := room.ID
room.RUnlock()
blackNickname := resolveNickname(room, blackID)
whiteNickname := resolveNickname(room, whiteID)
return &protocol.Response{
Payload: &protocol.Response_GameStarting{
GameStarting: &protocol.GameStartingResponse{
RoomId: int32(roomID),
BlackPlayerId: int32(blackID),
BlackPlayerNickname: blackNickname,
WhitePlayerId: int32(whiteID),
WhitePlayerNickname: whiteNickname,
BoardSize: int32(game.BoardSize),
},
},
}
}
// resolveNickname maps a playerID to a nickname. Returns "AI" for -1 (PVE AI slot).
func resolveNickname(room *lobby.Room, playerID int64) string {
if playerID == -1 {
return "AI"
}
room.RLock()
p, ok := room.Players[playerID]
room.RUnlock()
if ok {
return p.Name
}
return "Unknown"
}
// buildMoveSuccessResponse constructs a GameMoveSuccessResponse using the typed Piece enum.
func buildMoveSuccessResponse(row, col int, piece game.Piece, playerID int64, nickname string) *protocol.Response {
p := protocol.Piece_BLACK
if piece == game.White {
p = protocol.Piece_WHITE
}
return &protocol.Response{
Payload: &protocol.Response_GameMoveSuccess{
GameMoveSuccess: &protocol.GameMoveSuccessResponse{
Row: int32(row),
Col: int32(col),
Piece: p,
PlayerNickname: nickname,
PlayerId: int32(playerID),
},
},
}
}
// buildGameOverResponse constructs a GameOverResponse using the typed GameResult enum.
// winnerNickname is empty string for a draw.
func buildGameOverResponse(result game.GameResult, winnerNickname string) *protocol.Response {
r := protocol.GameResult_DRAW
switch result {
case game.BlackWin:
r = protocol.GameResult_BLACK_WIN
case game.WhiteWin:
r = protocol.GameResult_WHITE_WIN
}
return &protocol.Response{
Payload: &protocol.Response_GameOver{
GameOver: &protocol.GameOverResponse{
Result: r,
WinnerNickname: winnerNickname,
},
},
}
}
// broadcastResponse sends resp to all players and spectators in the room.
// Acquires RLock internally; must NOT be called while holding room.Lock().
func broadcastResponse(room *lobby.Room, resp *protocol.Response) {
room.RLock()
targets := make([]*lobby.Player, 0, len(room.Players)+len(room.Spectators))
for _, p := range room.Players {
targets = append(targets, p)
}
for _, p := range room.Spectators {
targets = append(targets, p)
}
room.RUnlock()
for _, p := range targets {
_ = p.Send(resp)
}
}
// playerPieceInRoom returns the game.Piece assigned to playerID in room.
// Returns game.Empty if not assigned.
func playerPieceInRoom(room *lobby.Room, playerID int64) game.Piece {
room.RLock()
black := room.BlackPlayerID
white := room.WhitePlayerID
room.RUnlock()
if black == playerID {
return game.Black
}
if white == playerID {
return game.White
}
return game.Empty
}
// winnerNicknameFor returns the display name of the winner given a GameResult.
// Returns empty string for a draw.
func winnerNicknameFor(room *lobby.Room, result game.GameResult) string {
room.RLock()
blackID := room.BlackPlayerID
whiteID := room.WhitePlayerID
players := room.Players
room.RUnlock()
var winnerID int64
switch result {
case game.BlackWin:
winnerID = blackID
case game.WhiteWin:
winnerID = whiteID
default:
return ""
}
if winnerID == -1 {
return "AI"
}
if p, ok := players[winnerID]; ok {
return p.Name
}
return "Unknown"
}