feat: initial commit with M1-M4 implementation
This commit is contained in:
233
internal/game/world_test.go
Normal file
233
internal/game/world_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"github.com/soer/football/internal/entities"
|
||||
)
|
||||
|
||||
const epsilon = 1e-9
|
||||
|
||||
func TestWorld_Friction(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
w.Puck.Velocity = entities.Vector2{X: 10, Y: 0}
|
||||
|
||||
w.Update()
|
||||
|
||||
expectedX := 10.0 * Friction
|
||||
if math.Abs(w.Puck.Velocity.X-expectedX) > epsilon {
|
||||
t.Errorf("Expected velocity X to be %f, got %f", expectedX, w.Puck.Velocity.X)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_StopThreshold(t *testing.T) {
|
||||
w := NewWorld()
|
||||
// Set velocity just below StopThreshold
|
||||
w.Puck.Velocity = entities.Vector2{X: StopThreshold * 0.5, Y: 0}
|
||||
|
||||
w.Update()
|
||||
|
||||
if math.Abs(w.Puck.Velocity.X) > epsilon || math.Abs(w.Puck.Velocity.Y) > epsilon {
|
||||
t.Errorf("Expected velocity to be 0, got {%f, %f}", w.Puck.Velocity.X, w.Puck.Velocity.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_Restitution(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
w.Puck.Position = entities.Vector2{X: 8, Y: 360}
|
||||
w.Puck.Velocity = entities.Vector2{X: -2, Y: 0}
|
||||
|
||||
w.Update()
|
||||
|
||||
expectedX := (-2.0 * Friction) * -Restitution
|
||||
if math.Abs(w.Puck.Velocity.X-expectedX) > epsilon {
|
||||
t.Errorf("Expected velocity X to be %f, got %f", expectedX, w.Puck.Velocity.X)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_ZoningAI(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
// TeamRed Defender
|
||||
redDef := &entities.Player{
|
||||
Position: entities.Vector2{X: 200, Y: 360},
|
||||
HomePosition: entities.Vector2{X: 200, Y: 360},
|
||||
Team: entities.TeamRed,
|
||||
Role: entities.RoleDefender,
|
||||
}
|
||||
|
||||
// TeamRed Striker
|
||||
redStr := &entities.Player{
|
||||
Position: entities.Vector2{X: 400, Y: 360},
|
||||
HomePosition: entities.Vector2{X: 400, Y: 360},
|
||||
Team: entities.TeamRed,
|
||||
Role: entities.RoleStriker,
|
||||
}
|
||||
|
||||
// Case 1: Puck in Defensive Zone (Zone 0)
|
||||
puckPos0 := entities.Vector2{X: 100, Y: 360}
|
||||
|
||||
// Defender should intercept
|
||||
targetDef0 := w.calculateTarget(redDef, puckPos0)
|
||||
expectedDef0 := entities.Vector2{X: (100.0 + GoalLeft.X) / 2, Y: 360}
|
||||
if targetDef0 != expectedDef0 {
|
||||
t.Errorf("Zone 0: Expected Red Defender to target %v, got %v", expectedDef0, targetDef0)
|
||||
}
|
||||
|
||||
// Striker should return to center
|
||||
targetStr0 := w.calculateTarget(redStr, puckPos0)
|
||||
expectedStr0 := entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
||||
if targetStr0 != expectedStr0 {
|
||||
t.Errorf("Zone 0: Expected Red Striker to target %v, got %v", expectedStr0, targetStr0)
|
||||
}
|
||||
|
||||
// Case 2: Puck in Middle Zone (Zone 1)
|
||||
puckPos1 := entities.Vector2{X: 600, Y: 360}
|
||||
|
||||
// Defender should stay slightly behind
|
||||
targetDef1 := w.calculateTarget(redDef, puckPos1)
|
||||
expectedDef1 := entities.Vector2{X: 600 + (GoalLeft.X-600)*0.2, Y: 360}
|
||||
if targetDef1 != expectedDef1 {
|
||||
t.Errorf("Zone 1: Expected Red Defender to target %v, got %v", expectedDef1, targetDef1)
|
||||
}
|
||||
|
||||
// Striker should pursue
|
||||
targetStr1 := w.calculateTarget(redStr, puckPos1)
|
||||
if targetStr1 != puckPos1 {
|
||||
t.Errorf("Zone 1: Expected Red Striker to target puck %v, got %v", puckPos1, targetStr1)
|
||||
}
|
||||
|
||||
// Case 3: Puck in Offensive Zone (Zone 2)
|
||||
puckPos2 := entities.Vector2{X: 1000, Y: 360}
|
||||
|
||||
// Defender should return home
|
||||
targetDef2 := w.calculateTarget(redDef, puckPos2)
|
||||
if targetDef2 != redDef.HomePosition {
|
||||
t.Errorf("Zone 2: Expected Red Defender to target HomePosition %v, got %v", redDef.HomePosition, targetDef2)
|
||||
}
|
||||
|
||||
// Striker should aggressively pursue
|
||||
targetStr2 := w.calculateTarget(redStr, puckPos2)
|
||||
if targetStr2 != puckPos2 {
|
||||
t.Errorf("Zone 2: Expected Red Striker to target puck %v, got %v", puckPos2, targetStr2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_GoalieAI(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
// Create a Red Goalie
|
||||
redGoalie := &entities.Player{
|
||||
Position: entities.Vector2{X: GoalLeft.X, Y: GoalLeft.Y},
|
||||
HomePosition: entities.Vector2{X: GoalLeft.X, Y: GoalLeft.Y},
|
||||
Team: entities.TeamRed,
|
||||
Role: entities.RoleGoalie,
|
||||
}
|
||||
|
||||
// Case 1: Puck is far away. Target should be at GoalieMaxDistance from goal.
|
||||
w.Puck.Position = entities.Vector2{X: 500, Y: 360}
|
||||
target := w.calculateTarget(redGoalie, w.Puck.Position)
|
||||
|
||||
dx := target.X - GoalLeft.X
|
||||
dy := target.Y - GoalLeft.Y
|
||||
dist := math.Sqrt(dx*dx + dy*dy)
|
||||
if math.Abs(dist-GoalieMaxDistance) > epsilon {
|
||||
t.Errorf("Expected Red Goalie to target distance %f, got %f", GoalieMaxDistance, dist)
|
||||
}
|
||||
|
||||
// Case 2: Puck is close. Target should be exactly at puck position.
|
||||
w.Puck.Position = entities.Vector2{X: GoalLeft.X + 10, Y: GoalLeft.Y + 10}
|
||||
target = w.calculateTarget(redGoalie, w.Puck.Position)
|
||||
if target != w.Puck.Position {
|
||||
t.Errorf("Expected Red Goalie to target puck position when close, got %v", target)
|
||||
}
|
||||
|
||||
// Case 3: Puck is behind the goal. Target should be goal center.
|
||||
w.Puck.Position = entities.Vector2{X: GoalLeft.X - 10, Y: GoalLeft.Y}
|
||||
target = w.calculateTarget(redGoalie, w.Puck.Position)
|
||||
if target != GoalLeft {
|
||||
t.Errorf("Expected Red Goalie to target goal center when puck is behind, got %v", target)
|
||||
}
|
||||
|
||||
// Case 4: Puck is within GoalieDeadzone. Target should be goal center.
|
||||
w.Puck.Position = entities.Vector2{X: GoalLeft.X + GoalieDeadzone*0.5, Y: GoalLeft.Y}
|
||||
target = w.calculateTarget(redGoalie, w.Puck.Position)
|
||||
if target != GoalLeft {
|
||||
t.Errorf("Expected Red Goalie to target goal center when puck is within deadzone, got %v", target)
|
||||
}
|
||||
|
||||
// Case 5: Blue Goalie
|
||||
blueGoalie := &entities.Player{
|
||||
Position: entities.Vector2{X: GoalRight.X, Y: GoalRight.Y},
|
||||
HomePosition: entities.Vector2{X: GoalRight.X, Y: GoalRight.Y},
|
||||
Team: entities.TeamBlue,
|
||||
Role: entities.RoleGoalie,
|
||||
}
|
||||
w.Puck.Position = entities.Vector2{X: 700, Y: 360}
|
||||
target = w.calculateTarget(blueGoalie, w.Puck.Position)
|
||||
dx = target.X - GoalRight.X
|
||||
dy = target.Y - GoalRight.Y
|
||||
dist = math.Sqrt(dx*dx + dy*dy)
|
||||
if math.Abs(dist-GoalieMaxDistance) > epsilon {
|
||||
t.Errorf("Expected Blue Goalie to target distance %f, got %f", GoalieMaxDistance, dist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_CheckGoal(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
// Case 1: Puck goes off left side (Blue scores)
|
||||
w.Puck.Position = entities.Vector2{X: -1, Y: 360}
|
||||
team, scored := w.CheckGoal()
|
||||
if !scored || team != entities.TeamBlue {
|
||||
t.Errorf("Expected TeamBlue to score, got %v, %v", team, scored)
|
||||
}
|
||||
|
||||
// Case 2: Puck goes off right side (Red scores)
|
||||
w.Puck.Position = entities.Vector2{X: WorldWidth + 1, Y: 360}
|
||||
team, scored = w.CheckGoal()
|
||||
if !scored || team != entities.TeamRed {
|
||||
t.Errorf("Expected TeamRed to score, got %v, %v", team, scored)
|
||||
}
|
||||
|
||||
// Case 3: Puck is in bounds
|
||||
w.Puck.Position = entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
||||
team, scored = w.CheckGoal()
|
||||
if scored {
|
||||
t.Errorf("Expected no goal, but scored: %v", team)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorld_ResetPositions(t *testing.T) {
|
||||
w := NewWorld()
|
||||
|
||||
// Move puck and players away from home
|
||||
w.Puck.Position = entities.Vector2{X: 100, Y: 100}
|
||||
w.Puck.Velocity = entities.Vector2{X: 5, Y: 5}
|
||||
for _, p := range w.Players {
|
||||
p.Position = entities.Vector2{X: 500, Y: 500}
|
||||
p.Velocity = entities.Vector2{X: 2, Y: 2}
|
||||
}
|
||||
|
||||
w.ResetPositions()
|
||||
|
||||
// Verify puck
|
||||
if w.Puck.Position != (entities.Vector2{X: WorldCenterX, Y: WorldCenterY}) {
|
||||
t.Errorf("Expected puck at center, got %v", w.Puck.Position)
|
||||
}
|
||||
if w.Puck.Velocity != (entities.Vector2{X: 0, Y: 0}) {
|
||||
t.Errorf("Expected puck velocity 0, got %v", w.Puck.Velocity)
|
||||
}
|
||||
|
||||
// Verify players
|
||||
for _, p := range w.Players {
|
||||
if p.Position != p.HomePosition {
|
||||
t.Errorf("Expected player at home position %v, got %v", p.HomePosition, p.Position)
|
||||
}
|
||||
if p.Velocity != (entities.Vector2{X: 0, Y: 0}) {
|
||||
t.Errorf("Expected player velocity 0, got %v", p.Velocity)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user