Files
2026-08-14 13:17:03 +08:00

453 lines
13 KiB
Go
Raw Permalink 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 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 连掷三个 6Plane 号棋子从 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
}