mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-06-01 04:10:50 +00:00
acf08669dd
- Split monolithic database.go/model.go/gomoku.go into focused files: player.go, room.go, store.go, errors.go, events.go, cleanup.go - NewRoom embeds game.Board directly; carries blackPlayerId, whitePlayerId, currentTurn, moveHistory, spectators, owner, type, difficulty from caro domain - Store singleton with sync.RWMutex: RegisterPlayer, CreatePvpRoom, CreatePveRoom, JoinNewRoom, LeaveNewRoom, WatchNewRoom, UnwatchNewRoom, BroadcastToNewRoom - PVE rooms randomly assign human to Black or White; AI takes opposite side - legacy.go shim keeps server/state/*.go and state/game/gomoku.go compiling without modification until phase-06 rewrites them - consts/const.go: add RoomTypePvp/Pve, Status*, RoomStatus*, Difficulty* enums
351 lines
8.0 KiB
Go
351 lines
8.0 KiB
Go
package database
|
|
|
|
import (
|
|
"math/rand"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tiennm99/gomoku/server/game"
|
|
)
|
|
|
|
// store is the package-level in-memory data store for the new domain model.
|
|
// It is separate from the legacy hashmaps in legacy.go which back the old state machine.
|
|
//
|
|
// Lock discipline:
|
|
// - Acquire store.mu only to read/write the maps and counters.
|
|
// - Release store.mu before calling any per-room method or player callback.
|
|
// - Per-room mutations use room.Lock() / room.RLock() independently.
|
|
var store = &roomStore{
|
|
players: make(map[int64]*Player),
|
|
rooms: make(map[int64]*NewRoom),
|
|
nextPlayerID: 1,
|
|
nextRoomID: 1,
|
|
}
|
|
|
|
type roomStore struct {
|
|
mu sync.RWMutex
|
|
players map[int64]*Player
|
|
rooms map[int64]*NewRoom
|
|
nextPlayerID int64
|
|
nextRoomID int64
|
|
}
|
|
|
|
// --- Player operations ---
|
|
|
|
// RegisterPlayer assigns a unique ID and registers the player in the store.
|
|
func RegisterPlayer(nickname string) *Player {
|
|
store.mu.Lock()
|
|
id := store.nextPlayerID
|
|
store.nextPlayerID++
|
|
p := &Player{
|
|
ID: id,
|
|
Name: nickname,
|
|
}
|
|
store.players[id] = p
|
|
store.mu.Unlock()
|
|
return p
|
|
}
|
|
|
|
// GetPlayer returns the player with the given ID, or (nil, false) if not found.
|
|
func GetNewPlayer(id int64) (*Player, bool) {
|
|
store.mu.RLock()
|
|
p, ok := store.players[id]
|
|
store.mu.RUnlock()
|
|
return p, ok
|
|
}
|
|
|
|
// RemovePlayer unregisters the player and removes them from any room they are in.
|
|
func RemovePlayer(id int64) {
|
|
store.mu.Lock()
|
|
p, ok := store.players[id]
|
|
if ok {
|
|
delete(store.players, id)
|
|
}
|
|
store.mu.Unlock()
|
|
if ok && p.RoomID != 0 {
|
|
LeaveNewRoom(p)
|
|
}
|
|
}
|
|
|
|
// --- Room creation ---
|
|
|
|
// CreatePvpRoom creates a PVP room owned by the given player.
|
|
func CreatePvpRoom(owner *Player) (*NewRoom, error) {
|
|
store.mu.Lock()
|
|
id := store.nextRoomID
|
|
store.nextRoomID++
|
|
r := &NewRoom{
|
|
ID: id,
|
|
OwnerID: owner.ID,
|
|
OwnerNickname: owner.Name,
|
|
RoomType: RoomTypePvp,
|
|
Status: RoomStatusWaiting,
|
|
Board: game.NewBoard(),
|
|
BlackPlayerID: 0,
|
|
WhitePlayerID: 0,
|
|
CurrentTurn: game.Black,
|
|
Players: make(map[int64]*Player),
|
|
Spectators: make(map[int64]*Player),
|
|
MoveHistory: nil,
|
|
CreatedAt: time.Now(),
|
|
LastActive: time.Now(),
|
|
}
|
|
store.rooms[id] = r
|
|
store.mu.Unlock()
|
|
return r, nil
|
|
}
|
|
|
|
// CreatePveRoom creates a PVE room. difficulty must be 1, 2, or 3.
|
|
// The human player is randomly assigned Black or White; the AI takes the other side.
|
|
func CreatePveRoom(owner *Player, difficulty int) (*NewRoom, error) {
|
|
if difficulty < 1 || difficulty > 3 {
|
|
return nil, ErrInvalidDifficulty
|
|
}
|
|
|
|
seed := rand.Int63()
|
|
var (
|
|
humanBlack bool
|
|
blackPlayerID int64
|
|
whitePlayerID int64
|
|
aiPiece game.Piece
|
|
)
|
|
// Random coin flip: even seed → human is Black.
|
|
if seed%2 == 0 {
|
|
humanBlack = true
|
|
blackPlayerID = owner.ID
|
|
whitePlayerID = -1
|
|
aiPiece = game.White
|
|
} else {
|
|
humanBlack = false
|
|
blackPlayerID = -1
|
|
whitePlayerID = owner.ID
|
|
aiPiece = game.Black
|
|
}
|
|
_ = humanBlack // used implicitly via blackPlayerID/whitePlayerID
|
|
|
|
aiSeed := rand.Int63()
|
|
ai := game.NewAI(aiPiece, aiSeed)
|
|
|
|
store.mu.Lock()
|
|
id := store.nextRoomID
|
|
store.nextRoomID++
|
|
r := &NewRoom{
|
|
ID: id,
|
|
OwnerID: owner.ID,
|
|
OwnerNickname: owner.Name,
|
|
RoomType: RoomTypePve,
|
|
Status: RoomStatusWaiting,
|
|
Board: game.NewBoard(),
|
|
BlackPlayerID: blackPlayerID,
|
|
WhitePlayerID: whitePlayerID,
|
|
CurrentTurn: game.Black,
|
|
Players: make(map[int64]*Player),
|
|
Spectators: make(map[int64]*Player),
|
|
MoveHistory: nil,
|
|
Difficulty: difficulty,
|
|
AI: ai,
|
|
CreatedAt: time.Now(),
|
|
LastActive: time.Now(),
|
|
}
|
|
store.rooms[id] = r
|
|
store.mu.Unlock()
|
|
return r, nil
|
|
}
|
|
|
|
// --- Room lookups ---
|
|
|
|
// GetNewRoom returns the room with the given ID, or (nil, false) if not found.
|
|
func GetNewRoom(id int64) (*NewRoom, bool) {
|
|
store.mu.RLock()
|
|
r, ok := store.rooms[id]
|
|
store.mu.RUnlock()
|
|
return r, ok
|
|
}
|
|
|
|
// GetAllRooms returns a snapshot slice of all rooms sorted by ID ascending.
|
|
// The slice is a copy; callers must not mutate rooms through it without locking.
|
|
func GetAllRooms() []*NewRoom {
|
|
store.mu.RLock()
|
|
list := make([]*NewRoom, 0, len(store.rooms))
|
|
for _, r := range store.rooms {
|
|
list = append(list, r)
|
|
}
|
|
store.mu.RUnlock()
|
|
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
|
|
return list
|
|
}
|
|
|
|
// deleteNewRoom removes a room from the store. Caller must hold store.mu.Lock().
|
|
func deleteNewRoom(id int64) {
|
|
delete(store.rooms, id)
|
|
}
|
|
|
|
// --- Room membership ---
|
|
|
|
// JoinNewRoom adds a player to a room as a human participant.
|
|
// Returns ErrRoomNotFound, ErrRoomFull, or ErrRoomPlaying on failure.
|
|
// On success, sets player.RoomID.
|
|
func JoinNewRoom(roomID int64, player *Player) error {
|
|
store.mu.RLock()
|
|
r, ok := store.rooms[roomID]
|
|
store.mu.RUnlock()
|
|
if !ok {
|
|
return ErrRoomNotFound
|
|
}
|
|
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
if r.Status == RoomStatusPlaying {
|
|
return ErrRoomPlaying
|
|
}
|
|
maxPlayers := 2
|
|
if r.RoomType == RoomTypePve {
|
|
maxPlayers = 1
|
|
}
|
|
if len(r.Players) >= maxPlayers {
|
|
return ErrRoomFull
|
|
}
|
|
|
|
r.Players[player.ID] = player
|
|
r.LastActive = time.Now()
|
|
player.RoomID = roomID
|
|
player.Role = RolePlayer
|
|
if r.OwnerID == player.ID {
|
|
player.Role = RoleOwner
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LeaveNewRoom removes a player from their current room (Players or Spectators).
|
|
// If the room becomes empty it is deleted from the store.
|
|
// Broadcasts nothing — callers handle messaging.
|
|
func LeaveNewRoom(player *Player) {
|
|
if player.RoomID == 0 {
|
|
return
|
|
}
|
|
|
|
store.mu.RLock()
|
|
r, ok := store.rooms[player.RoomID]
|
|
store.mu.RUnlock()
|
|
if !ok {
|
|
player.RoomID = 0
|
|
return
|
|
}
|
|
|
|
r.Lock()
|
|
|
|
wasPlayer := false
|
|
if _, found := r.Players[player.ID]; found {
|
|
delete(r.Players, player.ID)
|
|
wasPlayer = true
|
|
}
|
|
if _, found := r.Spectators[player.ID]; found {
|
|
delete(r.Spectators, player.ID)
|
|
}
|
|
|
|
// Owner reassignment: if owner left and room still has players, pick another.
|
|
if wasPlayer && r.OwnerID == player.ID && len(r.Players) > 0 {
|
|
for newOwnerID, newOwner := range r.Players {
|
|
r.OwnerID = newOwnerID
|
|
r.OwnerNickname = newOwner.Name
|
|
newOwner.Role = RoleOwner
|
|
break
|
|
}
|
|
}
|
|
|
|
empty := len(r.Players) == 0 && len(r.Spectators) == 0
|
|
roomID := r.ID
|
|
r.Unlock()
|
|
|
|
player.RoomID = 0
|
|
player.Role = ""
|
|
|
|
if empty {
|
|
store.mu.Lock()
|
|
deleteNewRoom(roomID)
|
|
store.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// WatchNewRoom adds a player to a room's Spectators list.
|
|
// Returns ErrRoomNotFound if the room does not exist.
|
|
func WatchNewRoom(roomID int64, player *Player) error {
|
|
store.mu.RLock()
|
|
r, ok := store.rooms[roomID]
|
|
store.mu.RUnlock()
|
|
if !ok {
|
|
return ErrRoomNotFound
|
|
}
|
|
|
|
r.Lock()
|
|
r.Spectators[player.ID] = player
|
|
r.LastActive = time.Now()
|
|
r.Unlock()
|
|
|
|
player.RoomID = roomID
|
|
player.Role = RoleSpectator
|
|
return nil
|
|
}
|
|
|
|
// UnwatchNewRoom removes a spectator from their room.
|
|
func UnwatchNewRoom(player *Player) {
|
|
if player.RoomID == 0 {
|
|
return
|
|
}
|
|
|
|
store.mu.RLock()
|
|
r, ok := store.rooms[player.RoomID]
|
|
store.mu.RUnlock()
|
|
if !ok {
|
|
player.RoomID = 0
|
|
return
|
|
}
|
|
|
|
r.Lock()
|
|
delete(r.Spectators, player.ID)
|
|
empty := len(r.Players) == 0 && len(r.Spectators) == 0
|
|
roomID := r.ID
|
|
r.Unlock()
|
|
|
|
player.RoomID = 0
|
|
player.Role = ""
|
|
|
|
if empty {
|
|
store.mu.Lock()
|
|
deleteNewRoom(roomID)
|
|
store.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// BroadcastToNewRoom calls each player's sendFn in Players + Spectators,
|
|
// skipping any IDs listed in excludePlayerIDs.
|
|
// Safe to call without holding any lock — acquires RLock internally.
|
|
// If sendFn is nil (phase-05 not yet wired) the call is a no-op for that player.
|
|
func BroadcastToNewRoom(room *NewRoom, msg string, excludePlayerIDs ...int64) {
|
|
exclude := make(map[int64]bool, len(excludePlayerIDs))
|
|
for _, id := range excludePlayerIDs {
|
|
exclude[id] = true
|
|
}
|
|
|
|
room.RLock()
|
|
targets := make([]*Player, 0, len(room.Players)+len(room.Spectators))
|
|
for id, p := range room.Players {
|
|
if !exclude[id] {
|
|
targets = append(targets, p)
|
|
}
|
|
}
|
|
for id, p := range room.Spectators {
|
|
if !exclude[id] {
|
|
targets = append(targets, p)
|
|
}
|
|
}
|
|
room.RUnlock()
|
|
|
|
// Send outside the lock to avoid holding room lock while doing I/O.
|
|
for _, p := range targets {
|
|
_ = p.WriteString(msg)
|
|
}
|
|
}
|