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

203 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 // 未连成
}