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
25 changes: 19 additions & 6 deletions api/entity/apple.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import Enum
from api.entity.entity import Entity
from api.world import World


class AppleType(Enum):
Expand All @@ -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.

Expand All @@ -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:
"""
Expand All @@ -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:
"""
Expand All @@ -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
Expand All @@ -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)
11 changes: 11 additions & 0 deletions api/entity/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
90 changes: 59 additions & 31 deletions api/entity/snake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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

Expand All @@ -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.
Expand All @@ -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:
"""
Expand All @@ -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
42 changes: 39 additions & 3 deletions api/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from api.entity.entity import Entity
import sys
import copy
import random


class World:
Expand All @@ -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] = []
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down