diff --git a/api/entity/apple.py b/api/entity/apple.py index 8e66d69..1a15748 100644 --- a/api/entity/apple.py +++ b/api/entity/apple.py @@ -1,5 +1,6 @@ from enum import Enum from api.entity.entity import Entity +from api.world import World class AppleType(Enum): @@ -22,7 +23,7 @@ class Apple(Entity): apple_type (AppleType): The type of the apple (RED or GREEN). """ - def __init__(self, x: int, y: int, apple_type: AppleType): + def __init__(self, world: World, x: int, y: int, apple_type: AppleType): """ Initializes an apple with a specified type and position. @@ -32,7 +33,9 @@ def __init__(self, x: int, y: int, apple_type: AppleType): apple_type (AppleType): The type of the apple (RED or GREEN). """ super().__init__(x, y) - self.apple_type = apple_type + + self.__world = world + self.__apple_type = apple_type def is_green(self) -> bool: """ @@ -41,7 +44,7 @@ def is_green(self) -> bool: Returns: bool: True if the apple is green, False otherwise. """ - return self.apple_type == AppleType.GREEN + return self.__apple_type == AppleType.GREEN def is_red(self) -> bool: """ @@ -50,12 +53,12 @@ def is_red(self) -> bool: Returns: bool: True if the apple is red, False otherwise. """ - return self.apple_type == AppleType.RED + return self.__apple_type == AppleType.RED def get_char(self) -> str: """ - Returns a character representation of the apple, colored based on its - type. + Returns a character representation of the apple, colored based on + its type. Returns: str: A string representing the apple with appropriate color @@ -65,3 +68,13 @@ def get_char(self) -> str: return "\033[32m@\033[0m" # Green apple return "\033[31m@\033[0m" # Red apple + + def consume(self): + """ + Removes the apple from the world and respawns it. + + The apple is removed from the world and then spawned again at the + same location. + """ + self.__world.remove_entity(self) + self.__world.spawn_entity(self) diff --git a/api/entity/entity.py b/api/entity/entity.py index 3c40042..feb43c1 100644 --- a/api/entity/entity.py +++ b/api/entity/entity.py @@ -66,6 +66,17 @@ def set_y(self, y: int) -> None: """ self.__y = y + def teleport(self, x: int, y: int) -> None: + """ + Teleports the entity to a new position (x, y). + + Args: + x (int): The new X-coordinate. + y (int): The new Y-coordinate. + """ + self.__x = x + self.__y = y + @abstractmethod def get_char(self) -> str: """ diff --git a/api/entity/snake.py b/api/entity/snake.py index 4e2b8b3..d9fa9b9 100644 --- a/api/entity/snake.py +++ b/api/entity/snake.py @@ -3,6 +3,8 @@ from api.entity.apple import Apple from api.world import World from api.entity.entity import Entity +import random +from collections import deque class Snake(Entity): @@ -11,12 +13,12 @@ class Snake(Entity): world. Attributes: - __x (int): The X-coordinate of the snake's head. - __y (int): The Y-coordinate of the snake's head. - __body (list[tuple[int, int]]): A list representing the snake's body - segments. - __world (World): The game world where the snake exists. - __last_direction (Direction): The last movement direction of the snake. + __body (deque[tuple[int, int]]): + Deque of tuples representing the snake's body segments. + __world (World): + The game world where the snake exists. + __last_direction (Direction): + The last movement direction of the snake. """ def __init__(self, world: World, x: int, y: int, direction: Direction): @@ -32,7 +34,7 @@ def __init__(self, world: World, x: int, y: int, direction: Direction): """ super().__init__(x, y) - self.__body: list[tuple[int, int]] = [] + self.__body: deque[tuple[int, int]] = deque() self.__world: World = world self.__last_direction: Direction = direction @@ -44,42 +46,66 @@ def __init__(self, world: World, x: int, y: int, direction: Direction): y += dir_y self.__body.append((x, y)) - def move(self, direction: Direction): + def teleport(self, x: int, y: int) -> None: + """ + Teleports the snake to a new position and resets its body direction. + + Args: + x (int): The new X position. + y (int): The new Y position. + """ + super().teleport(x, y) + + new_body = deque() + for _ in range(len(self.__body)): + dir_name = self.__last_direction.name + self.__last_direction = random.choice([ + d for d in list(Direction) if d.name != dir_name + ]) + dir_x, dir_y = self.__last_direction.value + + x += dir_x + y += dir_y + + new_body.append((x, y)) + + self.__body = new_body + + def move(self, direction: Direction) -> None: """ Moves the snake in the given direction. If the new location is not passable, the game ends. Args: direction (Direction): The direction in which the snake should - move. + move. Raises: GameOver: If the snake collides with an obstacle or itself. """ x, y = direction.value - info = self.__world.get_location(self.get_x() + x, self.get_y() + y) + new_x = self.get_x() + x + new_y = self.get_y() + y + info = self.__world.get_location(new_x, new_y) - if info.is_wall(): - raise GameOver("End game") - - if (self.get_x() + x, self.get_y() + y) in self.__body: + if info.is_wall() or (new_x, new_y) in self.__body: raise GameOver("End game") if isinstance(info.get_entity(), Apple): self.eat(info.get_entity()) - self.set_x(self.get_x() + x) - self.set_y(self.get_y() + y) + self.set_x(new_x) + self.set_y(new_y) self.__last_direction = direction # Move the body segments following the head - self.__body.insert(0, (self.get_x() - x, self.get_y() - y)) - del self.__body[-1] + self.__body.appendleft((self.get_x() - x, self.get_y() - y)) + self.__body.pop() - def eat(self, apple: Apple): + def eat(self, apple: Apple) -> None: """ - Handles the snake eating an apple. The snake grows if the apple is - green, otherwise, it loses a segment. + Handles the snake eating an apple. + The snake grows if the apple is green, otherwise, it loses a segment. Args: apple (Apple): The apple that the snake is eating. @@ -94,11 +120,13 @@ def eat(self, apple: Apple): # Grow the snake by adding a new body segment self.__body.append((last_body[0] + x, last_body[1] + y)) else: - if len(self.__body) == 0: + if not self.__body: raise GameOver("End game") # Remove the last body segment - del self.__body[-1] + self.__body.pop() + + apple.consume() def size(self) -> int: """ @@ -110,33 +138,33 @@ def size(self) -> int: """ return len(self.__body) + 1 - def get_body(self) -> list[tuple[int, int]]: + def get_body(self) -> deque[tuple[int, int]]: """ Returns the list of body segments of the snake. Returns: - list[tuple[int, int]]: The body segments of the snake. + deque[tuple[int, int]]: The body segments of the snake. """ return self.__body def get_char(self) -> str: """ - Returns the character representation of the snake, colored for - terminal output. + Returns the character representation of the snake, colored for terminal + output. Returns: str: A string representing the snake, colored in yellow. """ return "\033[33m#\033[0m" - def render(self): + def render(self) -> list[tuple[str, int, int]]: """ Renders the snake and its body in the world. Returns: - list: A list containing the snake's head and body positions. + list[tuple[str, int, int]]: A list containing the snake's head and + body positions. """ render = super().render() - [render.append(("#", body[0], body[1])) for body in self.__body] - + render.extend(("#", body[0], body[1]) for body in self.__body) return render diff --git a/api/world.py b/api/world.py index 4075848..d86f9a2 100644 --- a/api/world.py +++ b/api/world.py @@ -2,6 +2,7 @@ from api.entity.entity import Entity import sys import copy +import random class World: @@ -23,7 +24,7 @@ def __init__(self, height=10, width=10): height (int, optional): The height of the world. Defaults to 10. width (int, optional): The width of the world. Defaults to 10. """ - self.__world: list = [] + self.__world: list[list[str]] = [] self.__height: int = height self.__width: int = width self.__entities: list[Entity] = [] @@ -68,6 +69,28 @@ def get_location(self, x: int, y: int) -> MapLocation: return MapLocation(x, y, is_wall, self.get_entity_at(x, y)) + def get_empty_locations(self) -> list[tuple[int, int]]: + """ + Retrieves a list of all empty locations in the world. + + Returns: + list[tuple[int, int]]: A list of coordinates (x, y) where there + are no entities. + """ + empty_list = [] + + for y in range(len(self.__world)): + for x in range(len(self.__world[y])): + ceil = self.__world[y][x] + + if ceil != ' ': + continue + + if self.get_entity_at(x, y) is None: + empty_list.append((x, y)) + + return empty_list + def get_entity_at(self, x: int, y: int) -> Entity | None: """ Retrieves an entity at the given coordinates. @@ -78,22 +101,35 @@ def get_entity_at(self, x: int, y: int) -> Entity | None: Returns: object | None: The entity found at the coordinates, or None if - empty. + empty. """ for entity in self.__entities: if entity.get_x() == x and entity.get_y() == y: return entity return None - def spawn_entity(self, entity) -> None: + def spawn_entity(self, entity: Entity) -> None: """ Adds an entity to the world. Args: entity (object): The entity to add. """ + x, y = random.choice(self.get_empty_locations()) + + entity.teleport(x, y) + self.__entities.append(entity) + def remove_entity(self, entity: Entity): + """ + Removes an entity from the world. + + Args: + entity (Entity): The entity to remove. + """ + self.__entities.remove(entity) + def render(self): """ Renders the current state of the world to the terminal. diff --git a/main.py b/main.py index 0a08cdf..8e1624d 100644 --- a/main.py +++ b/main.py @@ -6,10 +6,12 @@ world = World() snake = Snake(world, 5, 5, Direction.SOUTH) -green_apple = Apple(2, 2, AppleType.GREEN) +green_apple = Apple(world, 2, 2, AppleType.GREEN) +red_apple = Apple(world, 2, 2, AppleType.RED) world.spawn_entity(snake) world.spawn_entity(green_apple) +world.spawn_entity(red_apple) while True: world.render()