Files
nl-game-api/internal/gamecore/ludo/ludo_test.go
2026-08-14 13:17:03 +08:00

177 lines
5.0 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.
// 飞行棋核心自测AI 自对弈跑完整局,验证规则与终局收敛
package ludo
import "testing"
// TestFullGame 四个规则 AI 对局 50 盘,必须都能正常结束
func TestFullGame(t *testing.T) {
for round := 0; round < 50; round++ {
g := NewGame()
for actions := 0; actions < 20000; actions++ {
if g.Phase == PhaseOver {
break
}
seat := g.Turn
if g.Phase == PhaseRoll {
if err := g.Roll(seat); err != nil {
t.Fatalf("第 %d 盘掷骰失败: %v", round, err)
}
} else if g.Phase == PhaseMove {
plane := AIChoosePlane(g, seat, "hard")
if plane < 0 {
t.Fatalf("第 %d 盘 move 阶段却无可动棋子", round)
}
if err := g.Move(seat, plane); err != nil {
t.Fatalf("第 %d 盘移动失败: %v", round, err)
}
}
g.DrainLog()
}
if g.Phase != PhaseOver {
t.Fatalf("第 %d 盘 20000 步仍未结束Rolls=%d", round, g.Rolls)
}
if g.Winner < 0 || g.Winner > 3 {
t.Fatalf("第 %d 盘获胜座位非法: %d", round, g.Winner)
}
// 校验:所有棋子步数都在合法范围
for s := 0; s < 4; s++ {
for i, st := range g.Planes[s] {
if st < -1 || st > DoneStep {
t.Fatalf("第 %d 盘 P%d 棋子 %d 步数越界: %d", round, s, i, st)
}
}
}
}
}
// TestCapture 击落规则:对手棋子被踩后回机库,并产生 capture 动画事件
func TestCapture(t *testing.T) {
g := NewGame()
// 座位1 的棋子放在座位0 起点前方第 6 格(绝对格 6
// 座位1 起点为 13绝对格6 = (13+steps)%52 → steps = 45
g.Planes[1][0] = 45
// 座位0 掷 6 起飞一架,再走 6 步(手动模拟到第 6 格)
g.Planes[0][0] = 0
g.Die = 6
g.Turn = 0
g.Phase = PhaseMove
if err := g.Move(0, 0); err != nil {
t.Fatalf("移动失败: %v", err)
}
if g.Planes[1][0] != -1 {
t.Fatalf("对手棋子应被击落回机库,实际步数 %d", g.Planes[1][0])
}
// 事件流校验:应包含 move 与 capturecapture 记录被击落者信息
var hasMove, hasCapture bool
for _, e := range g.DrainEvents() {
if e.Kind == "move" && e.Seat == 0 && e.From == 0 && e.To == 6 {
hasMove = true
}
if e.Kind == "capture" && e.Victim == 1 && e.VPlane == 0 && e.VFrom == 45 {
hasCapture = true
}
}
if !hasMove || !hasCapture {
t.Fatalf("事件流缺失 move/capture: move=%v capture=%v", hasMove, hasCapture)
}
}
// TestFlyChain 飞跃航线:落在相对 20 步的飞行格 → 飞到 32 → 衔接跳跃到 36
func TestFlyChain(t *testing.T) {
g := NewGame()
g.Planes[0][0] = 15
g.Die = 5 // 15+5=20 恰好踩中飞行格
g.Turn = 0
g.Phase = PhaseMove
if err := g.Move(0, 0); err != nil {
t.Fatalf("移动失败: %v", err)
}
if g.Planes[0][0] != 36 {
t.Fatalf("飞跃连锁应停在 36实际 %d", g.Planes[0][0])
}
var hasFly, hasJump bool
for _, e := range g.DrainEvents() {
if e.Kind == "fly" && e.From == FlyFrom && e.To == FlyTo {
hasFly = true
}
if e.Kind == "jump" && e.From == FlyTo && e.To == FlyTo+4 {
hasJump = true
}
}
if !hasFly || !hasJump {
t.Fatalf("事件流缺失 fly/jump: fly=%v jump=%v", hasFly, hasJump)
}
// 跳跃衔接:落在 16色格→ 跳到 20飞行格→ 飞到 32 → 再跳到 36
g2 := NewGame()
g2.Planes[1][0] = 12
g2.Die = 4
g2.Turn = 1
g2.Phase = PhaseMove
if err := g2.Move(1, 0); err != nil {
t.Fatalf("移动失败: %v", err)
}
if g2.Planes[1][0] != 36 {
t.Fatalf("跳跃衔接飞跃应停在 36实际 %d", g2.Planes[1][0])
}
}
// TestTripleSix 连掷三个 6最近移动的主航道棋子被遣返轮到下家
func TestTripleSix(t *testing.T) {
g := NewGame()
g.Planes[0][2] = 10
g.LastMv = [2]int{0, 2}
g.Turn = 0
g.punishTripleSix(0)
if g.Planes[0][2] != -1 {
t.Fatalf("棋子应被遣返机库,实际 %d", g.Planes[0][2])
}
if g.Turn == 0 {
t.Fatal("惩罚后应轮到下家")
}
found := false
for _, e := range g.DrainEvents() {
if e.Kind == "sixpenalty" && e.Seat == 0 && e.Plane == 2 && e.VFrom == 10 {
found = true
}
}
if !found {
t.Fatal("sixpenalty 事件缺失或字段错误")
}
// 无主航道棋子可罚:只作废回合
g2 := NewGame()
g2.Planes[0][0] = 52 // 终点跑道内不受罚
g2.Turn = 0
g2.punishTripleSix(0)
if g2.Planes[0][0] != 52 {
t.Fatal("终点跑道棋子不应被遣返")
}
if g2.Turn == 0 {
t.Fatal("无棋可罚也应轮到下家")
}
}
// TestMoveEvents 掷骰必产生 roll 事件;反弹时 move 事件的 Die 可还原路径
func TestMoveEvents(t *testing.T) {
g := NewGame()
// 棋子在 54 步,掷 5 → 54+5=59 > 56反弹到 53
g.Planes[0][0] = 54
g.Die = 5
g.Turn = 0
g.Phase = PhaseMove
if err := g.Move(0, 0); err != nil {
t.Fatalf("移动失败: %v", err)
}
if g.Planes[0][0] != 53 {
t.Fatalf("反弹计算错误,期望 53 实际 %d", g.Planes[0][0])
}
found := false
for _, e := range g.DrainEvents() {
if e.Kind == "move" && e.From == 54 && e.To == 53 && e.Die == 5 {
found = true
}
}
if !found {
t.Fatal("反弹 move 事件缺失或字段错误")
}
}