Skip to content

Game Mechanics

This document explains the core game mechanics and algorithms used in Ajitroids.

Physics System

Movement and Velocity

All entities use basic 2D physics:

# Position update
position += velocity * delta_time

# Velocity update with acceleration
velocity += acceleration * delta_time

Player Controls

The player ship uses rotation and thrust:

# Rotation
if left_key:
    rotation -= ROTATION_SPEED * dt
if right_key:
    rotation += ROTATION_SPEED * dt

# Thrust
if up_key:
    direction = Vector2(0, -1).rotate(rotation)
    velocity += direction * ACCELERATION * dt

Friction and Damping

To prevent infinite acceleration:

# Apply friction
velocity *= (1 - FRICTION * dt)

# Clamp maximum velocity
if velocity.length() > MAX_SPEED:
    velocity = velocity.normalize() * MAX_SPEED

Collision Detection

Circle-Circle Collision

All game entities use circular hitboxes:

def collides_with(self, other):
    distance = self.position.distance_to(other.position)
    return distance < self.radius + other.radius

Collision Response

Different collision pairs have different effects:

Player vs Asteroid:

if player.collides_with(asteroid):
    if not player.invulnerable:
        player.take_damage()
        asteroid.destroy()
        create_explosion(asteroid.position)

Shot vs Asteroid:

if shot.collides_with(asteroid):
    shot.kill()
    asteroid.split()  # Create smaller asteroids
    add_score(asteroid.points)

Asteroid Mechanics

Asteroid Types

Ajitroids features four distinct asteroid types, each with unique properties and behaviors:

Normal Asteroids (White)

  • Standard asteroids with balanced properties
  • Split into 2 pieces when destroyed
  • Most common type (50% spawn rate)

Ice Asteroids (Light Blue)

  • Slippery and fast-moving
  • Split into 2 pieces with 1.4x velocity multiplier
  • Harder to predict trajectory after splitting
  • Spawn rate: 20%

Metal Asteroids (Gray)

  • Reinforced and tough
  • Require 2 hits to destroy
  • First hit damages but doesn't split the asteroid
  • Split into 2 pieces on final hit
  • Spawn rate: 15%

Crystal Asteroids (Purple)

  • Brittle and shatter dramatically
  • Split into 3 pieces instead of 2
  • Creates more chaotic asteroid fields
  • Spawn rate: 15%

All asteroid types maintain their properties when split - ice asteroids spawn ice fragments, metal asteroids spawn tougher metal fragments, and crystal asteroids spawn crystal fragments.

Spawning

Asteroids spawn in waves:

def spawn_asteroid():
    # Spawn at random edge position
    edge = random.choice(['top', 'bottom', 'left', 'right'])
    position = get_edge_position(edge)

    # Random velocity toward center
    direction = (screen_center - position).normalize()
    velocity = direction * random.uniform(MIN_SPEED, MAX_SPEED)

    # Random size
    size = random.choice([LARGE, MEDIUM, SMALL])

    return Asteroid(position, velocity, size)

Splitting

When hit, asteroids split into smaller pieces with type-specific behavior:

def split(self):
    # Metal asteroids require multiple hits
    if self.type == METAL and self.health > 1 and not at_min_size:
        self.health -= 1
        create_damage_particles()
        return  # Don't split yet

    if self.size == LARGE:
        # Normal/Ice/Metal: Create 2 asteroids
        # Crystal: Create 3 asteroids
        split_count = 3 if self.type == CRYSTAL else 2

        for i in range(split_count):
            angle = calculate_split_angle(i, split_count)

            # Ice asteroids have higher velocity multiplier
            velocity_mult = 1.4 if self.type == ICE else 1.2
            velocity = Vector2(SPLIT_SPEED, 0).rotate(angle) * velocity_mult

            # New asteroid inherits parent type
            Asteroid(self.position, velocity, MEDIUM, self.type)

    elif self.size == MEDIUM:
        # Same splitting logic for medium asteroids
        # ...

    # Small asteroids just disappear
    self.kill()

Type-specific splitting behaviors:

  • Normal: Standard 2-way split with 1.2x velocity
  • Ice: 2-way split with 1.4x velocity (slippery/fast)
  • Metal: Requires 2 hits before splitting normally
  • Crystal: 3-way split for more chaotic asteroid fields

Boss Mechanics

Boss AI

Bosses follow attack patterns:

def update_boss(self, dt):
    # Pattern state machine
    if self.pattern == "circle":
        self.move_in_circle(dt)
    elif self.pattern == "chase":
        self.chase_player(dt)
    elif self.pattern == "shoot":
        self.shoot_at_player()

    # Change pattern after time
    self.pattern_timer -= dt
    if self.pattern_timer <= 0:
        self.pattern = random.choice(PATTERNS)
        self.pattern_timer = PATTERN_DURATION

Boss Health

Bosses have multi-stage health:

def take_damage(self, amount):
    self.health -= amount

    # Phase changes
    if self.health < self.max_health * 0.5:
        self.enter_rage_mode()

    if self.health <= 0:
        self.defeat()
        spawn_explosion(self.position, size='large')
        player.add_score(self.reward)

