From 783bad73cc812c894f1bd744953962880d5923ab Mon Sep 17 00:00:00 2001 From: GiordanoSL Date: Sat, 13 Dec 2025 00:07:18 -0300 Subject: [PATCH 1/3] update apple spawn mechanics for teleport mode --- src/ecs/components/game_state.py | 1 + src/ecs/entities/apple.py | 1 + src/ecs/systems/apple_spawn.py | 44 +++++++++++++-- src/ecs/systems/collision.py | 81 ++++++++++++++------------- src/game/game_modes_registry.py | 6 +- src/game/services/game_initializer.py | 49 ++++++++++++++++ 6 files changed, 135 insertions(+), 47 deletions(-) diff --git a/src/ecs/components/game_state.py b/src/ecs/components/game_state.py index f4f9d9c7..ceea8025 100644 --- a/src/ecs/components/game_state.py +++ b/src/ecs/components/game_state.py @@ -51,6 +51,7 @@ class GameState: trail_mode_enabled: bool = False shrinking_mode_enabled: bool = False lights_out_enabled: bool = False + teleport_mode_enabled: bool = False game_started: bool = False # Set to True after first input final_score: int = 0 # score at time of death trail_obstacles: List[Tuple[int, int]] = field(default_factory=list) diff --git a/src/ecs/entities/apple.py b/src/ecs/entities/apple.py index 4e71fe1e..8b799591 100644 --- a/src/ecs/entities/apple.py +++ b/src/ecs/entities/apple.py @@ -43,6 +43,7 @@ class Apple(Entity): edible: Edible renderable: Renderable moving_apple: Optional[MovingApple] = None + linked_apple: Optional["Apple"] = None # teleport target def get_type(self) -> EntityType: """Get the type of this entity. diff --git a/src/ecs/systems/apple_spawn.py b/src/ecs/systems/apple_spawn.py index 35cb8195..01d981b8 100644 --- a/src/ecs/systems/apple_spawn.py +++ b/src/ecs/systems/apple_spawn.py @@ -63,16 +63,52 @@ def update(self, world: World) -> None: Args: world: ECS world containing entities and components """ - # Get desired apple count from config - desired_count = self._get_desired_apple_count(world) - if desired_count <= 0: - return + # Check if Teleport mode is enabled + game_state_entities = world.registry.query_by_component("game_state") + teleport_mode = False + if game_state_entities: + entity = next(iter(game_state_entities.values())) + if hasattr(entity, "game_state"): + teleport_mode = entity.game_state.teleport_mode_enabled # Count current apples current_apples = world.registry.query_by_type(EntityType.APPLE) current_count = len(current_apples) + if teleport_mode: + if len(current_apples) >= 2: + return + + grid_size = world.board.cell_size + + pos_a = self._find_valid_position(world) + pos_b = self._find_valid_position(world) + + if not pos_a or not pos_b: + return + + x_a, y_a = pos_a + x_b, y_b = pos_b + id_a = create_apple(world, x=x_a, y=y_a, grid_size=grid_size, color=None) + id_b = create_apple( + world, x=x_b, y=y_b, grid_size=grid_size, color=(128, 0, 128) + ) + + apple_a = world.registry.get(id_a) + apple_b = world.registry.get(id_b) + + apple_a.linked_apple = apple_b + apple_b.linked_apple = apple_a + + return + + # Get desired apple count from config + desired_count = self._get_desired_apple_count(world) + + if desired_count <= 0: + return + # Spawn new apples if we're below desired count apples_to_spawn = desired_count - current_count diff --git a/src/ecs/systems/collision.py b/src/ecs/systems/collision.py index 4dd71b00..fabcfeb6 100644 --- a/src/ecs/systems/collision.py +++ b/src/ecs/systems/collision.py @@ -39,7 +39,7 @@ 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 TELEPORT_MODE_NAME, PLAYER_VS_PLAYER_MODE_NAME from game.services.game_over_service import GameOverService @@ -1039,45 +1039,46 @@ def _check_apple_collision(self, world: World) -> None: # reset current time to (possibly updated) max he.hunger.current_time = he.hunger.max_time - # Special handling for TELEPORT mode - if game_state and game_state.game_mode == GAME_MODE_TELEPORT: - # Find another active apple on the board - other_apple_id = None - other_apple = None - for other_id, other in apples.items(): - if other_id == entity_id: - continue - if hasattr(other, "position"): - other_apple_id = other_id - other_apple = other - break - - if other_apple is not None: - # Teleport snake head to the other apple's position - snake.position.prev_x = snake.position.x - snake.position.prev_y = snake.position.y - snake.position.x = other_apple.position.x - snake.position.y = other_apple.position.y - - # Keep velocity unchanged (do nothing to snake.velocity) - - # Remove both apples so AppleSpawnSystem will respawn them - try: - world.registry.remove(entity_id) - except Exception: - # ignore removal errors - pass - try: - if other_apple_id is not None: - world.registry.remove(other_apple_id) - except Exception: - pass - - break # handled teleport, only one apple per frame - - else: - # remove eaten apple - world.registry.remove(entity_id) + # # Special handling for TELEPORT mode + if game_state and game_state.game_mode == TELEPORT_MODE_NAME: + print("tp") + # # Find another active apple on the board + # other_apple_id = None + # other_apple = None + # for other_id, other in apples.items(): + # if other_id == entity_id: + # continue + # if hasattr(other, "position"): + # other_apple_id = other_id + # other_apple = other + # break + + # if other_apple is not None: + # # Teleport snake head to the other apple's position + # snake.position.prev_x = snake.position.x + # snake.position.prev_y = snake.position.y + # snake.position.x = other_apple.position.x + # snake.position.y = other_apple.position.y + + # # Keep velocity unchanged (do nothing to snake.velocity) + + # # Remove both apples so AppleSpawnSystem will respawn them + # try: + # world.registry.remove(entity_id) + # except Exception: + # # ignore removal errors + # pass + # try: + # if other_apple_id is not None: + # world.registry.remove(other_apple_id) + # except Exception: + # pass + + # break # handled teleport, only one apple per frame + + # else: + # remove eaten apple + world.registry.remove(entity_id) # Check for board-fill victory (Classic and other modes) # Skip for shrinking mode (handled separately) and AutoPlay (has its own check) diff --git a/src/game/game_modes_registry.py b/src/game/game_modes_registry.py index 2f4b1fcc..81ba4ff4 100644 --- a/src/game/game_modes_registry.py +++ b/src/game/game_modes_registry.py @@ -27,7 +27,7 @@ CHEESE_MODE_NAME = "Cheese Mode" AUTOPLAY_MODE_NAME = "AutoPlay" SHRINKING_MODE_NAME = "Shrinking Mode" -GAME_MODE_TELEPORT = "Teleport" +TELEPORT_MODE_NAME = "Teleport" BOX_MODE_NAME = "Box Mode" TRAIL_MODE_NAME = "Trail Mode" LIGHTS_OUT_MODE_NAME = "Lights Out" @@ -65,7 +65,7 @@ "detailed_info": "Sit back and watch! The snake uses AI to play itself automatically. No controls needed - just observe as the AI tries to survive and score points. Great for relaxing or studying AI pathfinding behavior. You can still access settings to customize the visual experience!", }, { - "name": GAME_MODE_TELEPORT, + "name": TELEPORT_MODE_NAME, "description": "Collect an apple to warp to the other one, maintaining your direction.", "detailed_info": "Quantum snake mechanics! Two apples appear on the board. When you eat one, you instantly teleport to where the other apple was, maintaining your current direction. The eaten apple respawns at a new location. This creates unique strategic opportunities and escape routes!", }, @@ -99,7 +99,7 @@ "MOVING_APPLE_MODE_NAME", "CHEESE_MODE_NAME", "AUTOPLAY_MODE_NAME", - "GAME_MODE_TELEPORT", + "TELEPORT_MODE_NAME", "BOX_MODE_NAME", "TRAIL_MODE_NAME", "LIGHTS_OUT_MODE_NAME", diff --git a/src/game/services/game_initializer.py b/src/game/services/game_initializer.py index b3bba331..c191a761 100644 --- a/src/game/services/game_initializer.py +++ b/src/game/services/game_initializer.py @@ -38,6 +38,7 @@ TRAIL_MODE_NAME, LIGHTS_OUT_MODE_NAME, PLAYER_VS_PLAYER_MODE_NAME, + TELEPORT_MODE_NAME, ) @@ -158,6 +159,8 @@ def create_initial_entities(self, world: World) -> None: # create initial apples (2 for PvP, normal count for others) if self._game_mode == PLAYER_VS_PLAYER_MODE_NAME: self._create_pvp_apples(world, grid_size) + elif self._game_mode == TELEPORT_MODE_NAME: + self._create_teleport_apples(world, grid_size) else: self._create_initial_apples(world, grid_size) @@ -195,6 +198,7 @@ def _create_game_state(self, world: World) -> None: trail_mode_enabled = current_mode == TRAIL_MODE_NAME shrinking_mode_enabled = current_mode == SHRINKING_MODE_NAME lights_out_enabled = current_mode == LIGHTS_OUT_MODE_NAME + teleport_mode_enabled = current_mode == TELEPORT_MODE_NAME class GameStateEntity: def __init__(self): @@ -210,6 +214,7 @@ def __init__(self): trail_mode_enabled=trail_mode_enabled, shrinking_mode_enabled=shrinking_mode_enabled, lights_out_enabled=lights_out_enabled, + teleport_mode_enabled=teleport_mode_enabled, ) def get_type(self): @@ -416,6 +421,50 @@ def _create_pvp_apples(self, world: World, grid_size: int) -> None: attempts += 1 + def _create_teleport_apples(self, world: World, grid_size: int) -> None: + """Create 2 apples for Player vs Player mode. + + Args: + world: ECS world instance + grid_size: Size of grid cells in pixels + """ + from ecs.prefabs.apple import create_apple + from ecs.entities.entity import EntityType + + # Get occupied positions (both snakes) + occupied_positions = set() + snakes = world.registry.query_by_type(EntityType.SNAKE) + for _, snake in snakes.items(): + if hasattr(snake, "position"): + occupied_positions.add((snake.position.x, snake.position.y)) + if hasattr(snake, "body"): + for segment in snake.body.segments: + occupied_positions.add((segment.x, segment.y)) + + attempts = 0 + max_attempts = 1000 + while attempts < max_attempts: + x_a = random.randint(0, world.board.width - 1) + y_a = random.randint(0, world.board.height - 1) + x_b = random.randint(0, world.board.width - 1) + y_b = random.randint(0, world.board.height - 1) + + if (x_a, y_a) not in occupied_positions and (x_a, y_a) != (x_b, y_b): + id_a = create_apple( + world, x=x_a, y=y_a, grid_size=grid_size, color=None + ) + id_b = create_apple( + world, x=x_b, y=y_b, grid_size=grid_size, color=(128, 0, 128) + ) + + apple_a = world.registry.get(id_a) + apple_b = world.registry.get(id_b) + + apple_a.linked_apple = apple_b + apple_b.linked_apple = apple_a + break + attempts += 1 + def _create_apple_config(self, world: World) -> None: """Create AppleConfig entity to track desired apple count. From 02a85404548a8914cc00fc708a56ecac03a3e490 Mon Sep 17 00:00:00 2001 From: GiordanoSL Date: Sat, 13 Dec 2025 00:23:43 -0300 Subject: [PATCH 2/3] add teleportation mechanics --- src/ecs/entities/apple.py | 2 +- src/ecs/systems/apple_spawn.py | 4 +- src/ecs/systems/collision.py | 71 +++++++++++++-------------- src/game/services/game_initializer.py | 4 +- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/ecs/entities/apple.py b/src/ecs/entities/apple.py index 8b799591..0d5aded8 100644 --- a/src/ecs/entities/apple.py +++ b/src/ecs/entities/apple.py @@ -43,7 +43,7 @@ class Apple(Entity): edible: Edible renderable: Renderable moving_apple: Optional[MovingApple] = None - linked_apple: Optional["Apple"] = None # teleport target + linked_apple_id: Optional[int] = None def get_type(self) -> EntityType: """Get the type of this entity. diff --git a/src/ecs/systems/apple_spawn.py b/src/ecs/systems/apple_spawn.py index 01d981b8..1312bd16 100644 --- a/src/ecs/systems/apple_spawn.py +++ b/src/ecs/systems/apple_spawn.py @@ -98,8 +98,8 @@ def update(self, world: World) -> None: apple_a = world.registry.get(id_a) apple_b = world.registry.get(id_b) - apple_a.linked_apple = apple_b - apple_b.linked_apple = apple_a + apple_a.linked_apple_id = id_b + apple_b.linked_apple_id = id_a return diff --git a/src/ecs/systems/collision.py b/src/ecs/systems/collision.py index fabcfeb6..ac3c7b16 100644 --- a/src/ecs/systems/collision.py +++ b/src/ecs/systems/collision.py @@ -1041,44 +1041,39 @@ def _check_apple_collision(self, world: World) -> None: # # Special handling for TELEPORT mode if game_state and game_state.game_mode == TELEPORT_MODE_NAME: - print("tp") - # # Find another active apple on the board - # other_apple_id = None - # other_apple = None - # for other_id, other in apples.items(): - # if other_id == entity_id: - # continue - # if hasattr(other, "position"): - # other_apple_id = other_id - # other_apple = other - # break - - # if other_apple is not None: - # # Teleport snake head to the other apple's position - # snake.position.prev_x = snake.position.x - # snake.position.prev_y = snake.position.y - # snake.position.x = other_apple.position.x - # snake.position.y = other_apple.position.y - - # # Keep velocity unchanged (do nothing to snake.velocity) - - # # Remove both apples so AppleSpawnSystem will respawn them - # try: - # world.registry.remove(entity_id) - # except Exception: - # # ignore removal errors - # pass - # try: - # if other_apple_id is not None: - # world.registry.remove(other_apple_id) - # except Exception: - # pass - - # break # handled teleport, only one apple per frame - - # else: - # remove eaten apple - world.registry.remove(entity_id) + # Find another active apple on the board + other_apple_id = apple.linked_apple_id + other_apple = None + + if other_apple_id is not None: + other_apple = world.registry.get(other_apple_id) + + if other_apple is not None: + # Teleport snake head to the other apple's position + snake.position.prev_x = snake.position.x + snake.position.prev_y = snake.position.y + snake.position.x = other_apple.position.x + snake.position.y = other_apple.position.y + + # Keep velocity unchanged (do nothing to snake.velocity) + + # Remove both apples so AppleSpawnSystem will respawn them + try: + world.registry.remove(entity_id) + except Exception: + # ignore removal errors + pass + try: + if other_apple_id is not None: + world.registry.remove(other_apple_id) + except Exception: + pass + + break # handled teleport, only one apple per frame + + else: + # remove eaten apple + world.registry.remove(entity_id) # Check for board-fill victory (Classic and other modes) # Skip for shrinking mode (handled separately) and AutoPlay (has its own check) diff --git a/src/game/services/game_initializer.py b/src/game/services/game_initializer.py index c191a761..af473039 100644 --- a/src/game/services/game_initializer.py +++ b/src/game/services/game_initializer.py @@ -460,8 +460,8 @@ def _create_teleport_apples(self, world: World, grid_size: int) -> None: apple_a = world.registry.get(id_a) apple_b = world.registry.get(id_b) - apple_a.linked_apple = apple_b - apple_b.linked_apple = apple_a + apple_a.linked_apple_id = id_b + apple_b.linked_apple_id = id_a break attempts += 1 From d17c2d3fa258a98a6eaba77e7776112195783fe4 Mon Sep 17 00:00:00 2001 From: Giordano Santorum Lorenzetto <129186535+GiordanoSL@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:04:51 -0300 Subject: [PATCH 3/3] Fix docstring for teleport apples creation --- src/game/services/game_initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/services/game_initializer.py b/src/game/services/game_initializer.py index af473039..07db16db 100644 --- a/src/game/services/game_initializer.py +++ b/src/game/services/game_initializer.py @@ -422,7 +422,7 @@ def _create_pvp_apples(self, world: World, grid_size: int) -> None: attempts += 1 def _create_teleport_apples(self, world: World, grid_size: int) -> None: - """Create 2 apples for Player vs Player mode. + """Create 2 apples for Teleport mode. Args: world: ECS world instance