156 lines
5.4 KiB
Go
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 // 未分出胜负
|
|
}
|