diff --git a/src/core/rendering/pygame_surface_renderer.py b/src/core/rendering/pygame_surface_renderer.py index 845395dd..5e041b08 100644 --- a/src/core/rendering/pygame_surface_renderer.py +++ b/src/core/rendering/pygame_surface_renderer.py @@ -75,6 +75,13 @@ def draw_line( def draw_rect( self, color: tuple[int, int, int], rect: pygame.Rect, width: int = 0 ) -> None: ... + def draw_circle( + self, + color: tuple[int, int, int], + center: tuple[int, int], + radius: int, + width: int = 0, + ) -> None: ... class _RendererView(RenderEnqueue): @@ -136,6 +143,15 @@ def draw_rect( ) -> None: self._impl.draw_rect(color, rect, width) + def draw_circle( + self, + color: tuple[int, int, int], + center: tuple[int, int], + radius: int, + width: int = 0, + ) -> None: + self._impl.draw_circle(color, center, radius, width) + class PygameSurfaceRenderer: """Command queue wrapper for pygame surface rendering. @@ -280,6 +296,29 @@ def draw_rect( ) ) + def draw_circle( + self, + color: tuple[int, int, int], + center: tuple[int, int], + radius: int, + width: int = 0, + ) -> None: + """Queue a circle drawing operation. + + Args: + color: RGB color tuple + center: Center position (x, y) + radius: Circle radius in pixels + width: Line width (0 for filled circle) + """ + self._command_queue.append( + DrawCommand( + operation=pygame.draw.circle, + args=(self._surface, color, center, radius, width), + kwargs={}, + ) + ) + # Frame control methods (only for main/core, not exposed in view) def begin_frame( diff --git a/src/ecs/systems/entity_render.py b/src/ecs/systems/entity_render.py index 2238a7fe..a6f53a16 100644 --- a/src/ecs/systems/entity_render.py +++ b/src/ecs/systems/entity_render.py @@ -78,10 +78,96 @@ def draw_entity( # Get color tuple color = renderable.get_color_tuple() - # Render all shapes as rectangles for now - # (RenderEnqueue only has draw_rect and draw_line) - rect = pygame.Rect(pixel_x, pixel_y, cell_size, cell_size) - self._renderer.draw_rect(color, rect, 0) + # Render based on shape + if renderable.shape == "circle": + # For apples, draw realistic apple with stem and leaf + self._draw_realistic_apple(pixel_x, pixel_y, cell_size, color) + elif renderable.shape in ["square", "rectangle"]: + # Draw as rectangle (existing behavior) + rect = pygame.Rect(pixel_x, pixel_y, cell_size, cell_size) + self._renderer.draw_rect(color, rect, 0) + else: + # Default fallback to rectangle for unknown shapes + rect = pygame.Rect(pixel_x, pixel_y, cell_size, cell_size) + self._renderer.draw_rect(color, rect, 0) + + def _draw_realistic_apple( + self, pixel_x: int, pixel_y: int, cell_size: int, color: tuple[int, int, int] + ) -> None: + """Draw a realistic apple with elliptical body, stem and leaf. + + Args: + pixel_x: X position in pixels + pixel_y: Y position in pixels + cell_size: Size of the cell + color: RGB color of the apple + """ + # Apple body - slightly flattened ellipse + body_width = int(cell_size * 0.8) + body_height = int(cell_size * 0.7) + body_x = pixel_x + (cell_size - body_width) // 2 + body_y = pixel_y + int(cell_size * 0.15) + + # Draw apple body as ellipse using multiple circles for smooth appearance + center_x = body_x + body_width // 2 + + # Main body - draw filled ellipse approximation + radius_x = body_width // 2 + radius_y = body_height // 2 + + # Draw ellipse as multiple horizontal lines + for y in range(body_height): + dy = y - radius_y + if radius_y != 0: + # Ellipse equation: (x/rx)² + (y/ry)² = 1 + # Solve for x: x = rx * sqrt(1 - (y/ry)²) + normalized_y = dy / radius_y + if abs(normalized_y) <= 1: + half_width = int( + radius_x * (1 - normalized_y * normalized_y) ** 0.5 + ) + line_y = body_y + y + line_start = center_x - half_width + line_end = center_x + half_width + if half_width > 0: + self._renderer.draw_line( + color, (line_start, line_y), (line_end, line_y), 1 + ) + + # Apple stem (caule) - small brown rectangle + stem_color = (101, 67, 33) # Brown color + stem_width = max(2, cell_size // 10) + stem_height = max(3, cell_size // 6) + stem_x = center_x - stem_width // 2 + stem_y = pixel_y + 2 + stem_rect = pygame.Rect(stem_x, stem_y, stem_width, stem_height) + self._renderer.draw_rect(stem_color, stem_rect, 0) + + # Apple leaf (folha) - small green triangle-like shape + leaf_color = (34, 139, 34) # Forest green + leaf_size = max(3, cell_size // 8) + + # Draw leaf as small lines forming a leaf shape + leaf_x = stem_x + stem_width + 1 + leaf_y = stem_y + 1 + + # Leaf outline - draw a small oval-like shape + for i in range(leaf_size): + y_offset = i + if i < leaf_size // 2: + # Upper half - expanding + width = (i * 2) // 3 + 1 + else: + # Lower half - contracting + width = ((leaf_size - i) * 2) // 3 + 1 + + for w in range(width): + point_x = leaf_x + w + point_y = leaf_y + y_offset + # Draw individual pixels for leaf + self._renderer.draw_line( + leaf_color, (point_x, point_y), (point_x, point_y), 1 + ) def update(self, world: World) -> None: """Update method required by BaseSystem. diff --git a/src/game/scenes/menu.py b/src/game/scenes/menu.py deleted file mode 100644 index dc3414e3..00000000 --- a/src/game/scenes/menu.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2023, Monaco F. J. -# -# 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 . - -#!/usr/bin/env python3 -# -# Copyright (c) 2023, Monaco F. J. -# -# 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 . - -"""Menu scene.""" - -from __future__ import annotations - -import pygame -from typing import Optional - -from game.scenes.base_scene import BaseScene -from game.services.assets import GameAssets -from game.settings import GameSettings -from game.constants import ARENA_COLOR, MESSAGE_COLOR, SCORE_COLOR, WINDOW_TITLE - - -class MenuScene(BaseScene): - """Main menu scene.""" - - def __init__( - self, - pygame_adapter, - renderer, - width: int, - height: int, - assets: GameAssets, - settings: GameSettings, - ): - """Initialize the menu scene. - - Args: - pygame_adapter: Pygame IO adapter - renderer: Renderer for drawing - width: Scene width - height: Scene height - assets: Game assets - settings: Game settings - """ - super().__init__(pygame_adapter, renderer, width, height) - self._assets = assets - self._settings = settings - self._selected_index = 0 - self._menu_items = ["Start Game", "Game Modes", "Settings", "Quit"] - - def update(self, dt_ms: float) -> Optional[str]: - """Update menu logic. - - Args: - dt_ms: Delta time in milliseconds - - Returns: - Next scene name or None - """ - # Handle input - for event in self._pygame_adapter.get_events(): - if event.type == pygame.QUIT: - pygame.quit() - exit() - - elif event.type == pygame.KEYDOWN: - if event.key in (pygame.K_UP, pygame.K_w): - self._selected_index = (self._selected_index - 1) % len( - self._menu_items - ) - elif event.key in (pygame.K_DOWN, pygame.K_s): - self._selected_index = (self._selected_index + 1) % len( - self._menu_items - ) - elif event.key in (pygame.K_RETURN, pygame.K_SPACE): - if self._menu_items[self._selected_index] == "Start Game": - return "gameplay" - elif self._menu_items[self._selected_index] == "Game Modes": - return "game_modes" - elif self._menu_items[self._selected_index] == "Settings": - return "settings" - elif self._menu_items[self._selected_index] == "Quit": - pygame.quit() - exit() - elif event.key == pygame.K_ESCAPE: - pygame.quit() - exit() - - return None - - def render(self) -> None: - """Render the menu.""" - # Clear screen - self._renderer.fill(ARENA_COLOR) - - # Draw title (bigger and more prominent) - title = self._assets.render_custom( - WINDOW_TITLE, MESSAGE_COLOR, int(self._width / 8) - ) - title_rect = title.get_rect(center=(self._width / 2, self._height / 5)) - self._renderer.blit(title, title_rect) - - # Draw selected game mode below title - mode_text = self._get_selected_mode_text() - mode_surface = self._assets.render_custom( - mode_text, (150, 150, 150), int(self._width / 32) - ) - mode_rect = mode_surface.get_rect( - center=(self._width / 2, self._height / 5 + self._height * 0.10) - ) - self._renderer.blit(mode_surface, mode_rect) - - # Draw menu items - for i, item in enumerate(self._menu_items): - color = SCORE_COLOR if i == self._selected_index else MESSAGE_COLOR - text = self._assets.render_small(item, color) - rect = text.get_rect( - center=(self._width / 2, self._height / 2 + i * (self._height * 0.12)) - ) - self._renderer.blit(text, rect) - - def on_enter(self) -> None: - """Called when entering menu.""" - self._selected_index = 0 - - # play menu music when entering menu - if self._settings.get("background_music"): - try: - import pygame - from game.services.audio_service import AudioService - from game.services.assets import GameAssets - - # only reload if menu music is not already playing - if GameAssets._current_music_track != "assets/sound/menu.mp3": - pygame.mixer.music.load("assets/sound/menu.mp3") - pygame.mixer.music.play(-1) # loop - GameAssets._current_music_track = "assets/sound/menu.mp3" - AudioService._current_music_track = "assets/sound/menu.mp3" - except Exception: - pass - - def _get_selected_mode_text(self) -> str: - """Get text for currently selected game mode. - - Returns: - Display text for selected game mode - """ - try: - from game.scenes.game_modes import get_display_mode_name - - return get_display_mode_name() - except Exception: - return "Classic Snake Game"