实现游戏大厅与房间模块:玩法插件化、匹配与房间生命周期管理
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
// Package gobang 是五子棋玩法:1v1 回合制,15×15 棋盘,横竖斜任一方向先连成五子者胜。
|
||||
// 结构与 tictactoe 一致:实现 game.Game 接口,由房间驱动,不接触连接与数据库。
|
||||
package gobang
|
||||
|
||||
import (
|
||||
"encoding/json" // 标准库:解析 place 动作参数
|
||||
"errors" // 标准库:返回玩法内校验错误
|
||||
"time" // 标准库:记录上一手落子时刻,用于可选的回合超时
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/game"
|
||||
)
|
||||
|
||||
const (
|
||||
gameID = "gobang" // 玩法 ID:匹配队列与房间都以它索引
|
||||
boardSize = 15 // 棋盘边长
|
||||
winCount = 5 // 连成几子获胜
|
||||
|
||||
// turnTimeoutSec 单步思考时长上限(秒):0 表示不限制。
|
||||
// 需要「超时判负」时改成 90 之类的正数,OnTick 会在超时后判对手胜。
|
||||
turnTimeoutSec = 0
|
||||
)
|
||||
|
||||
// placePayload 是 place 动作的参数。
|
||||
type placePayload struct {
|
||||
Row int `json:"row"` // 行下标 0-14
|
||||
Col int `json:"col"` // 列下标 0-14
|
||||
}
|
||||
|
||||
// state 是本玩法私有状态,房间只透传不解析。
|
||||
type state struct {
|
||||
Board []int8 `json:"board"` // 棋盘:长度 225,0=空,1=黑(先手),2=白(后手)
|
||||
Players []int64 `json:"players"` // 座位顺序:下标 0 执黑先手
|
||||
Turn int `json:"turn"` // 当前应落子方下标
|
||||
Winner int64 `json:"winner"` // 获胜用户 ID,0 表示未分出
|
||||
Moves int `json:"moves"` // 已落子数
|
||||
LastMove int64 `json:"last_move"` // 上一手落子时刻(秒),回合超时判定用
|
||||
}
|
||||
|
||||
// Game 实现 game.Game 接口。
|
||||
type Game struct{}
|
||||
|
||||
// New 创建玩法实例:每局一个,状态互不影响。
|
||||
func New() *Game { return &Game{} }
|
||||
|
||||
// Meta 声明本玩法为 2 人回合制。
|
||||
func (g *Game) Meta() game.Meta {
|
||||
return game.Meta{GameID: gameID, MinSeats: 2, MaxSeats: 2, Realtime: false, AllowJoin: false}
|
||||
}
|
||||
|
||||
// Init 按座位数初始化空棋盘。
|
||||
func (g *Game) Init(seatCount int) (game.State, error) {
|
||||
if seatCount != 2 { // 五子棋固定两人
|
||||
return nil, errors.New("gobang requires 2 seats")
|
||||
}
|
||||
return &state{
|
||||
Board: make([]int8, boardSize*boardSize), // 225 格全空
|
||||
LastMove: time.Now().Unix(), // 起手时刻:回合超时从开局算起
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnJoin 玩家入座:先到者执黑(1),后到者执白(2)。
|
||||
func (g *Game) OnJoin(uid int64, s game.State) ([]game.Event, error) {
|
||||
st, ok := s.(*state) // 断言为本玩法状态
|
||||
if !ok {
|
||||
return nil, errors.New("invalid state type")
|
||||
}
|
||||
if len(st.Players) >= 2 { // 已满员
|
||||
return nil, errors.New("room is full")
|
||||
}
|
||||
st.Players = append(st.Players, uid) // 记录座位顺序
|
||||
color := len(st.Players) // 1=黑,2=白
|
||||
return []game.Event{{Name: "seat", Payload: map[string]any{
|
||||
"user_id": uid, "seat": color - 1, "color": color,
|
||||
}}}, nil
|
||||
}
|
||||
|
||||
// OnLeave 玩家离座:判对手胜。
|
||||
func (g *Game) OnLeave(uid int64, s game.State) ([]game.Event, *game.Finish, error) {
|
||||
st, ok := s.(*state) // 断言状态
|
||||
if !ok {
|
||||
return nil, nil, errors.New("invalid state type")
|
||||
}
|
||||
for i, p := range st.Players { // 找到离座者
|
||||
if p == uid {
|
||||
var winner int64 // 默认无人获胜
|
||||
if len(st.Players) == 2 { // 双人对局:判对手胜
|
||||
winner = st.Players[1-i]
|
||||
}
|
||||
st.Winner = winner
|
||||
return []game.Event{{Name: "leave", Payload: map[string]any{"user_id": uid}}},
|
||||
&game.Finish{Winner: winner, Reason: "opponent_leave"}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, nil // 非本局成员:无事件
|
||||
}
|
||||
|
||||
// OnAction 处理一次落子:校验轮次与位置,判定胜负或平局。
|
||||
func (g *Game) OnAction(uid int64, a game.Action, s game.State) ([]game.Event, *game.Finish, error) {
|
||||
st, ok := s.(*state) // 断言状态
|
||||
if !ok {
|
||||
return nil, nil, errors.New("invalid state type")
|
||||
}
|
||||
if len(st.Players) != 2 { // 人数不足
|
||||
return nil, nil, errors.New("game not started")
|
||||
}
|
||||
if uid != st.Players[st.Turn] { // 不是该玩家的回合
|
||||
return nil, nil, errors.New("not your turn")
|
||||
}
|
||||
if a.Name != "place" { // 未知动作
|
||||
return nil, nil, errors.New("unsupported action: " + a.Name)
|
||||
}
|
||||
|
||||
var p placePayload // 解析动作参数
|
||||
if err := json.Unmarshal(a.Payload, &p); err != nil {
|
||||
return nil, nil, errors.New("invalid place payload")
|
||||
}
|
||||
if p.Row < 0 || p.Row >= boardSize || p.Col < 0 || p.Col >= boardSize { // 越界
|
||||
return nil, nil, errors.New("position out of range")
|
||||
}
|
||||
idx := p.Row*boardSize + p.Col // 一维下标
|
||||
if st.Board[idx] != 0 { // 该点已有子
|
||||
return nil, nil, errors.New("cell occupied")
|
||||
}
|
||||
|
||||
mark := int8(st.Turn + 1) // 执黑=1,执白=2
|
||||
st.Board[idx] = mark // 落子
|
||||
st.Moves++ // 步数加一
|
||||
st.LastMove = time.Now().Unix() // 刷新最后落子时刻
|
||||
|
||||
events := []game.Event{{Name: "placed", Payload: map[string]any{
|
||||
"user_id": uid, "row": p.Row, "col": p.Col, "color": mark,
|
||||
}}}
|
||||
|
||||
if checkWin(st.Board, p.Row, p.Col, mark) { // 连成五子
|
||||
st.Winner = uid
|
||||
return events, &game.Finish{Winner: uid, Reason: "normal"}, nil
|
||||
}
|
||||
if st.Moves == boardSize*boardSize { // 棋盘填满:平局
|
||||
return events, &game.Finish{Winner: 0, Reason: "normal"}, nil
|
||||
}
|
||||
|
||||
st.Turn = 1 - st.Turn // 轮转到对手
|
||||
return events, nil, nil
|
||||
}
|
||||
|
||||
// OnTick 每秒驱动:回合制默认不做事;打开 turnTimeoutSec 后用于超时判负。
|
||||
func (g *Game) OnTick(now int64, s game.State) ([]game.Event, *game.Finish, error) {
|
||||
if turnTimeoutSec <= 0 { // 未开启超时判负
|
||||
return nil, nil, nil
|
||||
}
|
||||
st, ok := s.(*state) // 断言状态
|
||||
if !ok || len(st.Players) != 2 || st.Winner != 0 { // 未开局或已分胜负
|
||||
return nil, nil, nil
|
||||
}
|
||||
if now-st.LastMove < int64(turnTimeoutSec) { // 未超时
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
loser := st.Turn // 超时方
|
||||
winner := st.Players[1-loser] // 判对手胜
|
||||
st.Winner = winner // 记录结果
|
||||
return []game.Event{{Name: "timeout", Payload: map[string]any{"user_id": st.Players[loser]}}},
|
||||
&game.Finish{Winner: winner, Reason: "timeout"}, nil
|
||||
}
|
||||
|
||||
// Snapshot 生成对指定玩家可见的状态(五子棋无隐藏信息,返回全量)。
|
||||
func (g *Game) Snapshot(uid int64, s game.State) any {
|
||||
st, ok := s.(*state) // 断言状态
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"board": st.Board, // 棋盘(225)
|
||||
"players": st.Players, // 座位:0=黑,1=白
|
||||
"turn": st.Turn, // 当前轮次
|
||||
"winner": st.Winner, // 获胜者
|
||||
"moves": st.Moves, // 步数
|
||||
"size": boardSize, // 棋盘边长,便于客户端渲染
|
||||
}
|
||||
}
|
||||
|
||||
// dirs 是四个判定方向:横、竖、右下、右上。
|
||||
var dirs = [4][2]int{{1, 0}, {0, 1}, {1, 1}, {1, -1}}
|
||||
|
||||
// checkWin 以落点为中心,统计四个方向的同色连续长度是否达到 winCount。
|
||||
func checkWin(board []int8, row, col int, mark int8) bool {
|
||||
for _, d := range dirs { // 遍历四个方向
|
||||
count := 1 // 含落点自身
|
||||
for _, sign := range [2]int{1, -1} { // 向两侧延伸
|
||||
r, c := row+d[0]*sign, col+d[1]*sign
|
||||
for r >= 0 && r < boardSize && c >= 0 && c < boardSize && board[r*boardSize+c] == mark {
|
||||
count++ // 连续同色
|
||||
r += d[0] * sign // 继续延伸
|
||||
c += d[1] * sign
|
||||
}
|
||||
}
|
||||
if count >= winCount { // 达到五连
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false // 未连成
|
||||
}
|
||||
Reference in New Issue
Block a user