Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/ecs/components/mirrored_pair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# Copyright (c) 2023, Monaco F. J. <monaco@usp.br>
#
# This file is part of Naja.
#
# Naja is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Component for tracking mirrored snake pairs."""

from dataclasses import dataclass
from typing import Optional


@dataclass
class MirroredPair:
"""Component that links two snakes in a mirrored relationship.

Attributes:
partner_id: Entity ID of the mirrored partner snake
"""

partner_id: Optional[int] = None
7 changes: 7 additions & 0 deletions src/ecs/prefabs/snake.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from ecs.components.renderable import Renderable
from ecs.components.input_buffer import InputBuffer
from ecs.components.hunger import Hunger
from ecs.components.mirrored_pair import MirroredPair
from core.types.color import Color
from game import constants

Expand All @@ -45,6 +46,7 @@ def create_snake(
autoplay_mode: bool = False,
initial_x: Optional[int] = None,
initial_y: Optional[int] = None,
mirrored_snake_id: Optional[int] = None,
) -> int:
"""Create a snake entity with all required components.

Expand All @@ -60,6 +62,7 @@ def create_snake(
autoplay_mode: Whether Autoplay Mode is enabled (starts with zero velocity)
initial_x: Initial X position in grid coordinates (default: center)
initial_y: Initial Y position in grid coordinates (default: center)
mirrored_snake_id: Entity ID of the mirrored snake (for Mirrored mode)

Returns:
int: Entity ID of created snake
Expand Down Expand Up @@ -145,6 +148,10 @@ def create_snake(
input_buffer=InputBuffer(),
)

# Add mirrored pair component if this is a mirrored snake
if mirrored_snake_id is not None:
snake.mirrored_pair = MirroredPair(partner_id=mirrored_snake_id)

# register entity with world and return ID
entity_id = world.registry.add(snake)
return entity_id
227 changes: 225 additions & 2 deletions src/ecs/systems/collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
from ecs.systems.scoring import ScoringSystem
from game.settings import GameSettings
from game.services.audio_service import AudioService
from game.game_modes_registry import GAME_MODE_TELEPORT, PLAYER_VS_PLAYER_MODE_NAME
from game.game_modes_registry import (
GAME_MODE_TELEPORT,
PLAYER_VS_PLAYER_MODE_NAME,
MIRRORED_MODE_NAME,
)
from game.services.game_over_service import GameOverService


Expand Down Expand Up @@ -104,13 +108,17 @@ def update(self, world: World) -> None:
Args:
world: ECS world to query entities
"""
# Check if we're in PvP mode
# Check if we're in PvP or Mirrored mode
game_state = self._get_game_state(world)
is_pvp_mode = game_state and game_state.game_mode == PLAYER_VS_PLAYER_MODE_NAME
is_mirrored_mode = game_state and game_state.game_mode == MIRRORED_MODE_NAME

if is_pvp_mode:
# In PvP mode, check collisions for each snake individually
self._check_all_snakes_collisions(world)
elif is_mirrored_mode:
# In Mirrored mode, check collisions for both snakes
self._check_mirrored_mode_collisions(world)
else:
# Single player mode - check collisions for the single snake
# Check wall collision first (highest priority)
Expand Down Expand Up @@ -386,6 +394,221 @@ def _check_all_snakes_collisions(self, world: World) -> None:
# check apple collisions for all snakes
self._check_apple_collision_all_snakes(world)

def _check_mirrored_mode_collisions(self, world: World) -> None:
"""Check collisions for both mirrored snakes.

Args:
world: ECS world
"""
from ecs.entities.entity import EntityType

snakes = world.registry.query_by_type(EntityType.SNAKE)
snake_list = list(snakes.items())

if len(snake_list) < 2:
return

snake1_id, snake1 = snake_list[0]
snake2_id, snake2 = snake_list[1]

# Check wall collisions for both snakes
if self._check_wall_collision_for_snake(
world, snake1
) or self._check_wall_collision_for_snake(world, snake2):
self._handle_death(world, "Wall collision")
return

# Check self-bite for both snakes
if self._check_self_bite_for_snake(
world, snake1
) or self._check_self_bite_for_snake(world, snake2):
self._handle_death(world, "Self-bite collision")
return

# Check if snakes collide with each other
if self._check_mirrored_snakes_collision(world, snake1, snake2):
self._handle_death(world, "Snake collision")
return

# Check obstacle collisions for both snakes
if self._check_obstacle_collision_for_snake(
world, snake1
) or self._check_obstacle_collision_for_snake(world, snake2):
self._handle_death(world, "Obstacle collision")
return

# Check apple collisions with synchronized growth
self._check_mirrored_apple_collision(world)

