A competitive AI programming environment where developers create intelligent tank controllers to battle against each other in a 2D arena. This project serves as both an educational tool for learning AI programming concepts and a competitive platform for testing different AI strategies.
Tank Game is a real-time combat simulation where AI-controlled tanks fight in a physics-based arena. Each tank is controlled by a custom AI script that makes decisions about movement, aiming, and firing. The last tank standing wins!
- Real-time Combat: Tanks move, aim, and fire projectiles with realistic physics
- Multiple AI Support: Run up to 15 different AI implementations simultaneously
- Physics-Based Movement: Acceleration, friction, and collision detection
- Health & Damage System: Tanks start with 100 HP, projectiles deal 5 damage
- Firing Mechanics: 0.5-second cooldown between shots prevents spam
- Arena Boundaries: 600x450 pixel battlefield with water obstacles
- Visual Feedback: SDL3-based graphics with colorful tank identification
- Arena Size: 600x450 pixels
- Tank Health: 100 HP (eliminated at 0 HP)
- Projectile Damage: 5 HP per hit
- Fire Cooldown: 0.5 seconds between shots
- Tank Speed: Max 150 pixels/second with acceleration
- Turn Rate: 180 degrees per second (π radians/second)
- Tank Colors: Red, Green, Blue, Yellow, Magenta, Cyan
SDL3 Library is required for graphics rendering.
- Download
sdl3.dllfrom the SDL3 GitHub releases - Place
sdl3.dllin the project root directory
sudo apt-get install libsdl3 libsdl3-imagesudo dnf install SDL3 SDL3_imagebrew install sdl3-
Clone the repository:
git clone <repository-url> cd tank-game
-
Install Go dependencies:
go mod tidy
-
Run the game:
go run main.go
The game will automatically load all registered AI implementations and start the battle!
The game runs automatically once started. You'll see:
- Colored tanks representing different AI implementations
- Projectiles flying across the battlefield
- Real-time combat as AIs make decisions and battle
- Health indicators showing tank status
- Victory condition when only one tank remains
This is an AI-only game - there are no manual controls. All tanks are controlled by their respective AI scripts.
Every AI must implement the game.AI interface with three methods:
type AI interface {
GetName() string
Initialize(tank *Tank, gameState *GameState) error
Update(tank *Tank, gameState *GameState, commands *Commands) error
}Your AI can control the tank using these commands:
type Commands struct {
MoveForward bool // Accelerate forward
MoveBackward bool // Accelerate backward
TurnLeft bool // Turn counterclockwise
TurnRight bool // Turn clockwise
Fire bool // Shoot projectile (if cooldown ready)
}Your AI has access to your tank's properties:
// Position and movement
tank.Position // Vector2 - current position
tank.Rotation // float64 - current rotation in radians
tank.Velocity // Vector2 - current velocity
// Combat stats
tank.Health // int - current health (0-100)
tank.IsAlive // bool - whether tank is still active
tank.CanFire() // bool - whether tank can fire (cooldown ready)
// Physics properties
tank.MaxSpeed // float64 - maximum speed (150)
tank.ViewRange // float64 - sight range (200)Your AI can access global game information:
// All tanks in the game
gameState.Tanks // []*Tank - all tanks (including yours)
gameState.Projectiles // []*Projectile - all active projectiles
// Arena information
gameState.ArenaWidth // float64 - arena width (1024)
gameState.ArenaHeight // float64 - arena height (512)
gameState.GameTime // float64 - elapsed game time
// Utility methods
gameState.GetAliveTanks() // Get only living tanks
gameState.IsValidPosition(pos, radius) // Check if position is validHere's a basic AI that moves forward and shoots at the nearest enemy:
package ai
import (
"math"
"tank-game/lib/game"
)
type SimpleAI struct {
name string
}
func NewSimpleAI() *SimpleAI {
return &SimpleAI{name: "Simple AI"}
}
func (ai *SimpleAI) GetName() string {
return ai.name
}
func (ai *SimpleAI) Initialize(tank *game.Tank, gameState *game.GameState) error {
return nil
}
func (ai *SimpleAI) Update(tank *game.Tank, gameState *game.GameState, commands *game.Commands) error {
// Always move forward
commands.MoveForward = true
// Find nearest enemy
nearestEnemy := ai.findNearestEnemy(tank, gameState)
if nearestEnemy != nil {
// Calculate angle to enemy
dx := nearestEnemy.Position.X - tank.Position.X
dy := nearestEnemy.Position.Y - tank.Position.Y
targetAngle := math.Atan2(dy, dx)
// Calculate angle difference
angleDiff := targetAngle - tank.Rotation
// Normalize angle (-π to π)
for angleDiff > math.Pi {
angleDiff -= 2 * math.Pi
}
for angleDiff < -math.Pi {
angleDiff += 2 * math.Pi
}
// Turn towards enemy
if angleDiff > 0.1 {
commands.TurnRight = true
} else if angleDiff < -0.1 {
commands.TurnLeft = true
}
// Fire if aimed at enemy
if math.Abs(angleDiff) < 0.2 {
commands.Fire = true
}
}
return nil
}
func (ai *SimpleAI) findNearestEnemy(myTank *game.Tank, gameState *game.GameState) *game.Tank {
var nearest *game.Tank
minDistance := math.MaxFloat64
for _, tank := range gameState.Tanks {
if tank == myTank || !tank.IsAlive {
continue
}
dx := tank.Position.X - myTank.Position.X
dy := tank.Position.Y - myTank.Position.Y
distance := math.Sqrt(dx*dx + dy*dy)
if distance < minDistance {
minDistance = distance
nearest = tank
}
}
return nearest
}Here's a more sophisticated AI with projectile dodging and tactical positioning:
package ai
import (
"math"
"tank-game/lib/common"
"tank-game/lib/game"
)
type AdvancedAI struct {
name string
lastDodgeTime float64
targetPosition *common.Vector2
patrolRadius float64
}
func NewAdvancedAI() *AdvancedAI {
return &AdvancedAI{
name: "Advanced AI",
patrolRadius: 150.0,
}
}
func (ai *AdvancedAI) GetName() string {
return ai.name
}
func (ai *AdvancedAI) Initialize(tank *game.Tank, gameState *game.GameState) error {
// Set initial patrol center
ai.targetPosition = &common.Vector2{
X: tank.Position.X,
Y: tank.Position.Y,
}
return nil
}
func (ai *AdvancedAI) Update(tank *game.Tank, gameState *game.GameState, commands *game.Commands) error {
// Priority 1: Dodge incoming projectiles
if ai.shouldDodge(tank, gameState) {
ai.performDodge(tank, gameState, commands)
ai.lastDodgeTime = gameState.GameTime
return nil
}
// Priority 2: Attack nearest enemy
nearestEnemy := ai.findNearestEnemy(tank, gameState)
if nearestEnemy != nil && ai.getDistance(tank.Position, nearestEnemy.Position) < tank.ViewRange {
ai.attackEnemy(tank, nearestEnemy, commands)
return nil
}
// Priority 3: Tactical positioning
ai.tacticalMovement(tank, gameState, commands)
return nil
}
func (ai *AdvancedAI) shouldDodge(tank *game.Tank, gameState *game.GameState) bool {
// Don't dodge too frequently
if gameState.GameTime-ai.lastDodgeTime < 1.0 {
return false
}
// Check for incoming projectiles
for _, projectile := range gameState.Projectiles {
if !projectile.IsActive {
continue
}
// Calculate if projectile is heading towards us
distance := ai.getDistance(tank.Position, projectile.Position)
if distance < 100 { // Danger zone
// Predict projectile path
futurePos := common.Vector2{
X: projectile.Position.X + projectile.Velocity.X*0.5,
Y: projectile.Position.Y + projectile.Velocity.Y*0.5,
}
if ai.getDistance(tank.Position, futurePos) < 30 {
return true
}
}
}
return false
}
func (ai *AdvancedAI) performDodge(tank *game.Tank, gameState *game.GameState, commands *game.Commands) {
// Find safest direction to dodge
bestAngle := tank.Rotation + math.Pi/2 // Default: turn 90 degrees
// Check multiple dodge directions
for angle := 0.0; angle < 2*math.Pi; angle += math.Pi / 4 {
testPos := common.Vector2{
X: tank.Position.X + math.Cos(angle)*50,
Y: tank.Position.Y + math.Sin(angle)*50,
}
if ai.isPositionSafe(testPos, gameState) {
bestAngle = angle
break
}
}
// Turn towards safe direction
angleDiff := bestAngle - tank.Rotation
ai.normalizeAngle(&angleDiff)
if angleDiff > 0.1 {
commands.TurnRight = true
} else if angleDiff < -0.1 {
commands.TurnLeft = true
}
commands.MoveForward = true
}
func (ai *AdvancedAI) attackEnemy(tank *game.Tank, enemy *game.Tank, commands *game.Commands) {
distance := ai.getDistance(tank.Position, enemy.Position)
// Maintain optimal combat distance
optimalDistance := 120.0
if distance > optimalDistance+20 {
// Too far - move closer
ai.moveTowards(tank, enemy.Position, commands)
} else if distance < optimalDistance-20 {
// Too close - back away
commands.MoveBackward = true
}
// Aim at enemy with prediction
predictedPos := ai.predictEnemyPosition(enemy, 0.3) // Predict 0.3 seconds ahead
ai.aimAt(tank, predictedPos, commands)
// Fire if well-aimed
targetAngle := math.Atan2(predictedPos.Y-tank.Position.Y, predictedPos.X-tank.Position.X)
aimError := targetAngle - tank.Rotation
ai.normalizeAngle(&aimError)
if math.Abs(aimError) < 0.15 && tank.CanFire() {
commands.Fire = true
}
}
func (ai *AdvancedAI) tacticalMovement(tank *game.Tank, gameState *game.GameState, commands *game.Commands) {
// Move to strategic position
centerX := gameState.ArenaWidth / 2
centerY := gameState.ArenaHeight / 2
// Patrol around center with some randomness
patrolAngle := gameState.GameTime * 0.5 // Slow patrol
targetX := centerX + math.Cos(patrolAngle)*ai.patrolRadius
targetY := centerY + math.Sin(patrolAngle)*ai.patrolRadius
targetPos := common.Vector2{X: targetX, Y: targetY}
ai.moveTowards(tank, targetPos, commands)
}
func (ai *AdvancedAI) moveTowards(tank *game.Tank, target common.Vector2, commands *game.Commands) {
targetAngle := math.Atan2(target.Y-tank.Position.Y, target.X-tank.Position.X)
angleDiff := targetAngle - tank.Rotation
ai.normalizeAngle(&angleDiff)
// Turn towards target
if angleDiff > 0.1 {
commands.TurnRight = true
} else if angleDiff < -0.1 {
commands.TurnLeft = true
}
// Move forward if roughly facing target
if math.Abs(angleDiff) < math.Pi/2 {
commands.MoveForward = true
}
}
func (ai *AdvancedAI) aimAt(tank *game.Tank, target common.Vector2, commands *game.Commands) {
targetAngle := math.Atan2(target.Y-tank.Position.Y, target.X-tank.Position.X)
angleDiff := targetAngle - tank.Rotation
ai.normalizeAngle(&angleDiff)
if angleDiff > 0.05 {
commands.TurnRight = true
} else if angleDiff < -0.05 {
commands.TurnLeft = true
}
}
func (ai *AdvancedAI) predictEnemyPosition(enemy *game.Tank, timeAhead float64) common.Vector2 {
return common.Vector2{
X: enemy.Position.X + enemy.Velocity.X*timeAhead,
Y: enemy.Position.Y + enemy.Velocity.Y*timeAhead,
}
}
func (ai *AdvancedAI) isPositionSafe(pos common.Vector2, gameState *game.GameState) bool {
// Check arena bounds
margin := 30.0
if pos.X < margin || pos.X > gameState.ArenaWidth-margin ||
pos.Y < margin || pos.Y > gameState.ArenaHeight-margin {
return false
}
// Check for nearby projectiles
for _, projectile := range gameState.Projectiles {
if ai.getDistance(pos, projectile.Position) < 40 {
return false
}
}
return gameState.IsValidPosition(pos, 25.0)
}
func (ai *AdvancedAI) findNearestEnemy(myTank *game.Tank, gameState *game.GameState) *game.Tank {
var nearest *game.Tank
minDistance := math.MaxFloat64
for _, tank := range gameState.Tanks {
if tank == myTank || !tank.IsAlive {
continue
}
distance := ai.getDistance(myTank.Position, tank.Position)
if distance < minDistance {
minDistance = distance
nearest = tank
}
}
return nearest
}
func (ai *AdvancedAI) getDistance(pos1, pos2 common.Vector2) float64 {
dx := pos1.X - pos2.X
dy := pos1.Y - pos2.Y
return math.Sqrt(dx*dx + dy*dy)
}
func (ai *AdvancedAI) normalizeAngle(angle *float64) {
for *angle > math.Pi {
*angle -= 2 * math.Pi
}
for *angle < -math.Pi {
*angle += 2 * math.Pi
}
}To add your AI to the game:
-
Create your AI file in the
ai/directory (e.g.,ai/my_ai.go) -
Implement the AI interface as shown in the examples above
-
Register your AI in
ai/registry.go:func init() { RegisterAI("example", func() game.AI { return NewExampleAI() }) RegisterAI("example_2", func() game.AI { return NewExampleAI2() }) RegisterAI("my_ai", func() game.AI { return NewMyAI() }) // Add this line }
-
Run the game - your AI will automatically be loaded and assigned a colored tank!
- Aggressive: Always move towards enemies and fire frequently
- Defensive: Maintain distance and only engage when advantageous
- Evasive: Focus on dodging and hit-and-run tactics
- Projectile Prediction: Calculate where enemies will be when your shot arrives
- Collision Avoidance: Use
gameState.IsValidPosition()for pathfinding - Tactical Positioning: Control key areas of the battlefield
- Resource Management: Balance aggression with survival
- Avoid expensive calculations in the
Update()method (called 60 times per second) - Cache frequently used calculations
- Use efficient algorithms for distance calculations and enemy detection
The game ends when only one tank remains alive. Victory strategies include:
- Elimination: Destroy all enemy tanks
- Survival: Outlast opponents through superior tactics
- Positioning: Control the battlefield and force enemies into disadvantageous positions
Game won't start: Ensure SDL3 is properly installed and sdl3.dll is in the project directory (Windows)
AI not appearing: Check that your AI is registered in ai/registry.go and implements all required methods
Compilation errors: Run go mod tidy to ensure all dependencies are installed
Tank gets stuck: Use gameState.IsValidPosition() to check for valid movement positions
- Use
fmt.Printf()in your AI'sUpdate()method for debugging (remove before final submission) - Check tank health and position values to understand behavior
- Monitor
gameState.GameTimefor time-based debugging
Ready to create your AI? Start with the simple example and gradually add more sophisticated behaviors. Good luck in the arena! 🚀