Skip to content
Draft
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
28 changes: 28 additions & 0 deletions src/game_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from src.snake.snakeColorGenerator import SnakeColorGenerator
from src.environment.pyEnvLibEnvironmentRepositoryImpl import PyEnvLibEnvironmentRepositoryImpl
from src.score.game_score import GameScore
from src.score.high_score_entry import HighScoreEntry
from src.score.high_score_repository import HighScoreRepository
from src.state.game_state_repository import GameStateRepository

logger = logging.getLogger(__name__)
Expand All @@ -22,6 +24,7 @@ class GameEngine:
def __init__(self, config):
self.config = config
self.state_repository = GameStateRepository()
self.high_score_repository = HighScoreRepository()

# Game state
self.level = 1
Expand All @@ -35,6 +38,9 @@ def __init__(self, config):
self.environment_repository = None
self.game_score = None
self.selected_snake_part = None

# Track maximum snake length for high score entry
self.max_snake_length = 0

def initialize_game(self):
"""Initialize or reset the game state"""
Expand Down Expand Up @@ -103,6 +109,7 @@ def handle_direction_input(self, direction: int) -> bool:
def handle_restart(self):
"""Handle game restart"""
logger.info("Restarting the game...")
self._save_high_score_if_eligible()
self.game_score.reset()
self.check_for_level_progress_and_reinitialize()

Expand All @@ -126,6 +133,11 @@ def update(self):
# Update score
self.game_score.calculate()

# Track maximum snake length
current_length = self.snake_part_repository.get_length()
if current_length > self.max_snake_length:
self.max_snake_length = current_length

# Handle tick timing
if self.config.limit_tick_speed:
self.tick += 1
Expand All @@ -143,6 +155,8 @@ def check_for_level_progress_and_reinitialize(self):
self.game_score.level_complete()
self.level += 1
else:
# Game ended (player died), save high score
self._save_high_score_if_eligible()
self.game_score.reset()

self.save_game_state()
Expand All @@ -154,6 +168,20 @@ def check_for_level_progress_and_reinitialize(self):
logger.info("Re-initializing the game")
self._initialize_level()

def _save_high_score_if_eligible(self):
"""Save high score if the current score qualifies"""
if self.game_score and self.game_score.cumulative_points > 0:
entry = HighScoreEntry(
score=self.game_score.cumulative_points,
length=self.max_snake_length,
level=self.level
)
is_high_score = self.high_score_repository.add_score(entry)
if is_high_score:
logger.info(f"New high score! Score: {entry.score}, Length: {entry.length}, Level: {entry.level}")
# Reset max length for next game
self.max_snake_length = 0