def _check_mirrored_snakes_collision(self, world: World, snake1, snake2) -> bool:
"""Check if two mirrored snakes collide with each other.

Args:
world: ECS world
snake1: First snake entity
snake2: Second snake entity

Returns:
bool: True if collision detected
"""
if not hasattr(snake1, "position") or not hasattr(snake2, "position"):
return False

# Check head-to-head collision
if (
snake1.position.x == snake2.position.x
and snake1.position.y == snake2.position.y
):
return True

# Check if snake1 head hits snake2 body
if hasattr(snake2, "body"):
for segment in snake2.body.segments:
if snake1.position.x == segment.x and snake1.position.y == segment.y:
return True

# Check if snake2 head hits snake1 body
if hasattr(snake1, "body"):
for segment in snake1.body.segments:
if snake2.position.x == segment.x and snake2.position.y == segment.y:
return True

return False

def _check_mirrored_apple_collision(self, world: World) -> None:
"""Check apple collision for mirrored snakes with synchronized growth.

When either snake eats an apple, both snakes grow.

Args:
world: ECS world
"""
from ecs.entities.entity import EntityType

snakes = world.registry.query_by_type(EntityType.SNAKE)
snake_list = list(snakes.values())

if len(snake_list) < 2:
return

snake1 = snake_list[0]
snake2 = snake_list[1]

# Check apple collision for both snakes
apples = world.registry.query_by_type(EntityType.APPLE)
for entity_id, apple in apples.items():
if not hasattr(apple, "position"):
continue

# Check if either snake ate the apple
snake1_ate = (
hasattr(snake1, "position")
and snake1.position.x == apple.position.x
and snake1.position.y == apple.position.y
)
snake2_ate = (
hasattr(snake2, "position")
and snake2.position.x == apple.position.x
and snake2.position.y == apple.position.y
)

if snake1_ate or snake2_ate:
print(
f"APPLE EATEN IN MIRRORED MODE: apple=({apple.position.x},{apple.position.y})"
)

# Play apple eating sound
if self._audio_service:
self._audio_service.play_sound("assets/sound/eat.flac")

# Grow BOTH snakes
if hasattr(snake1, "body"):
snake1.body.size += 1
if hasattr(snake2, "body"):
snake2.body.size += 1

# Increment score
if self._scoring_system:
points = 1
if hasattr(apple, "edible"):
points = apple.edible.points
self._scoring_system.on_apple_eaten(world, points)

game_state = self._get_game_state(world)
if game_state:
game_state.apples_eaten_count += 1

# Apply (constant) speed ensures variables stay consistent
max_speed = (
float(self._settings.get("max_speed")) if self._settings else 20.0
)
speed_increase_rate = (
self._settings.get("speed_increase_rate")
if self._settings
else "10%"
)
multiplier = 1.05 if speed_increase_rate == "5%" else 1.10
if hasattr(snake1, "velocity"):
snake1.velocity.speed = min(
snake1.velocity.speed * multiplier, max_speed
)
if hasattr(snake2, "velocity"):
snake2.velocity.speed = min(
snake2.velocity.speed * multiplier, max_speed
)

# Remove eaten apple
world.registry.remove(entity_id)

# [FIX 2] Robust Apple Respawn Logic
from ecs.prefabs.apple import create_apple
import random

occupied_positions = set()

# Add snake positions (Force INT to match random grid coordinates)
if hasattr(snake1, "position"):
occupied_positions.add(
(int(snake1.position.x), int(snake1.position.y))
)
if hasattr(snake1, "body"):
for segment in snake1.body.segments:
occupied_positions.add((int(segment.x), int(segment.y)))

if hasattr(snake2, "position"):
occupied_positions.add(
(int(snake2.position.x), int(snake2.position.y))
)
if hasattr(snake2, "body"):
for segment in snake2.body.segments:
occupied_positions.add((int(segment.x), int(segment.y)))

# Add obstacles (Force INT)
obstacles = world.registry.query_by_type(EntityType.OBSTACLE)
for _, obstacle in obstacles.items():
if hasattr(obstacle, "position"):
occupied_positions.add(
(int(obstacle.position.x), int(obstacle.position.y))
)

# Find valid position for new apple
attempts = 0
max_attempts = 1000
while attempts < max_attempts:
x = random.randint(0, world.board.width - 1)
y = random.randint(0, world.board.height - 1)

if (x, y) not in occupied_positions:
create_apple(
world, x=x, y=y, grid_size=world.board.cell_size, color=None
)
print(f"NEW MIRRORED APPLE: ({x}, {y})")
break

attempts += 1

return # Only process one apple per frame # Only process one apple per frame

def _check_wall_collision_for_snake(self, world: World, snake) -> bool:
"""Check wall collision for a specific snake.

Expand Down
Loading