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

128 lines
3.4 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 billiards
import (
"math"
"testing"
)
// TestPhysicsStable 大力开球后所有球必须停下且不出界
func TestPhysicsStable(t *testing.T) {
g := NewGame()
res, err := g.Shoot(0, 0, 100)
if err != nil {
t.Fatalf("击球失败: %v", err)
}
if len(res.Frames) < 2 {
t.Fatal("模拟帧数过少")
}
for _, b := range g.Balls {
if !b.On {
continue
}
if b.X < BallR-1 || b.X > TableW-BallR+1 || b.Y < BallR-1 || b.Y > TableH-BallR+1 {
t.Fatalf("球 %d 停在界外: (%.1f, %.1f)", b.ID, b.X, b.Y)
}
if math.Hypot(b.vx, b.vy) > 0.01 {
t.Fatalf("球 %d 模拟结束仍在运动", b.ID)
}
}
}
// TestAIGame 两个 AI 互打 20 盘:每盘必须在 500 杆内分出胜负
func TestAIGame(t *testing.T) {
for round := 0; round < 20; round++ {
g := NewGame()
for shot := 0; shot < 500; shot++ {
if g.Phase == PhaseOver {
break
}
seat := g.Turn
angle, power := AIShot(g, seat, "hard")
if _, err := g.Shoot(seat, angle, power); err != nil {
t.Fatalf("第 %d 盘第 %d 杆失败: %v", round, shot, err)
}
}
if g.Phase != PhaseOver {
// 统计剩余球数辅助定位
left := 0
for _, b := range g.Balls {
if b.On && b.ID != 0 {
left++
}
}
t.Fatalf("第 %d 盘 500 杆仍未结束,桌面剩 %d 球", round, left)
}
if g.Winner != 0 && g.Winner != 1 {
t.Fatalf("第 %d 盘获胜座位非法: %d", round, g.Winner)
}
}
}
// TestSpinFollowDraw 高低杆:正对目标球满击后,高杆跟进、低杆回拉,停点应明显分化
// 场景:低力度短距离正碰,目标球滚动距离不足以撞库弹回,避免二次碰撞干扰停点
func TestSpinFollowDraw(t *testing.T) {
mk := func(spinY float64) float64 {
g := NewGame()
for _, b := range g.Balls {
if b.ID != 0 && b.ID != 1 {
b.On = false
}
}
g.Balls[0].X, g.Balls[0].Y = 60, 200
g.Balls[1].X, g.Balls[1].Y = 160, 200
if _, err := g.ShootSpin(0, 0, 30, 0, spinY); err != nil {
t.Fatalf("击球失败: %v", err)
}
return g.Balls[0].X
}
center := mk(0)
follow := mk(1)
draw := mk(-1)
if !(follow > center+30 && draw < center-30) {
t.Fatalf("高低杆效果异常: 中杆停点 %.1f 高杆 %.1f 低杆 %.1f", center, follow, draw)
}
}
// TestSpinEnglish 左右塞:白球正打右库,右塞反弹后向下偏、左塞向上偏
func TestSpinEnglish(t *testing.T) {
mk := func(spinX float64) float64 {
g := NewGame()
for _, b := range g.Balls {
if b.ID != 0 {
b.On = false
}
}
g.Balls[0].X, g.Balls[0].Y = 400, 200
if _, err := g.ShootSpin(0, 0, 50, spinX, 0); err != nil {
t.Fatalf("击球失败: %v", err)
}
return g.Balls[0].Y
}
right := mk(1)
left := mk(-1)
if !(right < 195 && left > 205) {
t.Fatalf("左右塞效果异常: 右塞停点 Y=%.1f 左塞停点 Y=%.1f(期望右塞<195 左塞>205", right, left)
}
}
// TestFoulRespot 白球落袋后重置回开球点附近且不与其他球重叠
func TestFoulRespot(t *testing.T) {
g := NewGame()
// 构造直接把白球打进左上角袋的场景
g.Balls[0].X, g.Balls[0].Y = 60, 60
res, err := g.Shoot(0, math.Atan2(-60, -60), 80)
if err != nil {
t.Fatalf("击球失败: %v", err)
}
if !g.Balls[0].On {
t.Fatal("白球落袋后应被重置回桌面")
}
if !res.Foul {
t.Fatal("白球落袋应判犯规")
}
if g.Turn != 1 {
t.Fatal("犯规后击球权应交给对方")
}
}