Power-Up System

Power-Up Types

Different power-ups provide various benefits:

POWERUP_TYPES = {
    "rapid_fire": {
        "duration": 10,
        "effect": "reduce_shot_cooldown",
        "multiplier": 0.5
    },
    "shield": {
        "duration": 15,
        "effect": "invulnerability",
    },
    "multi_shot": {
        "duration": 12,
        "effect": "shoot_multiple",
        "count": 3
    },
    "speed_boost": {
        "duration": 8,
        "effect": "increase_speed",
        "multiplier": 1.5
    }
}

Power-Up Activation

def activate_powerup(self, powerup_type):
    config = POWERUP_TYPES[powerup_type]

    # Apply effect
    if config["effect"] == "reduce_shot_cooldown":
        self.shot_cooldown *= config["multiplier"]

    # Set timer
    self.powerup_timer = config["duration"]
    self.active_powerup = powerup_type

Scoring System

Point Values

Different actions award different points:

POINTS = {
    "asteroid_large": 20,
    "asteroid_medium": 50,
    "asteroid_small": 100,
    "boss": 1000,
    "enemy_ship": 200,
    "powerup": 50,
}

Score Multipliers

Combo system increases score:

def add_score(self, base_points):
    # Check time since last kill
    if time.now() - self.last_kill_time < COMBO_WINDOW:
        self.combo_multiplier += 0.1
    else:
        self.combo_multiplier = 1.0

    points = base_points * self.combo_multiplier
    self.score += points
    self.last_kill_time = time.now()

Lives and Respawn

Taking Damage

def take_damage(self):
    if self.invulnerable:
        return

    self.lives -= 1

    if self.lives > 0:
        self.respawn()
    else:
        self.game_over()

Respawn Mechanic

def respawn(self):
    # Reset position to center
    self.position = Vector2(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)
    self.velocity = Vector2(0, 0)

    # Invulnerability period
    self.invulnerable = True
    self.invulnerable_timer = INVULNERABLE_DURATION

    # Clear nearby threats
    self.clear_spawn_area()

Achievement System

Achievement Conditions

Achievements check for various conditions:

ACHIEVEMENTS = {
    "first_kill": {
        "condition": lambda stats: stats.kills >= 1,
        "title": "First Blood",
        "description": "Destroy your first asteroid"
    },
    "sharpshooter": {
        "condition": lambda stats: stats.accuracy >= 0.9,
        "title": "Sharpshooter",
        "description": "Maintain 90% accuracy"
    },
    "survivor": {
        "condition": lambda stats: stats.time_survived >= 300,
        "title": "Survivor",
        "description": "Survive for 5 minutes"
    }
}

Achievement Checking

def check_achievements(self):
    for achievement_id, achievement in ACHIEVEMENTS.items():
        if achievement_id not in self.unlocked:
            if achievement["condition"](self.stats):
                self.unlock_achievement(achievement_id)

Particle System

Particle Creation

Explosions and effects use particles:

def create_explosion(position, count=20):
    for i in range(count):
        angle = random.uniform(0, 360)
        speed = random.uniform(50, 150)
        velocity = Vector2(speed, 0).rotate(angle)

        Particle(
            position=position,
            velocity=velocity,
            lifetime=random.uniform(0.5, 1.5),
            color=random.choice([RED, ORANGE, YELLOW])
        )

Particle Update

def update(self, dt):
    # Update position
    self.position += self.velocity * dt

    # Apply gravity/friction
    self.velocity *= 0.95

    # Fade out
    self.lifetime -= dt
    self.alpha = self.lifetime / self.max_lifetime

    # Remove when expired
    if self.lifetime <= 0:
        self.kill()

Screen Wrapping

Toroidal World

Entities wrap around screen edges:

def wrap_around_screen(self):
    if self.position.x < 0:
        self.position.x = SCREEN_WIDTH
    elif self.position.x > SCREEN_WIDTH:
        self.position.x = 0

    if self.position.y < 0:
        self.position.y = SCREEN_HEIGHT
    elif self.position.y > SCREEN_HEIGHT:
        self.position.y = 0

Difficulty Scaling

Wave System

Difficulty increases with each wave:

def calculate_wave_difficulty(wave_number):
    base_asteroids = 3
    asteroids_per_wave = 1

    total_asteroids = base_asteroids + (wave_number * asteroids_per_wave)
    asteroid_speed_multiplier = 1 + (wave_number * 0.1)

    return {
        "asteroid_count": min(total_asteroids, 15),
        "speed_multiplier": min(asteroid_speed_multiplier, 2.0),
        "boss_spawn": wave_number % 5 == 0
    }

Ship Types

Ship Attributes

Different ships have unique stats:

SHIPS = {
    "default": {
        "speed": 100,
        "rotation_speed": 180,
        "fire_rate": 0.3,
        "health": 3
    },
    "speedster": {
        "speed": 150,
        "rotation_speed": 220,
        "fire_rate": 0.25,
        "health": 2
    },
    "tank": {
        "speed": 80,
        "rotation_speed": 140,
        "fire_rate": 0.4,
        "health": 5,
        "dual_cannons": True
    }
}

Next Steps