// 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 // 未分出胜负 }