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, } }