177 lines
5.0 KiB
Go
177 lines
5.0 KiB
Go
// 飞行棋核心自测: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 与 capture,capture 记录被击落者信息
|
||
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 事件缺失或字段错误")
|
||
}
|
||
}
|