Files
zt-online-game-backend/internal/game/tictactoe/tictactoe.go
T

156 lines
5.4 KiB
Go

// Package tictactoe 是井字棋示例玩法:1v1 回合制。
// 新玩法照此结构新增子包并实现 game.Game,然后在 boot.InitWS 中注册即可。
package tictactoe
import (
"encoding/json" // 标准库:解析 place 动作参数
"errors" // 标准库:返回玩法内校验错误
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/game"
)
const gameID = "tictactoe" // 玩法 ID:匹配队列与房间都以它索引
// placePayload 是 place 动作的参数。
type placePayload struct {
Index int `json:"index"` // 落子位置 0-8
}
// state 是本玩法私有状态,房间只透传不解析。
type state struct {
Board [9]int8 `json:"board"` // 棋盘:0=空,1=先手,2=后手
Players []int64 `json:"players"` // 座位顺序:下标 0 为先手
Turn int `json:"turn"` // 当前应落子的玩家下标
Winner int64 `json:"winner"` // 已产生的获胜者,0 表示未分出
Moves int `json:"moves"` // 已落子数,用于判平局
}
// 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("tictactoe requires 2 seats")
}
return &state{}, nil // 返回空棋盘
}
// OnJoin 玩家入座:按加入顺序确定先后手。
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) // 记录座位顺序
return []game.Event{{Name: "seat", Payload: map[string]any{"user_id": uid, "seat": len(st.Players) - 1}}}, 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 // 默认 0(无人获胜)
if len(st.Players) == 2 { // 双人对局:判对手胜
winner = st.Players[1-i]
}
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.Index < 0 || p.Index > 8 { // 越界
return nil, nil, errors.New("index out of range")
}
if st.Board[p.Index] != 0 { // 该格已有子
return nil, nil, errors.New("cell occupied")
}
mark := int8(st.Turn + 1) // 先手=1,后手=2
st.Board[p.Index] = mark // 落子
st.Moves++ // 步数加一
events := []game.Event{{Name: "placed", Payload: map[string]any{"user_id": uid, "index": p.Index, "mark": mark}}}
if w := checkWin(st.Board); w != 0 { // 出现连线
st.Winner = st.Players[w-1] // 换算为用户 ID
return events, &game.Finish{Winner: st.Winner, Reason: "normal"}, nil
}
if st.Moves == 9 { // 棋盘下满:平局
return events, &game.Finish{Winner: 0, Reason: "normal"}, nil
}
st.Turn = 1 - st.Turn // 轮转到对手
return events, nil, nil
}
// OnTick 每秒驱动:回合制无需驱动,直接返回空。
func (g *Game) OnTick(now int64, s game.State) ([]game.Event, *game.Finish, error) {
return nil, nil, 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, // 棋盘
"players": st.Players, // 座位
"turn": st.Turn, // 当前轮次
"winner": st.Winner, // 获胜者
"moves": st.Moves, // 步数
}
}
// lines 是全部八条连线(三横、三竖、两斜)。
var lines = [8][3]int{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {2, 4, 6}}
// checkWin 返回连成一线的标记(1/2),无则返回 0。
func checkWin(b [9]int8) int8 {
for _, l := range lines { // 遍历八条线
if b[l[0]] != 0 && b[l[0]] == b[l[1]] && b[l[1]] == b[l[2]] { // 三格相同且非空
return b[l[0]] // 返回获胜标记
}
}
return 0 // 未分出胜负
}