108 lines
4.5 KiB
Go
108 lines
4.5 KiB
Go
// Package game 定义玩法插件的统一接口与注册表。
|
||
// 房间只通过该接口驱动玩法:新增一个游戏 = 新增实现 + 注册,网关/大厅/房间零改动。
|
||
package game
|
||
|
||
import (
|
||
"encoding/json" // 标准库:动作参数以原始 JSON 透传给玩法
|
||
"fmt" // 标准库:未注册玩法时返回明确错误
|
||
"sync" // 标准库:保护注册表并发读写
|
||
)
|
||
|
||
// Meta 描述玩法的匹配与房间形态:人数与节奏差异全部由它声明。
|
||
type Meta struct {
|
||
GameID string // 游戏唯一 ID,匹配队列与房间都用它索引
|
||
MinSeats int // 最少人数:双人=2,多人=4/6...
|
||
MaxSeats int // 最多人数
|
||
Realtime bool // true=实时玩法(依赖 OnTick 驱动),false=回合制
|
||
AllowJoin bool // 是否允许开局后中途加入
|
||
TickMs int // 房间驱动周期(毫秒):0 表示用房间默认 1 秒;实时玩法设小值(如贪吃蛇 200)
|
||
}
|
||
|
||
// Action 是一次玩家动作的通用载体:房间只透传,不解析内容。
|
||
type Action struct {
|
||
Name string // 动作名,玩法内部约定(如 "place")
|
||
Payload json.RawMessage // 动作参数原样 JSON,由玩法自行解析
|
||
}
|
||
|
||
// Event 描述「发给谁、发什么」:投递由房间完成,玩法不接触连接。
|
||
type Event struct {
|
||
To []int64 // 收件人;nil 或空表示全体广播
|
||
Name string // 事件名,客户端据此分发
|
||
Payload any // 事件内容
|
||
}
|
||
|
||
// Finish 是对局结果:由玩法返回,房间负责广播与结算。
|
||
type Finish struct {
|
||
Winner int64 // 获胜用户 ID,0 表示平局
|
||
Scores map[int64]int // 各用户得分,可为空
|
||
Reason string // 结束原因:normal / opponent_leave / timeout
|
||
}
|
||
|
||
// State 是玩法私有状态:房间只做透传,具体类型由玩法在 Init 中创建。
|
||
type State any
|
||
|
||
// Game 是所有玩法必须实现的接口,房间按序调用这些方法驱动一局游戏。
|
||
type Game interface {
|
||
Meta() Meta // 元信息:人数、是否实时等
|
||
Init(seatCount int) (State, error) // 开局初始化私有状态
|
||
OnJoin(uid int64, s State) ([]Event, error) // 玩家入座
|
||
OnLeave(uid int64, s State) ([]Event, *Finish, error) // 玩家离座;可返回 Finish 判负
|
||
OnAction(uid int64, a Action, s State) ([]Event, *Finish, error) // 玩家动作;返回 nil Finish 表示未结束
|
||
OnTick(now int64, s State) ([]Event, *Finish, error) // 每秒驱动:实时玩法与超时判定
|
||
Snapshot(uid int64, s State) any // 按观众裁剪的可见状态(重连/观战用)
|
||
}
|
||
|
||
// AIAction 是 AI 玩家要执行的一次动作:带执行者,便于房间代它调用 OnAction。
|
||
type AIAction struct {
|
||
UID int64 // 执行者(AI 的 uid)
|
||
Action Action // 要执行的动作
|
||
}
|
||
|
||
// AIBrain 是可选接口:玩法实现它即可支持「AI 补位」。
|
||
// 房间在每次推进前调用一次,把返回的动作代 AI 执行;不实现该接口的玩法不会被补 AI。
|
||
type AIBrain interface {
|
||
// TickAI 返回本回合所有 AI 玩家要执行的动作(含思考延迟由玩法内部决定)。
|
||
TickAI(now int64, s State) []AIAction
|
||
}
|
||
|
||
// Factory 创建玩法实例:每局一个新实例,保证状态隔离。
|
||
type Factory func() Game
|
||
|
||
var (
|
||
mu sync.RWMutex // 保护注册表
|
||
registry = map[string]Factory{} // gameID -> 构造函数
|
||
)
|
||
|
||
// Register 注册一个玩法;同名 ID 重复注册会覆盖(便于测试替换)。
|
||
func Register(gameID string, f Factory) {
|
||
mu.Lock() // 加写锁
|
||
defer mu.Unlock() // 返回时解锁
|
||
registry[gameID] = f
|
||
}
|
||
|
||
// New 按 ID 创建玩法实例。
|
||
func New(gameID string) (Game, error) {
|
||
mu.RLock() // 加读锁
|
||
f, ok := registry[gameID] // 查注册表
|
||
mu.RUnlock() // 解锁
|
||
if !ok { // 未注册
|
||
return nil, fmt.Errorf("game %q not registered", gameID)
|
||
}
|
||
return f(), nil // 返回新实例
|
||
}
|
||
|
||
// LookupMeta 取已注册玩法的元信息(匹配器用它决定队列人数)。
|
||
func LookupMeta(gameID string) (Meta, error) {
|
||
g, err := New(gameID) // 先建实例
|
||
if err != nil { // 未注册
|
||
return Meta{}, err
|
||
}
|
||
return g.Meta(), nil // 返回元信息
|
||
}
|
||
|
||
// HasAIBrain 判断玩法是否支持 AI 补位(实现了 AIBrain 接口)。
|
||
func HasAIBrain(g Game) bool {
|
||
_, ok := g.(AIBrain)
|
||
return ok
|
||
}
|