Spaces:
Runtime error
Runtime error
File size: 8,444 Bytes
764e34c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | """
Module for level-related functionality in the ARCEngine.
"""
import copy
from typing import Any, List, Optional, Tuple
from .enums import BlockingMode, PlaceableArea
from .sprites import Sprite
class Level:
"""A level that manages a collection of sprites."""
_sprites: List[Sprite]
_sorted_sprites: List[Sprite] | None # Sorted High to low
_grid_size: Tuple[int, int] | None
_data: dict[str, Any]
_name: str
_placeable_areas: List[PlaceableArea]
_need_sort: bool
def __init__(
self,
sprites: Optional[List[Sprite]] = None,
grid_size: Tuple[int, int] | None = None,
data: dict[str, Any] = {},
name: str = "Level",
placeable_areas: Optional[List[PlaceableArea]] = None,
):
"""Initialize a new level.
Args:
sprites: List of sprites to add to the level
grid_size: Tuple of width and height of the grid
data: Dictionary of data to store in the level
name: Name of the level
placeable_areas: List of placeable areas in the level
"""
self._sprites = []
self._grid_size = grid_size
self._data = data
self._name = name
self._placeable_areas = placeable_areas if placeable_areas is not None else []
self._need_sort = True
if sprites:
# Add first (fast path), then do one-time merge+sort.
self._sprites.extend(sprites)
self._merge_sys_static_pixel_perfect_on_init()
def _merge_sys_static_pixel_perfect_on_init(self) -> None:
"""
Merge any sprites that are:
- PIXEL_PERFECT
- have tag "sys_static"
into ONE sprite per layer.
This runs only during construction.
"""
if not self._sprites:
return
# Partition sprites into merge-candidates (by layer) and others.
by_layer: dict[int, List[Sprite]] = {}
others: List[Sprite] = []
for s in self._sprites:
if s.blocking == BlockingMode.PIXEL_PERFECT and "sys_static" in s.tags:
by_layer.setdefault(s.layer, []).append(s)
else:
others.append(s)
merged: List[Sprite] = []
for layer, group in by_layer.items():
if not group:
continue
if len(group) == 1:
merged.append(group[0])
continue
# Merge left-to-right; merge() returns a NEW Sprite each time.
base = group[0]
for nxt in group[1:]:
base = base.merge(nxt)
# Ensure the merged sprite stays on this layer.
# (merge() uses max layer, but all are same layer anyway; set explicitly for safety.)
base.set_layer(layer)
# Ensure sys_static remains (merge unions tags, so it should already be present)
if "sys_static" not in base.tags:
base.tags.append("sys_static")
merged.append(base)
self._sprites = others + merged
def remove_all_sprites(self) -> None:
"""Remove all sprites from the level."""
self._sprites = []
def add_sprite(self, sprite: Sprite) -> None:
"""Add a sprite to the level.
Args:
sprite: The sprite to add
"""
self._sprites.append(sprite)
self._need_sort = True
def remove_sprite(self, sprite: Sprite) -> None:
"""Remove a sprite from the level.
Args:
sprite: The sprite to remove
"""
if sprite in self._sprites:
self._sprites.remove(sprite)
def get_sprites(self) -> List[Sprite]:
"""Get all sprites in the level.
Returns:
List[Sprite]: All sprites in the level
"""
return self._sprites.copy() # Return copy to prevent external modification
def get_sprites_by_name(self, name: str) -> List[Sprite]:
"""Get all sprites with the given name.
Args:
name: The name to search for
Returns:
List[Sprite]: All sprites with the given name
"""
return [s for s in self._sprites if s.name == name]
def get_sprites_by_tag(self, tag: str) -> List[Sprite]:
"""Get all sprites that have the given tag.
Args:
tag: The tag to search for
Returns:
List[Sprite]: All sprites that have the given tag
"""
return [s for s in self._sprites if tag in s.tags]
def get_sprites_by_tags(self, tags: List[str]) -> List[Sprite]:
"""Get all sprites that have all of the given tags.
Args:
tags: The tags to search for
Returns:
List[Sprite]: All sprites that have all of the given tags
"""
if not tags:
return []
return [s for s in self._sprites if all(tag in s.tags for tag in tags)]
def get_sprites_by_any_tag(self, tags: List[str]) -> List[Sprite]:
"""Get all sprites that have any of the specified tags.
Args:
tags: List of tags to search for
Returns:
List[Sprite]: List of sprites that have any of the specified tags
"""
return [sprite for sprite in self._sprites if any(tag in sprite.tags for tag in tags)]
def get_all_tags(self) -> set[str]:
"""Get all unique tags from all sprites in the level.
This method collects all tags from all sprites and returns them as a set,
ensuring each tag appears only once in the result.
Returns:
set[str]: A set containing all unique tags from all sprites
"""
all_tags = set()
for sprite in self._sprites:
all_tags.update(sprite.tags)
return all_tags
def get_sprite_at(self, x: int, y: int, tag: Optional[str] = None, ignore_collidable: bool = False) -> Sprite | None:
"""Get the sprite at the given coordinates.
This method returns the first sprite that is at the given coordinates.
If a tag is provided, it will return the first sprite that has the given tag.
Args:
x: The x coordinate
y: The y coordinate
tag: The tag to search for
"""
if self._need_sort or self._sorted_sprites is None or len(self._sorted_sprites) != len(self._sprites):
self._sorted_sprites = sorted(self._sprites, key=lambda sprite: sprite.layer, reverse=True)
self._need_sort = False
for sprite in self._sorted_sprites:
if (ignore_collidable or sprite.is_collidable) and x >= sprite.x and y >= sprite.y and x < sprite.x + sprite.width and y < sprite.y + sprite.height:
if sprite.blocking == BlockingMode.PIXEL_PERFECT:
pixels = sprite.render()
if pixels[y - sprite.y][x - sprite.x] == -1:
continue
if tag is None or tag in sprite.tags:
return sprite
return None
def collides_with(self, sprite: Sprite, ignoreMode: bool = False) -> List[Sprite]:
"""Checks all sprites in the level for collisions with the given sprite.
Args:
sprite: The sprite to check for collisions
"""
return [s for s in self._sprites if sprite.collides_with(other=s, ignoreMode=ignoreMode)]
@property
def name(self) -> str:
"""Get the name of the level."""
return self._name
def get_data(self, key: str) -> Any:
return self._data.get(key)
@property
def grid_size(self) -> Tuple[int, int] | None:
"""Get the grid size of the level.
Returns:
Tuple[int, int]: The grid size of the level
"""
return self._grid_size
@property
def placeable_areas(self) -> List[PlaceableArea]:
"""Get the placeable areas of the level."""
return self._placeable_areas
def clone(self) -> "Level":
"""Create a deep copy of this level.
Returns:
Level: A new Level instance with cloned sprites
"""
# Clone each sprite and create new level
cloned_sprites = [sprite.clone() for sprite in self._sprites]
return Level(name=self._name, sprites=cloned_sprites, grid_size=self._grid_size, data=copy.deepcopy(self._data), placeable_areas=self._placeable_areas)
|