feat: initial commit with M1-M4 implementation
This commit is contained in:
40
internal/entities/player.go
Normal file
40
internal/entities/player.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package entities
|
||||
|
||||
type Team int
|
||||
|
||||
const (
|
||||
TeamRed Team = iota
|
||||
TeamBlue
|
||||
)
|
||||
|
||||
type Role int
|
||||
|
||||
const (
|
||||
RoleStriker Role = iota
|
||||
RoleDefender
|
||||
RoleGoalie
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
Position Vector2
|
||||
Velocity Vector2
|
||||
Radius float64
|
||||
Team Team
|
||||
Role Role
|
||||
HomePosition Vector2
|
||||
MaxSpeed float64
|
||||
Acceleration float64
|
||||
}
|
||||
|
||||
func NewPlayer(x, y float64, team Team, role Role) *Player {
|
||||
return &Player{
|
||||
Position: Vector2{X: x, Y: y},
|
||||
HomePosition: Vector2{X: x, Y: y},
|
||||
Velocity: Vector2{X: 0, Y: 0},
|
||||
Radius: 15,
|
||||
Team: team,
|
||||
Role: role,
|
||||
MaxSpeed: 3.0,
|
||||
Acceleration: 0.1,
|
||||
}
|
||||
}
|
||||
19
internal/entities/puck.go
Normal file
19
internal/entities/puck.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
type Vector2 struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
type Puck struct {
|
||||
Position Vector2
|
||||
Velocity Vector2
|
||||
Radius float64
|
||||
}
|
||||
|
||||
func NewPuck(x, y float64) *Puck {
|
||||
return &Puck{
|
||||
Position: Vector2{X: x, Y: y},
|
||||
Velocity: Vector2{X: 0, Y: 0},
|
||||
Radius: 10,
|
||||
}
|
||||
}
|
||||
258
internal/game/world.go
Normal file
258
internal/game/world.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/soer/football/internal/entities"
|
||||
)
|
||||
|
||||
const (
|
||||
Friction = 0.99
|
||||
Restitution = 0.8
|
||||
StopThreshold = 0.1
|
||||
WorldWidth = 1280.0
|
||||
WorldHeight = 720.0
|
||||
WorldCenterX = 640.0
|
||||
WorldCenterY = 360.0
|
||||
ZoneWidth = WorldWidth / 3
|
||||
GoalieMaxDistance = 80.0
|
||||
GoalieDeadzone = 1.0
|
||||
)
|
||||
|
||||
var (
|
||||
GoalLeft = entities.Vector2{X: 50, Y: WorldCenterY}
|
||||
GoalRight = entities.Vector2{X: 1230, Y: WorldCenterY}
|
||||
)
|
||||
|
||||
type World struct {
|
||||
Puck *entities.Puck
|
||||
Players []*entities.Player
|
||||
Width float64
|
||||
Height float64
|
||||
}
|
||||
|
||||
func NewWorld() *World {
|
||||
return &World{
|
||||
Puck: entities.NewPuck(WorldCenterX, WorldCenterY),
|
||||
Width: WorldWidth,
|
||||
Height: WorldHeight,
|
||||
Players: []*entities.Player{
|
||||
entities.NewPlayer(200, WorldCenterY, entities.TeamRed, entities.RoleDefender),
|
||||
entities.NewPlayer(400, WorldCenterY, entities.TeamRed, entities.RoleStriker),
|
||||
entities.NewPlayer(GoalLeft.X, GoalLeft.Y, entities.TeamRed, entities.RoleGoalie),
|
||||
entities.NewPlayer(1080, WorldCenterY, entities.TeamBlue, entities.RoleDefender),
|
||||
entities.NewPlayer(880, WorldCenterY, entities.TeamBlue, entities.RoleStriker),
|
||||
entities.NewPlayer(GoalRight.X, GoalRight.Y, entities.TeamBlue, entities.RoleGoalie),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) CheckGoal() (entities.Team, bool) {
|
||||
if w.Puck.Position.X < 0 {
|
||||
return entities.TeamBlue, true
|
||||
}
|
||||
if w.Puck.Position.X > w.Width {
|
||||
return entities.TeamRed, true
|
||||
}
|
||||
return entities.TeamRed, false
|
||||
}
|
||||
|
||||
func (w *World) ResetPositions() {
|
||||
w.Puck.Position = entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
||||
w.Puck.Velocity = entities.Vector2{X: 0, Y: 0}
|
||||
for _, p := range w.Players {
|
||||
p.Position = p.HomePosition
|
||||
p.Velocity = entities.Vector2{X: 0, Y: 0}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) Update() {
|
||||
// 1. Update Puck
|
||||
w.Puck.Velocity.X *= Friction
|
||||
w.Puck.Velocity.Y *= Friction
|
||||
|
||||
if (w.Puck.Velocity.X*w.Puck.Velocity.X + w.Puck.Velocity.Y*w.Puck.Velocity.Y) < StopThreshold*StopThreshold {
|
||||
w.Puck.Velocity.X = 0
|
||||
w.Puck.Velocity.Y = 0
|
||||
}
|
||||
|
||||
w.Puck.Position.X += w.Puck.Velocity.X
|
||||
w.Puck.Position.Y += w.Puck.Velocity.Y
|
||||
|
||||
if w.Puck.Position.X < w.Puck.Radius {
|
||||
w.Puck.Position.X = w.Puck.Radius
|
||||
w.Puck.Velocity.X *= -Restitution
|
||||
} else if w.Puck.Position.X > w.Width-w.Puck.Radius {
|
||||
w.Puck.Position.X = w.Width - w.Puck.Radius
|
||||
w.Puck.Velocity.X *= -Restitution
|
||||
}
|
||||
|
||||
if w.Puck.Position.Y < w.Puck.Radius {
|
||||
w.Puck.Position.Y = w.Puck.Radius
|
||||
w.Puck.Velocity.Y *= -Restitution
|
||||
} else if w.Puck.Position.Y > w.Height-w.Puck.Radius {
|
||||
w.Puck.Position.Y = w.Height - w.Puck.Radius
|
||||
w.Puck.Velocity.Y *= -Restitution
|
||||
}
|
||||
|
||||
// 2. Update Players
|
||||
for _, p := range w.Players {
|
||||
// AI Target
|
||||
target := w.calculateTarget(p, w.Puck.Position)
|
||||
w.applySteering(p, target)
|
||||
|
||||
// Anti-clumping (repulsion from teammates)
|
||||
for _, other := range w.Players {
|
||||
if p == other || p.Team != other.Team {
|
||||
continue
|
||||
}
|
||||
dx := p.Position.X - other.Position.X
|
||||
dy := p.Position.Y - other.Position.Y
|
||||
distSq := dx*dx + dy*dy
|
||||
if distSq < 40*40 && distSq > 0.01 {
|
||||
dist := math.Sqrt(distSq)
|
||||
p.Velocity.X += (dx / dist) * 0.1
|
||||
p.Velocity.Y += (dy / dist) * 0.1
|
||||
}
|
||||
}
|
||||
|
||||
// Apply movement
|
||||
p.Position.X += p.Velocity.X
|
||||
p.Position.Y += p.Velocity.Y
|
||||
|
||||
// Boundary clamping
|
||||
if p.Position.X < 0 {
|
||||
p.Position.X = 0
|
||||
} else if p.Position.X > w.Width {
|
||||
p.Position.X = w.Width
|
||||
}
|
||||
if p.Position.Y < 0 {
|
||||
p.Position.Y = 0
|
||||
} else if p.Position.Y > w.Height {
|
||||
p.Position.Y = w.Height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) applySteering(p *entities.Player, target entities.Vector2) {
|
||||
dx := target.X - p.Position.X
|
||||
dy := target.Y - p.Position.Y
|
||||
dist := math.Sqrt(dx*dx + dy*dy)
|
||||
|
||||
// Dead zone
|
||||
if dist < 1.0 {
|
||||
p.Velocity.X = 0
|
||||
p.Velocity.Y = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Desired velocity
|
||||
desiredX := (dx / dist) * p.MaxSpeed
|
||||
desiredY := (dy / dist) * p.MaxSpeed
|
||||
|
||||
// Steering force
|
||||
steeringX := desiredX - p.Velocity.X
|
||||
steeringY := desiredY - p.Velocity.Y
|
||||
steeringDist := math.Sqrt(steeringX*steeringX + steeringY*steeringY)
|
||||
|
||||
if steeringDist > p.Acceleration {
|
||||
steeringX = (steeringX / steeringDist) * p.Acceleration
|
||||
steeringY = (steeringY / steeringDist) * p.Acceleration
|
||||
}
|
||||
|
||||
p.Velocity.X += steeringX
|
||||
p.Velocity.Y += steeringY
|
||||
|
||||
// Cap final velocity
|
||||
speed := math.Sqrt(p.Velocity.X*p.Velocity.X + p.Velocity.Y*p.Velocity.Y)
|
||||
if speed > p.MaxSpeed {
|
||||
p.Velocity.X = (p.Velocity.X / speed) * p.MaxSpeed
|
||||
p.Velocity.Y = (p.Velocity.Y / speed) * p.MaxSpeed
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) getPuckZone(team entities.Team, puckPos entities.Vector2) int {
|
||||
// 0: Defensive, 1: Middle, 2: Offensive
|
||||
if team == entities.TeamRed {
|
||||
if puckPos.X < ZoneWidth {
|
||||
return 0
|
||||
}
|
||||
if puckPos.X < 2*ZoneWidth {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
} else {
|
||||
if puckPos.X > WorldWidth-ZoneWidth {
|
||||
return 0
|
||||
}
|
||||
if puckPos.X > WorldWidth-2*ZoneWidth {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) calculateTarget(p *entities.Player, puckPos entities.Vector2) entities.Vector2 {
|
||||
zone := w.getPuckZone(p.Team, puckPos)
|
||||
|
||||
goalPos := GoalLeft
|
||||
if p.Team == entities.TeamBlue {
|
||||
goalPos = GoalRight
|
||||
}
|
||||
|
||||
switch p.Role {
|
||||
case entities.RoleDefender:
|
||||
switch zone {
|
||||
case 0: // Defensive: Intercept puck
|
||||
return entities.Vector2{
|
||||
X: (puckPos.X + goalPos.X) / 2,
|
||||
Y: (puckPos.Y + goalPos.Y) / 2,
|
||||
}
|
||||
case 1: // Middle: Move towards puck but stay slightly behind it
|
||||
return entities.Vector2{
|
||||
X: puckPos.X + (goalPos.X-puckPos.X)*0.2,
|
||||
Y: puckPos.Y + (goalPos.Y-puckPos.Y)*0.2,
|
||||
}
|
||||
case 2: // Offensive: Return to Middle Zone
|
||||
return p.HomePosition
|
||||
}
|
||||
case entities.RoleStriker:
|
||||
switch zone {
|
||||
case 2: // Offensive: Aggressively pursue puck
|
||||
return puckPos
|
||||
case 1: // Middle: Pursue puck
|
||||
return puckPos
|
||||
case 0: // Defensive: Return to Middle Zone
|
||||
return entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
||||
}
|
||||
case entities.RoleGoalie:
|
||||
return w.updateGoalieAI(p, puckPos)
|
||||
}
|
||||
return p.HomePosition
|
||||
}
|
||||
|
||||
func (w *World) updateGoalieAI(p *entities.Player, puckPos entities.Vector2) entities.Vector2 {
|
||||
goalCenter := GoalLeft
|
||||
if p.Team == entities.TeamBlue {
|
||||
goalCenter = GoalRight
|
||||
}
|
||||
|
||||
// Check if puck is behind the goal
|
||||
if (p.Team == entities.TeamRed && puckPos.X < goalCenter.X) || (p.Team == entities.TeamBlue && puckPos.X > goalCenter.X) {
|
||||
return goalCenter
|
||||
}
|
||||
|
||||
dx := puckPos.X - goalCenter.X
|
||||
dy := puckPos.Y - goalCenter.Y
|
||||
dist := math.Sqrt(dx*dx + dy*dy)
|
||||
|
||||
if dist < GoalieDeadzone {
|
||||
return goalCenter
|
||||
}
|
||||
|
||||
moveDist := math.Min(dist, GoalieMaxDistance)
|
||||
return entities.Vector2{
|
||||
X: goalCenter.X + (dx/dist)*moveDist,
|
||||
Y: goalCenter.Y + (dy/dist)*moveDist,
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
47
internal/render/renderer.go
Normal file
47
internal/render/renderer.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
"github.com/soer/football/internal/entities"
|
||||
"github.com/soer/football/internal/game"
|
||||
)
|
||||
|
||||
type Renderer struct{}
|
||||
|
||||
func NewRenderer() *Renderer {
|
||||
return &Renderer{}
|
||||
}
|
||||
|
||||
func (r *Renderer) DrawWorld(screen *ebiten.Image, world *game.World) {
|
||||
// Draw Puck
|
||||
vector.DrawFilledCircle(
|
||||
screen,
|
||||
float32(world.Puck.Position.X),
|
||||
float32(world.Puck.Position.Y),
|
||||
float32(world.Puck.Radius),
|
||||
color.White,
|
||||
true,
|
||||
)
|
||||
|
||||
// Draw Players
|
||||
for _, p := range world.Players {
|
||||
var c color.Color
|
||||
if p.Team == entities.TeamRed {
|
||||
c = color.RGBA{R: 255, G: 0, B: 0, A: 255}
|
||||
} else {
|
||||
c = color.RGBA{R: 0, G: 0, B: 255, A: 255}
|
||||
}
|
||||
|
||||
vector.DrawFilledCircle(
|
||||
screen,
|
||||
float32(p.Position.X),
|
||||
float32(p.Position.Y),
|
||||
float32(p.Radius),
|
||||
c,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user