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
58 changes: 58 additions & 0 deletions src/ecs/components/portal_barrier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/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/>.

"""Portal Barrier components for Portal Barrier Mode."""

from dataclasses import dataclass


@dataclass
class PortalBarrierTag:
"""Marker component for portal barrier entities.

Tag-only component with no data fields.
Used by: PortalBarrier entity
"""


@dataclass
class PortalBarrier:
"""Component representing a two-block portal barrier.

A portal barrier consists of two adjacent blocks on the grid.
When the snake's head enters either block, it teleports
to the opposite block and continues movement in the same direction.

Contains:
- block1_x: X coordinate of first block
- block1_y: Y coordinate of first block
- block2_x: X coordinate of second block
- block2_y: Y coordinate of second block

The barrier works bidirectionally:
- If snake enters block1, it exits at block2
- If snake enters block2, it exits at block1

Used by: CollisionSystem
"""

block1_x: int
block1_y: int
block2_x: int
block2_y: int
1 change: 1 addition & 0 deletions src/ecs/entities/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class EntityType(Enum):
OBSTACLE = auto()
BOX = auto()
HOLE = auto()
PORTAL_BARRIER = auto()


class Entity(ABC):
Expand Down
53 changes: 53 additions & 0 deletions src/ecs/entities/portal_barrier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/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/>.

"""Portal Barrier entity for Portal Barrier Mode."""

from dataclasses import dataclass
from typing import Optional

from ecs.entities.entity import Entity, EntityType
from ecs.components.position import Position
from ecs.components.portal_barrier import PortalBarrier, PortalBarrierTag
from ecs.components.renderable import Renderable


@dataclass
class PortalBarrierEntity(Entity):
"""Portal Barrier entity component composition.

Defines the components that make up a portal barrier entity:
- position: location of entry block in grid
- portal_barrier: the two-block barrier configuration (entry/exit blocks)
- tag: marker component
- renderable: visual representation for entry block
"""

position: Position
portal_barrier: PortalBarrier
tag: PortalBarrierTag
renderable: Optional[Renderable] = None

def get_type(self) -> EntityType:
"""Get the type of this entity.

Returns:
EntityType.PORTAL_BARRIER
"""
return EntityType.PORTAL_BARRIER
85 changes: 85 additions & 0 deletions src/ecs/prefabs/portal_barrier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/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/>.

"""Portal Barrier entity prefab factory."""

from typing import Optional

from ecs.world import World
from ecs.entities.portal_barrier import PortalBarrierEntity
from ecs.components.position import Position
from ecs.components.portal_barrier import PortalBarrier, PortalBarrierTag
from ecs.components.renderable import Renderable
from core.types.color import Color


def create_portal_barrier(
world: World,
block1_x: int,
block1_y: int,
block2_x: int,
block2_y: int,
grid_size: int,
color: Optional[tuple[int, int, int]] = None,
) -> int:
"""Create a portal barrier entity with two interconnected blocks.

Args:
world: ECS world to create entity in
block1_x: X position of first block in pixels (grid-aligned)
block1_y: Y position of first block in pixels (grid-aligned)
block2_x: X position of second block in pixels (grid-aligned)
block2_y: Y position of second block in pixels (grid-aligned)
grid_size: Size of each grid cell in pixels
color: RGB color for portal barrier (default: cyan)

Returns:
Entity ID of the created portal barrier
"""
if color is None:
color = (0, 255, 255) # cyan

# Create portal barrier component with both blocks
portal_barrier_component = PortalBarrier(
block1_x=block1_x,
block1_y=block1_y,
block2_x=block2_x,
block2_y=block2_y,
)

# Create renderable for the first block
block1_renderable = Renderable(
shape="square",
color=Color(color[0], color[1], color[2]),
size=grid_size,
)

tag = PortalBarrierTag()

# Create the entity with Position set to first block
entity = PortalBarrierEntity(
position=Position(x=block1_x, y=block1_y, prev_x=block1_x, prev_y=block1_y),
portal_barrier=portal_barrier_component,
tag=tag,
renderable=block1_renderable,
)

# Add to world registry and return entity ID
entity_id = world.registry.add(entity)
return entity_id
72 changes: 70 additions & 2 deletions src/ecs/systems/collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ def update(self, world: World) -> None:
1. Wall collision (electric mode only)
2. Self-bite collision
3. Player-vs-player collision (PvP mode)
4. Obstacle collision
5. Apple collision
4. Portal barrier collision (teleport, no death)
5. Obstacle collision
6. Apple collision

Args:
world: ECS world to query entities
Expand All @@ -123,6 +124,9 @@ def update(self, world: World) -> None:
self._handle_death(world, "Self-bite collision")
return

# Check portal barrier collision (teleport without death)
self._check_portal_barrier_collision(world)

# Check obstacle collision
if self._check_obstacle_collision(world):
self._handle_death(world, "Obstacle collision")
Expand Down Expand Up @@ -626,6 +630,70 @@ def _check_obstacle_collision(self, world: World) -> bool:

return False

def _check_portal_barrier_collision(self, world: World) -> None:
"""Check collision with portal barriers.

Checks if snake's CURRENT position (after movement) collides with a portal barrier.

Args:
world: ECS world to query portal barriers

Returns:
None
"""
snake = self._get_snake_entity(world)
# Getting position and velocity to know where to "push" the snake
if (
not snake
or not hasattr(snake, "position")
or not hasattr(snake, "velocity")
):
return

current_x = snake.position.x
current_y = snake.position.y
dx = snake.velocity.dx
dy = snake.velocity.dy

from ecs.entities.entity import EntityType

portal_barriers = world.registry.query_by_type(EntityType.PORTAL_BARRIER)

for _, portal_barrier in portal_barriers.items():
if not hasattr(portal_barrier, "portal_barrier"):
continue

barrier = portal_barrier.portal_barrier

exit_x = None
exit_y = None

# Check if snake is colliding with either block of the portal barrier
if current_x == barrier.block1_x and current_y == barrier.block1_y:
# Entered on 1, exit on 2
exit_x = barrier.block2_x
exit_y = barrier.block2_y

elif current_x == barrier.block2_x and current_y == barrier.block2_y:
# Entered on 2, exit on 1
exit_x = barrier.block1_x
exit_y = barrier.block1_y

# If there was a collision, calculate the teleport output position
if exit_x is not None and exit_y is not None:
# The new current position will be one step AFTER the exit
snake.position.x = exit_x + dx
snake.position.y = exit_y + dy

# The "trail" (prev) should be placed exactly at the portal EXIT.
# It tells the body that the head "passed" through the exit block,
# maintaining visual continuity without stretching the snake.
snake.position.prev_x = exit_x
snake.position.prev_y = exit_y

# We stop the loop because we already found the collided portal
break

def _check_box_collision(self, world: World) -> None:
"""Check collision with boxes and push them.

Expand Down
Loading