Files
gomoku/server/database/database.go
T
tiennm99 5ccd4e7ce2 feat: add documentation, code comments, and update Docker config
Rewrite README with usage guide, deployment instructions, and protocol
docs. Update CLAUDE.md to reflect gomoku-only architecture. Add English
doc comments to all key server Go files, replacing Chinese comments.

Create docs/system-architecture.md (state machine, protocol, database
schema) and docs/deployment-guide.md (local dev, Docker, production
nginx, resource requirements).

Update Dockerfile to Go 1.22 with repo-root build context to include
web client. Update docker-compose to match.
2026-04-09 23:35:46 +07:00

425 lines
9.5 KiB
Go

package database
import (
"sort"
stringx "strings"
"sync/atomic"
"time"
"github.com/awesome-cap/hashmap"
"github.com/ratel-online/core/log"
modelx "github.com/ratel-online/core/model"
"github.com/ratel-online/core/network"
"github.com/ratel-online/core/util/async"
"github.com/ratel-online/core/util/json"
"github.com/ratel-online/core/util/strings"
"github.com/ratel-online/server/consts"
)
// In-memory data store. All state is volatile — server restart clears everything.
var roomIds int64 = 0
var players = hashmap.New() // all players ever connected (by ID)
var connPlayers = hashmap.New() // currently connected players
var rooms = hashmap.New() // active rooms (by room ID)
var roomPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
var roomSpectators = hashmap.New() // map[roomID] -> map[playerID]int (join order)
var roomKickedPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
var roomPropsSetter = map[string]func(r *Room, v string){
consts.RoomPropsPassword: func(r *Room, v string) {
if v == "off" {
r.Password = ""
} else {
r.Password = v
}
},
consts.RoomPropsShowIP: func(r *Room, v string) {
r.EnableShowIP = v == "on"
},
}
func init() {
async.Async(func() {
loopCount := 0
for {
loopCount++
if loopCount%60 == 0 {
log.Infof("[database.init] Room cleanup loop count: %d (running for %d hours)\n", loopCount, loopCount/60)
}
time.Sleep(1 * time.Minute)
rooms.Foreach(func(e *hashmap.Entry) {
roomCancel(e.Value().(*Room))
})
}
})
}
func Connected(conn *network.Conn, info *modelx.AuthInfo) *Player {
player := &Player{
ID: conn.ID(),
IP: conn.IP(),
Name: strings.Desensitize(info.Name),
Amount: 2000,
}
player.Conn(conn)
players.Set(conn.ID(), player)
connPlayers.Set(conn.ID(), player)
return player
}
func CreateRoom(creator int64, t int) *Room {
room := &Room{
ID: atomic.AddInt64(&roomIds, 1),
Type: t,
State: consts.RoomStateWaiting,
Creator: creator,
ActiveTime: time.Now(),
MaxPlayers: 2,
EnableChat: true,
}
roomPlayers.Set(room.ID, map[int64]bool{})
roomSpectators.Set(room.ID, map[int64]int{})
rooms.Set(room.ID, room)
return room
}
func deleteRoom(room *Room) {
if room != nil {
rooms.Del(room.ID)
roomPlayers.Del(room.ID)
roomSpectators.Del(room.ID)
if room.Game != nil {
room.Game.Clean()
}
}
}
func GetRooms() []*Room {
list := make([]*Room, 0)
rooms.Foreach(func(e *hashmap.Entry) {
list = append(list, e.Value().(*Room))
})
sort.Slice(list, func(i, j int) bool {
return list[i].ID < list[j].ID
})
return list
}
func GetRoom(roomId int64) *Room {
return getRoom(roomId)
}
func getRoom(roomId int64) *Room {
if v, ok := rooms.Get(roomId); ok {
return v.(*Room)
}
return nil
}
func getPlayer(playerId int64) *Player {
if v, ok := players.Get(playerId); ok {
return v.(*Player)
}
return nil
}
func SetRoomProps(room *Room, k, v string) {
// Only allow properties appropriate for the game type
allowedProps := getAllowedPropsByGameType(room.Type)
if !allowedProps[k] {
return
}
if setter, ok := roomPropsSetter[k]; ok {
setter(room, v)
}
}
// getAllowedPropsByGameType returns allowed room properties for each game type.
func getAllowedPropsByGameType(gameType int) map[string]bool {
return map[string]bool{
consts.RoomPropsShowIP: true,
consts.RoomPropsPassword: true,
}
}
func getRoomPlayers(roomId int64) map[int64]bool {
if v, ok := roomPlayers.Get(roomId); ok {
return v.(map[int64]bool)
}
return nil
}
func getRoomSpectators(roomId int64) map[int64]int {
if v, ok := roomSpectators.Get(roomId); ok {
return v.(map[int64]int)
}
return nil
}
func IsValidPlayer(roomId, playerId int64) bool {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
playersIds := getRoomPlayers(roomId)
if playersIds != nil {
_, exists := playersIds[playerId]
if exists {
return true
}
}
spectatorsIds := getRoomSpectators(roomId)
if spectatorsIds != nil {
_, exists := spectatorsIds[playerId]
if exists {
return true
}
}
return false
}
return false
}
// JoinRoom adds a player to a room. If the room is full or running, the player
// becomes a spectator instead. Returns an error if the player was previously kicked.
func JoinRoom(roomId, playerId int64) error {
player := getPlayer(playerId)
if player == nil {
return consts.ErrorsExist
}
room := getRoom(roomId)
if room == nil {
return consts.ErrorsRoomInvalid
}
room.Lock()
defer room.Unlock()
if hasKicked(roomId, playerId) {
return consts.ErrorsJoinFailForKicked
}
room.ActiveTime = time.Now()
if room.Players >= room.MaxPlayers || room.State == consts.RoomStateRunning {
spectatorsIds := getRoomSpectators(roomId)
spectatorsIds[playerId] = len(spectatorsIds)
player.RoomID = roomId
player.Role = RoleSpectator
} else {
playersIds := getRoomPlayers(roomId)
playersIds[playerId] = true
room.Players++
player.RoomID = roomId
player.Role = RolePlayer
if room.Creator == playerId {
player.Role = RoleOwner
}
}
return nil
}
func LeaveRoom(roomId, playerId int64) {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
leaveRoom(room, getPlayer(playerId))
}
}
func Backfill(roomId int64) *Player {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
return backfill(room)
}
return nil
}
func backfill(room *Room) *Player {
if room.Players >= room.MaxPlayers {
return nil
}
spectatorsIds := getRoomSpectators(room.ID)
if len(spectatorsIds) == 0 {
return nil
}
spectators := make([]struct {
id int64
index int
}, 0)
for id, index := range spectatorsIds {
spectators = append(spectators, struct {
id int64
index int
}{id: id, index: index})
}
sort.Slice(spectators, func(i, j int) bool {
return spectators[i].index < spectators[j].index
})
playerId := spectators[0].id
delete(spectatorsIds, playerId)
playersIds := getRoomPlayers(room.ID)
playersIds[playerId] = true
room.Players++
player := getPlayer(playerId)
if player != nil {
player.Role = RolePlayer
}
return player
}
func Kicking(roomId, playerId int64) {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
leaveRoom(room, getPlayer(playerId))
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
if !ok {
kickedPlayers = map[int64]bool{}
roomKickedPlayers.Set(roomId, kickedPlayers)
}
kickedPlayers.(map[int64]bool)[playerId] = true
}
}
func hasKicked(roomId, playerId int64) bool {
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
if !ok {
return false
}
_, exists := kickedPlayers.(map[int64]bool)[playerId]
return exists
}
func leaveRoom(room *Room, player *Player) {
if room == nil || player == nil {
return
}
room.ActiveTime = time.Now()
playersIds := getRoomPlayers(room.ID)
if _, ok := playersIds[player.ID]; ok {
room.Players--
player.RoomID = 0
player.Role = ""
delete(playersIds, player.ID)
if len(playersIds) > 0 && room.Creator == player.ID {
for k := range playersIds {
room.Creator = k
if p := getPlayer(k); p != nil {
p.Role = RoleOwner
}
break
}
}
}
spectatorsIds := getRoomSpectators(room.ID)
if _, ok := spectatorsIds[player.ID]; ok {
player.RoomID = 0
player.Role = ""
delete(spectatorsIds, player.ID)
}
if len(playersIds) == 0 && len(spectatorsIds) == 0 {
deleteRoom(room)
}
}
func roomCancel(room *Room) {
if room.ActiveTime.Add(24 * time.Hour).Before(time.Now()) {
log.Infof("room %d is timeout 24 hours, removed.\n", room.ID)
deleteRoom(room)
return
}
living := false
playerIds := getRoomPlayers(room.ID)
for id := range playerIds {
if getPlayer(id).online {
living = true
break
}
}
spectatorIds := getRoomSpectators(room.ID)
for id := range spectatorIds {
if getPlayer(id).online {
living = true
break
}
}
if !living {
log.Infof("room %d is not living, removed.\n", room.ID)
deleteRoom(room)
}
}
func RoomPlayers(roomId int64) map[int64]bool {
return getRoomPlayers(roomId)
}
func RoomSpectators(roomId int64) map[int64]int {
return getRoomSpectators(roomId)
}
func broadcast(room *Room, msg string, exclude ...int64) {
room.ActiveTime = time.Now()
excludeSet := map[int64]bool{}
for _, exc := range exclude {
excludeSet[exc] = true
}
for playerId := range getRoomPlayers(room.ID) {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(">> " + msg)
}
}
for playerId := range getRoomSpectators(room.ID) {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(">> " + msg)
}
}
}
func Broadcast(roomId int64, msg string, exclude ...int64) {
room := getRoom(roomId)
if room == nil {
return
}
broadcast(room, msg, exclude...)
}
func BroadcastChat(player *Player, msg string, exclude ...int64) {
log.Infof("chat msg, player %s[%d] %s say: %s\n", player.Name, player.ID, player.IP, stringx.TrimSpace(msg))
Broadcast(player.RoomID, strings.Desensitize(msg), exclude...)
}
func BroadcastObject(roomId int64, object interface{}, exclude ...int64) {
room := getRoom(roomId)
if room == nil {
return
}
room.Lock()
defer room.Unlock()
excludeSet := map[int64]bool{}
for _, exc := range exclude {
excludeSet[exc] = true
}
msg := json.Marshal(object)
playerIds := getRoomPlayers(roomId)
for playerId := range playerIds {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(string(msg))
}
}
}
func GetPlayer(playerId int64) *Player {
return getPlayer(playerId)
}