// 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 // 未连成 }