453 lines
13 KiB
Go
453 lines
13 KiB
Go
// Package ludo 飞行棋核心规则:52 格环形主航道 + 各家 6 格终点跑道
|
||
// 掷 6 可起飞并奖励再掷一次;落在对方棋子上将其击落回机库;落在己方颜色格向前跳 4 格
|
||
// 棋子步数编码:-1=机库,0..49=主航道(相对自家起点),50..55=终点跑道,56=到达终点
|
||
package ludo
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand"
|
||
)
|
||
|
||
// 阶段
|
||
const (
|
||
PhaseRoll = "roll" // 等待当前玩家掷骰
|
||
PhaseMove = "move" // 等待当前玩家选择要动的棋子
|
||
PhaseOver = "over" // 对局结束
|
||
)
|
||
|
||
// 棋盘常量
|
||
const (
|
||
TrackLen = 52 // 主航道格数
|
||
MainSteps = 50 // 主航道上要走的步数(之后进入终点跑道)
|
||
DoneStep = 56 // 到达终点的步数值
|
||
PlaneCount = 4 // 每人棋子数
|
||
SeatGap = 13 // 相邻玩家起点间隔格数
|
||
MaxRolls = 400 // 总掷骰次数上限(防止无限对局)
|
||
FlyFrom = 20 // 飞行格:踩中己方相对第 20 步的色格沿虚线航道飞跃
|
||
FlyTo = 32 // 飞跃落点:相对第 32 步(+12 格,落点仍是己方色格可衔接跳跃)
|
||
)
|
||
|
||
// Event 结构化动画事件:随状态广播下发,前端据此按顺序播放骰子/飞行/击落等动效
|
||
// Kind 取值:
|
||
// roll 掷骰(Die 为点数)
|
||
// nomove 无棋可动,轮到下家
|
||
// takeoff 起飞:机库 → 自家起点格
|
||
// move 沿航道逐步前进(From→To 为步数域;含终点反弹,配合 Die 还原路径)
|
||
// jump 踩中己方颜色格向前跳 4 格(From→To)
|
||
// fly 踩中飞行格沿虚线航道飞跃(From→To,+12 格)
|
||
// capture 击落:Victim 座位的 VPlane 号棋子从 VFrom 步被送回机库
|
||
// arrive 棋子到达终点
|
||
// bonus 掷出 6 点奖励再掷一次
|
||
// sixpenalty 连掷三个 6:Plane 号棋子从 VFrom 步遣返机库(Plane=-1 表示无棋可罚)
|
||
type Event struct {
|
||
Kind string `json:"kind"` // 事件类型
|
||
Seat int `json:"seat"` // 动作主体座位
|
||
Plane int `json:"plane"` // 主体棋子编号
|
||
Die int `json:"die"` // roll/move:骰子点数
|
||
From int `json:"from"` // move/jump:起始步数
|
||
To int `json:"to"` // move/jump:结束步数
|
||
Victim int `json:"victim"` // capture:被击落座位
|
||
VPlane int `json:"vplane"` // capture:被击落棋子编号
|
||
VFrom int `json:"vfrom"` // capture:被击落时所在步数(按其自家步数域)
|
||
}
|
||
|
||
// Game 一局飞行棋(固定 4 座位)
|
||
type Game struct {
|
||
Planes [4][PlaneCount]int // 每个座位 4 架飞机的步数
|
||
Turn int // 当前行动座位
|
||
Phase string // roll / move / over
|
||
Die int // 当前骰子点数(0 表示尚未掷)
|
||
Winner int // 获胜座位(-1 未定)
|
||
Active [4]bool // 座位是否仍在对局中(中途退出置 false)
|
||
Rolls int // 累计掷骰次数
|
||
Sixes [4]int // 各座位连续掷出 6 的次数(第三个连 6 触发遣返惩罚)
|
||
LastMv [2]int // 最近移动 [座位, 棋子](前端高亮,-1 表示无)
|
||
Log []string // 动作描述(房间取走后广播)
|
||
Events []Event // 最近动作的动画事件(房间取走后随状态下发)
|
||
}
|
||
|
||
// NewGame 创建对局:全部飞机在机库,0 号座位先手
|
||
func NewGame() *Game {
|
||
g := &Game{Turn: 0, Phase: PhaseRoll, Winner: -1, LastMv: [2]int{-1, -1}}
|
||
for s := 0; s < 4; s++ {
|
||
g.Active[s] = true
|
||
for i := 0; i < PlaneCount; i++ {
|
||
g.Planes[s][i] = -1
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
|
||
// logf 追加一条动作描述
|
||
func (g *Game) logf(format string, args ...any) {
|
||
g.Log = append(g.Log, fmt.Sprintf(format, args...))
|
||
}
|
||
|
||
// DrainLog 取走并清空累计的动作描述
|
||
func (g *Game) DrainLog() []string {
|
||
out := g.Log
|
||
g.Log = nil
|
||
return out
|
||
}
|
||
|
||
// ev 追加一条动画事件
|
||
func (g *Game) ev(e Event) {
|
||
g.Events = append(g.Events, e)
|
||
}
|
||
|
||
// DrainEvents 取走并清空累计的动画事件
|
||
func (g *Game) DrainEvents() []Event {
|
||
out := g.Events
|
||
g.Events = nil
|
||
return out
|
||
}
|
||
|
||
// cellOf 步数换算主航道绝对格号(仅 0..49 有效)
|
||
func cellOf(seat, steps int) int {
|
||
return (seat*SeatGap + steps) % TrackLen
|
||
}
|
||
|
||
// Movable 当前骰子下可移动的棋子下标列表
|
||
// 掷出 6 时机库棋子可起飞;场上棋子(未到终点)总是可动(超出终点按反弹处理)
|
||
func (g *Game) Movable(seat int) []int {
|
||
if g.Die == 0 {
|
||
return nil
|
||
}
|
||
var list []int
|
||
for i, s := range g.Planes[seat] {
|
||
if s == -1 {
|
||
if g.Die == 6 {
|
||
list = append(list, i)
|
||
}
|
||
} else if s < DoneStep {
|
||
list = append(list, i)
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|
||
// Roll 当前玩家掷骰:无棋可动直接轮到下家;唯一可动棋子自动移动
|
||
func (g *Game) Roll(seat int) error {
|
||
if g.Phase != PhaseRoll {
|
||
return fmt.Errorf("当前不在掷骰阶段")
|
||
}
|
||
if g.Turn != seat {
|
||
return fmt.Errorf("还没轮到你掷骰")
|
||
}
|
||
g.Die = rand.Intn(6) + 1
|
||
g.Rolls++
|
||
// 连续 6 点计数:掷出非 6 立即清零
|
||
if g.Die == 6 {
|
||
g.Sixes[seat]++
|
||
} else {
|
||
g.Sixes[seat] = 0
|
||
}
|
||
movable := g.Movable(seat)
|
||
g.ev(Event{Kind: "roll", Seat: seat, Die: g.Die})
|
||
g.logf("P%d 掷出了 %d 点", seat, g.Die)
|
||
if g.Rolls >= MaxRolls {
|
||
g.finishByProgress()
|
||
return nil
|
||
}
|
||
// 连掷三个 6:触发遣返惩罚,本次不能行动
|
||
if g.Sixes[seat] >= 3 {
|
||
g.Sixes[seat] = 0
|
||
g.punishTripleSix(seat)
|
||
return nil
|
||
}
|
||
if len(movable) == 0 {
|
||
g.ev(Event{Kind: "nomove", Seat: seat})
|
||
g.logf("P%d 无棋可动,轮到下家", seat)
|
||
g.advanceTurn()
|
||
return nil
|
||
}
|
||
if len(movable) == 1 {
|
||
// 只有一种选择:自动执行,省一次点击
|
||
g.applyMove(seat, movable[0])
|
||
return nil
|
||
}
|
||
g.Phase = PhaseMove
|
||
return nil
|
||
}
|
||
|
||
// Move 当前玩家选择棋子移动
|
||
func (g *Game) Move(seat, plane int) error {
|
||
if g.Phase != PhaseMove {
|
||
return fmt.Errorf("当前不在选棋阶段")
|
||
}
|
||
if g.Turn != seat {
|
||
return fmt.Errorf("还没轮到你行动")
|
||
}
|
||
if plane < 0 || plane >= PlaneCount {
|
||
return fmt.Errorf("棋子编号不合法")
|
||
}
|
||
legal := false
|
||
for _, i := range g.Movable(seat) {
|
||
if i == plane {
|
||
legal = true
|
||
}
|
||
}
|
||
if !legal {
|
||
return fmt.Errorf("这架飞机现在不能移动")
|
||
}
|
||
g.applyMove(seat, plane)
|
||
return nil
|
||
}
|
||
|
||
// applyMove 执行移动:起飞/前进/反弹/击落/同色跳跃/到达判定,然后决定是否连掷
|
||
func (g *Game) applyMove(seat, plane int) {
|
||
steps := g.Planes[seat][plane]
|
||
if steps == -1 {
|
||
// 起飞:落到自家起点格
|
||
g.Planes[seat][plane] = 0
|
||
g.ev(Event{Kind: "takeoff", Seat: seat, Plane: plane})
|
||
g.logf("P%d 的 %d 号飞机起飞!", seat, plane+1)
|
||
g.captureAt(seat, 0)
|
||
} else {
|
||
next := steps + g.Die
|
||
if next > DoneStep {
|
||
// 终点跑道冲过头:按多出的步数反弹
|
||
next = 2*DoneStep - next
|
||
}
|
||
g.Planes[seat][plane] = next
|
||
g.ev(Event{Kind: "move", Seat: seat, Plane: plane, Die: g.Die, From: steps, To: next})
|
||
if next == DoneStep {
|
||
g.ev(Event{Kind: "arrive", Seat: seat, Plane: plane})
|
||
g.logf("P%d 的 %d 号飞机到达终点!", seat, plane+1)
|
||
} else if next >= MainSteps {
|
||
g.logf("P%d 的 %d 号飞机进入终点跑道", seat, plane+1)
|
||
} else {
|
||
g.captureAt(seat, next)
|
||
// 第一段:己方色格跳跃 +4(不连锁;直接落在飞行格时交给下面的飞跃处理)
|
||
if next != FlyFrom && cellOf(seat, next)%4 == seat && next+4 < MainSteps {
|
||
g.Planes[seat][plane] = next + 4
|
||
g.ev(Event{Kind: "jump", Seat: seat, Plane: plane, From: next, To: next + 4})
|
||
g.logf("P%d 的 %d 号飞机踩中己方颜色格,向前跳 4 格", seat, plane+1)
|
||
next += 4
|
||
g.captureAt(seat, next)
|
||
}
|
||
// 第二段:飞行格飞跃 +12(直接落上或跳跃衔接到飞行格都会触发)
|
||
if next == FlyFrom {
|
||
g.Planes[seat][plane] = FlyTo
|
||
g.ev(Event{Kind: "fly", Seat: seat, Plane: plane, From: FlyFrom, To: FlyTo})
|
||
g.logf("P%d 的 %d 号飞机踩中飞行格,沿虚线航道飞跃 12 格!", seat, plane+1)
|
||
next = FlyTo
|
||
g.captureAt(seat, next)
|
||
// 第三段:飞跃落点仍是己方色格,衔接一次跳跃收尾(经典连锁)
|
||
if cellOf(seat, next)%4 == seat && next+4 < MainSteps {
|
||
g.Planes[seat][plane] = next + 4
|
||
g.ev(Event{Kind: "jump", Seat: seat, Plane: plane, From: next, To: next + 4})
|
||
g.logf("P%d 的 %d 号飞机飞跃落点又是己方色格,再跳 4 格!", seat, plane+1)
|
||
next += 4
|
||
g.captureAt(seat, next)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
g.LastMv = [2]int{seat, plane}
|
||
// 全部到达 → 获胜
|
||
done := 0
|
||
for _, s := range g.Planes[seat] {
|
||
if s == DoneStep {
|
||
done++
|
||
}
|
||
}
|
||
if done == PlaneCount {
|
||
g.Winner = seat
|
||
g.Phase = PhaseOver
|
||
g.logf("P%d 四架飞机全部到达,获得胜利!", seat)
|
||
return
|
||
}
|
||
// 掷出 6 奖励再掷一次,否则轮到下家
|
||
if g.Die == 6 {
|
||
g.Die = 0
|
||
g.Phase = PhaseRoll
|
||
g.ev(Event{Kind: "bonus", Seat: seat})
|
||
g.logf("P%d 掷出 6 点,奖励再掷一次", seat)
|
||
return
|
||
}
|
||
g.advanceTurn()
|
||
}
|
||
|
||
// punishTripleSix 连掷三个 6 的惩罚:优先遣返最近移动的主航道棋子,
|
||
// 其次遣返最靠前的主航道棋子;无棋可罚则只作废本回合。惩罚后轮到下家(不再奖励连掷)
|
||
func (g *Game) punishTripleSix(seat int) {
|
||
victim := -1
|
||
if g.LastMv[0] == seat && g.LastMv[1] >= 0 {
|
||
if s := g.Planes[seat][g.LastMv[1]]; s >= 0 && s < MainSteps {
|
||
victim = g.LastMv[1]
|
||
}
|
||
}
|
||
if victim == -1 {
|
||
best := -1
|
||
for i, s := range g.Planes[seat] {
|
||
if s >= 0 && s < MainSteps && s > best {
|
||
best, victim = s, i
|
||
}
|
||
}
|
||
}
|
||
if victim >= 0 {
|
||
g.ev(Event{Kind: "sixpenalty", Seat: seat, Plane: victim, VFrom: g.Planes[seat][victim]})
|
||
g.Planes[seat][victim] = -1
|
||
g.logf("P%d 连掷三个 6 触发惩罚,%d 号飞机被遣返回机库!", seat, victim+1)
|
||
} else {
|
||
g.ev(Event{Kind: "sixpenalty", Seat: seat, Plane: -1})
|
||
g.logf("P%d 连掷三个 6 触发惩罚,本回合作废", seat)
|
||
}
|
||
g.advanceTurn()
|
||
}
|
||
|
||
// captureAt 击落:把停在指定步数对应主航道格上的所有他人棋子送回机库
|
||
func (g *Game) captureAt(seat, steps int) {
|
||
if steps < 0 || steps >= MainSteps {
|
||
return
|
||
}
|
||
cell := cellOf(seat, steps)
|
||
for other := 0; other < 4; other++ {
|
||
if other == seat || !g.Active[other] {
|
||
continue
|
||
}
|
||
for i, s := range g.Planes[other] {
|
||
if s >= 0 && s < MainSteps && cellOf(other, s) == cell {
|
||
g.Planes[other][i] = -1
|
||
g.ev(Event{Kind: "capture", Seat: seat, Victim: other, VPlane: i, VFrom: s})
|
||
g.logf("P%d 击落了 P%d 的 %d 号飞机!", seat, other, i+1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// advanceTurn 轮到下一个仍在对局中的座位
|
||
func (g *Game) advanceTurn() {
|
||
g.Die = 0
|
||
g.Phase = PhaseRoll
|
||
for i := 0; i < 4; i++ {
|
||
g.Turn = (g.Turn + 1) % 4
|
||
if g.Active[g.Turn] {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// progressOf 进度评分:到达 +100,场上棋子按步数累计
|
||
func (g *Game) progressOf(seat int) int {
|
||
total := 0
|
||
for _, s := range g.Planes[seat] {
|
||
if s == DoneStep {
|
||
total += 100
|
||
} else if s >= 0 {
|
||
total += s
|
||
}
|
||
}
|
||
return total
|
||
}
|
||
|
||
// finishByProgress 掷骰次数打满:按进度最高者获胜
|
||
func (g *Game) finishByProgress() {
|
||
best, bestSeat := -1, 0
|
||
for s := 0; s < 4; s++ {
|
||
if !g.Active[s] {
|
||
continue
|
||
}
|
||
if p := g.progressOf(s); p > best {
|
||
best, bestSeat = p, s
|
||
}
|
||
}
|
||
g.Winner = bestSeat
|
||
g.Phase = PhaseOver
|
||
g.logf("回合数已达上限,按进度判定 P%d 获胜!", bestSeat)
|
||
}
|
||
|
||
// ForceFinish 提前按进度结算(房间层在真人全部退出时调用,避免纯 AI 空跑)
|
||
func (g *Game) ForceFinish() {
|
||
if g.Phase != PhaseOver {
|
||
g.finishByProgress()
|
||
}
|
||
}
|
||
|
||
// Eliminate 玩家中途退出:棋子全部收回,不再参与轮转
|
||
func (g *Game) Eliminate(seat int) {
|
||
if !g.Active[seat] {
|
||
return
|
||
}
|
||
g.Active[seat] = false
|
||
for i := range g.Planes[seat] {
|
||
g.Planes[seat][i] = -1
|
||
}
|
||
if g.Phase != PhaseOver && g.Turn == seat {
|
||
g.advanceTurn()
|
||
}
|
||
}
|
||
|
||
// AIChoosePlane 规则 AI 选棋:按「击落 > 到达 > 进跑道 > 起飞 > 领先棋子推进」估值
|
||
// easy 随机选;medium 大概率选最优;hard 永远选最优
|
||
func AIChoosePlane(g *Game, seat int, difficulty string) int {
|
||
movable := g.Movable(seat)
|
||
if len(movable) == 0 {
|
||
return -1
|
||
}
|
||
if difficulty == "easy" || (difficulty == "medium" && rand.Float64() < 0.25) {
|
||
return movable[rand.Intn(len(movable))]
|
||
}
|
||
bestPlane, bestScore := movable[0], -1<<30
|
||
for _, i := range movable {
|
||
score := 0
|
||
steps := g.Planes[seat][i]
|
||
if steps == -1 {
|
||
// 起飞:机库棋子越多越值得起飞
|
||
hangar := 0
|
||
for _, s := range g.Planes[seat] {
|
||
if s == -1 {
|
||
hangar++
|
||
}
|
||
}
|
||
score = 30 + hangar*5
|
||
if g.enemyOnCell(seat, cellOf(seat, 0)) {
|
||
score += 100
|
||
}
|
||
} else {
|
||
next := steps + g.Die
|
||
if next > DoneStep {
|
||
next = 2*DoneStep - next
|
||
}
|
||
switch {
|
||
case next == DoneStep:
|
||
score = 80
|
||
case next >= MainSteps:
|
||
score = 40 + next
|
||
default:
|
||
score = next
|
||
if g.enemyOnCell(seat, cellOf(seat, next)) {
|
||
score += 100
|
||
}
|
||
if cellOf(seat, next)%4 == seat {
|
||
score += 15
|
||
}
|
||
// 落点是飞行格:飞跃 +12 再衔接跳跃,收益极高
|
||
if next == FlyFrom {
|
||
score += 45
|
||
}
|
||
}
|
||
}
|
||
if score > bestScore {
|
||
bestScore, bestPlane = score, i
|
||
}
|
||
}
|
||
return bestPlane
|
||
}
|
||
|
||
// enemyOnCell 指定主航道格上是否有他人棋子(击落估值用)
|
||
func (g *Game) enemyOnCell(seat, cell int) bool {
|
||
for other := 0; other < 4; other++ {
|
||
if other == seat || !g.Active[other] {
|
||
continue
|
||
}
|
||
for _, s := range g.Planes[other] {
|
||
if s >= 0 && s < MainSteps && cellOf(other, s) == cell {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|