def get_game_state(self):
"""Get current game state for rendering"""
return {
Expand Down
145 changes: 133 additions & 12 deletions src/graphics/high_scores_menu.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import pygame
from src.lib.graphik.src.graphik import Graphik
from src.state.menu_state import MenuState
from src.score.high_score_repository import HighScoreRepository


class HighScoresMenu:
def __init__(self, config, game_display):
self.config = config
self.game_display = game_display
self.graphik = Graphik(game_display)
self.high_score_repository = HighScoreRepository()
self.scroll_offset = 0
self.max_visible_scores = 8 # Number of scores visible on screen at once
self._cached_high_scores = None # Cache for high scores

def refresh_scores(self):
"""Reload high scores from repository"""
self._cached_high_scores = self.high_score_repository.load()

def _get_high_scores(self):
"""Get high scores, loading them if not cached"""
if self._cached_high_scores is None:
self.refresh_scores()
return self._cached_high_scores

def handle_key_down(self, key):
"""Handle keyboard input - return to main menu on escape or enter"""
"""Handle keyboard input - return to main menu on escape or enter, scroll with arrow keys"""
high_scores = self._get_high_scores()
max_scroll = max(0, len(high_scores) - self.max_visible_scores)

if key == pygame.K_ESCAPE or key == pygame.K_RETURN:
return MenuState.MAIN_MENU
elif key == pygame.K_UP or key == pygame.K_w:
# Scroll up
self.scroll_offset = max(0, self.scroll_offset - 1)
elif key == pygame.K_DOWN or key == pygame.K_s:
# Scroll down
self.scroll_offset = min(max_scroll, self.scroll_offset + 1)

return None

def handle_mouse_click(self, pos):
Expand All @@ -32,28 +57,124 @@ def draw(self):
self.game_display.fill(self.config.black)

# Draw title
title_y = 50
self.graphik.drawText(
"HIGH SCORES",
current_width // 2,
current_height // 2 - 100,
title_y,
self.config.text_size,
self.config.green
)

# Draw placeholder text
self.graphik.drawText(
"High scores coming soon...",
current_width // 2,
current_height // 2,
self.config.text_size // 2,
self.config.white
)
# Load high scores
high_scores = self._get_high_scores()

if not high_scores:
# No scores yet
self.graphik.drawText(
"No high scores yet!",
current_width // 2,
current_height // 2,
self.config.text_size // 2,
self.config.white
)
self.graphik.drawText(
"Play the game to set a high score",
current_width // 2,
current_height // 2 + 50,
self.config.text_size // 3,
self.config.white
)
else:
# Draw table header
header_y = title_y + 80
col_rank_x = current_width // 2 - 250
col_score_x = current_width // 2 - 50
col_length_x = current_width // 2 + 100
col_level_x = current_width // 2 + 200

header_size = self.config.text_size // 3
self.graphik.drawText("RANK", col_rank_x, header_y, header_size, self.config.yellow)
self.graphik.drawText("SCORE", col_score_x, header_y, header_size, self.config.yellow)
self.graphik.drawText("LENGTH", col_length_x, header_y, header_size, self.config.yellow)
self.graphik.drawText("LEVEL", col_level_x, header_y, header_size, self.config.yellow)

# Draw scores
score_start_y = header_y + 60
score_spacing = 45
text_size = self.config.text_size // 3

# Bronze color for 3rd place
bronze_color = (205, 127, 50)

# Calculate which scores to display based on scroll offset
start_idx = self.scroll_offset
end_idx = min(start_idx + self.max_visible_scores, len(high_scores))

for i in range(start_idx, end_idx):
score_entry = high_scores[i]
y_pos = score_start_y + (i - start_idx) * score_spacing

# Highlight top 3 scores with different colors
if i == 0:
color = self.config.green # Gold for 1st
elif i == 1:
color = self.config.white # Silver for 2nd
elif i == 2:
color = bronze_color # Bronze for 3rd
else:
color = self.config.white

# Draw rank (1-indexed)
self.graphik.drawText(f"#{i + 1}", col_rank_x, y_pos, text_size, color)

# Draw score
self.graphik.drawText(str(score_entry.score), col_score_x, y_pos, text_size, color)

# Draw length
self.graphik.drawText(str(score_entry.length), col_length_x, y_pos, text_size, color)

# Draw level
self.graphik.drawText(str(score_entry.level), col_level_x, y_pos, text_size, color)

# Draw scroll indicators if needed
if self.scroll_offset > 0:
# Can scroll up
self.graphik.drawText(
"▲ More above",
current_width // 2,
score_start_y - 30,
self.config.text_size // 4,
self.config.yellow
)

if end_idx < len(high_scores):
# Can scroll down
last_visible_y = score_start_y + (end_idx - start_idx - 1) * score_spacing
self.graphik.drawText(
"▼ More below",
current_width // 2,
last_visible_y + 60,
self.config.text_size // 4,
self.config.yellow
)

# Draw instructions
instructions_y = current_height - 60
self.graphik.drawText(
"Press ESC or ENTER to return to main menu",
current_width // 2,
current_height // 2 + 100,
instructions_y,
self.config.text_size // 3,
self.config.yellow
)
)

# Draw scroll instructions if there are enough scores
if len(high_scores) > self.max_visible_scores:
self.graphik.drawText(
"Use UP/DOWN arrows to scroll",
current_width // 2,
instructions_y + 30,
self.config.text_size // 4,
self.config.yellow
)
5 changes: 5 additions & 0 deletions src/ophidian.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ def change_state(self, new_state):
elif new_state == MenuState.MAIN_MENU and self.current_state == MenuState.GAME:
# Save game state when returning to menu
self.save_game_state()
elif new_state == MenuState.HIGH_SCORES:
# Refresh high scores when entering the high scores menu
if hasattr(self, 'high_scores_menu'):
self.high_scores_menu.refresh_scores()
self.high_scores_menu.scroll_offset = 0 # Reset scroll position

self.current_state = new_state

Expand Down
48 changes: 48 additions & 0 deletions src/score/high_score_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from datetime import datetime


class HighScoreEntry:
"""Represents a single high score entry"""

def __init__(self, score, length, level, timestamp=None):
"""
Initialize a high score entry

Args:
score: The cumulative score achieved
length: The maximum snake length achieved
level: The highest level reached
timestamp: When the score was achieved (defaults to current time)
"""
self.score = score
self.length = length
self.level = level
self.timestamp = timestamp or datetime.now().isoformat()

def __eq__(self, other):
"""Compare two high score entries for equality"""
if not isinstance(other, HighScoreEntry):
return False
return (self.score == other.score and
self.length == other.length and
self.level == other.level and
self.timestamp == other.timestamp)

def to_dict(self):
"""Convert to dictionary for JSON serialization"""
return {
'score': self.score,
'length': self.length,
'level': self.level,
'timestamp': self.timestamp
}

@classmethod
def from_dict(cls, data):
"""Create from dictionary for JSON deserialization"""
return cls(
score=data.get('score', 0),
length=data.get('length', 0),
level=data.get('level', 1),
timestamp=data.get('timestamp')
)
Loading
Loading