Extended the code using Copilot with tons of cool features. Read the CHAT.md for what I asked and what it did
This commit is contained in:
@@ -1,85 +1,129 @@
|
||||
"""Snake: A CLI-based Snake game using curses."""
|
||||
import curses
|
||||
import random
|
||||
import time
|
||||
from typing import Any
|
||||
import pygame
|
||||
import multiprocessing
|
||||
import signal
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Tuple, List, Optional
|
||||
from multiprocessing import Process, Queue
|
||||
|
||||
from constants import (
|
||||
DEFAULT_SNAKE_LENGTH, DEFAULT_SNAKE_SPEED, MIN_SNAKE_SPEED, MAX_SNAKE_SPEED,
|
||||
HEAD_CHAR, BODY_CHAR, FOOD_CHAR, MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH,
|
||||
SPEED_TO_MS, SCREEN_PADDING, MIN_SNAKE_LENGTH
|
||||
)
|
||||
from theme import ThemeManager
|
||||
from sound_manager import SoundManager
|
||||
from high_score import HighScoreManager
|
||||
from game_config import GameConfig
|
||||
|
||||
|
||||
class Game:
|
||||
"""Main game class."""
|
||||
|
||||
def __init__(self, stdscr, queue):
|
||||
# Get curses standard screen
|
||||
def __init__(self, stdscr, queue: Queue, args: argparse.Namespace):
|
||||
self.stdscr = stdscr
|
||||
|
||||
# Multiprocessing Queue
|
||||
self.queue = queue
|
||||
self.args = args
|
||||
|
||||
# Defaults
|
||||
self.snake_length = 3
|
||||
self.snake_speed = 7
|
||||
self.enable_effects = True
|
||||
self.enable_music = False
|
||||
# Load configuration
|
||||
self.config = GameConfig()
|
||||
self.high_score_manager = HighScoreManager()
|
||||
|
||||
# Theme
|
||||
self.theme = ThemeManager()
|
||||
self.theme.initialize(stdscr)
|
||||
|
||||
# Game state
|
||||
self.snake_length = self.config.get('snake_length', DEFAULT_SNAKE_LENGTH)
|
||||
self.snake_speed = self.config.get('snake_speed', DEFAULT_SNAKE_SPEED)
|
||||
self.enable_effects = args.no_effects is False and self.config.get('enable_effects', True)
|
||||
self.enable_music = args.music and self.config.get('enable_music', False)
|
||||
self.music_started_once = False
|
||||
|
||||
# Snake and food characters
|
||||
# ASCII Table: https://theasciicode.com.ar/
|
||||
self.head_char = '█'
|
||||
#head_char = '■'
|
||||
#head_char = '0'
|
||||
#head_char_up = '▄'
|
||||
#head_char_down = '▀'
|
||||
self.body_char = '▓'
|
||||
#body_char = 'Φ'
|
||||
#body_char = '■'
|
||||
#body_char = 'O'
|
||||
#body_char = '░'
|
||||
#body_char = '■'
|
||||
#food_char = '💗'
|
||||
self.food_char = '█'
|
||||
#food_char = '▓'
|
||||
#food_char = '▒'
|
||||
# Game statistics
|
||||
self.game_start_time = 0.0
|
||||
|
||||
# Character customization
|
||||
self.head_char = self.config.get('head_char', HEAD_CHAR)
|
||||
self.body_char = self.config.get('body_char', BODY_CHAR)
|
||||
self.food_char = self.config.get('food_char', FOOD_CHAR)
|
||||
|
||||
def load(self):
|
||||
# Screen dimensions
|
||||
self.screen_height = 0
|
||||
self.screen_width = 0
|
||||
|
||||
# Setup colors
|
||||
curses.start_color()
|
||||
self.colors = {
|
||||
'green': 1,
|
||||
'red': 2,
|
||||
'white': 3
|
||||
}
|
||||
curses.init_pair(self.colors['green'], curses.COLOR_GREEN, curses.COLOR_BLACK) # Snake
|
||||
curses.init_pair(self.colors['red'], curses.COLOR_RED, curses.COLOR_BLACK) # Food
|
||||
curses.init_pair(self.colors['white'], curses.COLOR_WHITE, curses.COLOR_BLACK) # Score/Text
|
||||
self.stdscr.bkgd(' ', curses.color_pair(self.colors['white']))
|
||||
# Player name
|
||||
self.player_name = ""
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load game and show setup menu."""
|
||||
curses.set_escdelay(1)
|
||||
|
||||
# Get terminal (screen) height and width
|
||||
self.screen_height, self.screen_width = self.stdscr.getmaxyx()
|
||||
|
||||
# Show selcome page
|
||||
# Show welcome page
|
||||
self.effect('start')
|
||||
self.load_welcome()
|
||||
|
||||
# Ask for player name
|
||||
self.player_name = self.ask_question(
|
||||
"Enter your name: ",
|
||||
str,
|
||||
"Player"
|
||||
)
|
||||
|
||||
# Show high score for this player if exists
|
||||
stats = self.high_score_manager.get_player_stats(self.player_name)
|
||||
if stats['total_games'] > 0:
|
||||
self.print_center(
|
||||
f"High Score: {stats['high_score']} | Games: {stats['total_games']}",
|
||||
1,
|
||||
self.theme.get_color_pair('text')
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
# Ask for snake length
|
||||
self.snake_length = self.ask_question(f"Snake Length (1 to {self.screen_width - 5}, Default {self.snake_length}): ", int, 3, 3, self.screen_width - 5)
|
||||
self.snake_length = self.ask_question(
|
||||
f"Snake Length (1 to {self.screen_width - 2*SCREEN_PADDING}, Default {self.snake_length}): ",
|
||||
int,
|
||||
self.snake_length,
|
||||
MIN_SNAKE_LENGTH,
|
||||
self.screen_width - 2*SCREEN_PADDING
|
||||
)
|
||||
|
||||
# Ask for snake speed
|
||||
self.snake_speed = self.ask_question(f"Snake Speed (1 to 10, Default {self.snake_speed}): ", int, 7, 1, 10)
|
||||
self.snake_speed = self.ask_question(
|
||||
f"Snake Speed (1 to 10, Default {self.snake_speed}): ",
|
||||
int,
|
||||
self.snake_speed,
|
||||
MIN_SNAKE_SPEED,
|
||||
MAX_SNAKE_SPEED
|
||||
)
|
||||
|
||||
# Ask for Music
|
||||
self.enable_music = (self.ask_question(f"Turn on Music (y/N): ", str, 'N')).upper() == 'Y'
|
||||
# Ask for Music (only if not disabled via CLI)
|
||||
if not args.no_music:
|
||||
enable_music = (self.ask_question(
|
||||
f"Turn on Music (Y/n): ",
|
||||
str,
|
||||
'Y'
|
||||
)).upper() == 'Y'
|
||||
self.enable_music = enable_music
|
||||
self.config.set('enable_music', enable_music)
|
||||
|
||||
# Ask for Effects
|
||||
self.enable_effects = (self.ask_question(f"Turn on Sound Effects (Y/n): ", str, 'Y')).upper() == 'Y'
|
||||
# Ask for Effects (only if not disabled via CLI)
|
||||
if not args.no_effects:
|
||||
enable_effects = (self.ask_question(
|
||||
f"Turn on Sound Effects (Y/n): ",
|
||||
str,
|
||||
'Y'
|
||||
)).upper() == 'Y'
|
||||
self.enable_effects = enable_effects
|
||||
self.config.set('enable_effects', enable_effects)
|
||||
|
||||
# Debug Inputs
|
||||
#self.print_center(f"Length: {self.snake_length}, Speed: {self.snake_speed}, Width: {self.screen_width}, Height: {self.screen_height}")
|
||||
#time.sleep(2)
|
||||
# Save preferences
|
||||
self.config.set('snake_length', self.snake_length)
|
||||
self.config.set('snake_speed', self.snake_speed)
|
||||
|
||||
# Start music
|
||||
if self.enable_music:
|
||||
@@ -90,357 +134,440 @@ class Game:
|
||||
else:
|
||||
self.music_pause()
|
||||
|
||||
# Start game!!!
|
||||
# Start game
|
||||
self.start_game()
|
||||
|
||||
|
||||
def start_game(self):
|
||||
# Hide cursor
|
||||
def start_game(self) -> None:
|
||||
"""Main game loop."""
|
||||
self.hide_cursor()
|
||||
|
||||
# Non-blocking getch (get character)
|
||||
self.stdscr.nodelay(1)
|
||||
|
||||
# Calculate snake speed
|
||||
wait_for_input_ms = {
|
||||
1: 250, 2: 225, 3: 200,
|
||||
4: 175, 5: 150, 6: 125,
|
||||
7: 100, 8: 75, 9: 50, 10: 25
|
||||
}
|
||||
self.stdscr.timeout(wait_for_input_ms[self.snake_speed])
|
||||
|
||||
# Ensure window is large enough
|
||||
if self.screen_height < 10 or self.screen_width < 20:
|
||||
self.print_center("Terminal window is too small!")
|
||||
# Validate screen size
|
||||
if self.screen_height < MIN_SCREEN_HEIGHT or self.screen_width < MIN_SCREEN_WIDTH:
|
||||
self.print_center("Terminal window is too small!", 0, self.theme.get_color_pair('food'))
|
||||
time.sleep(2)
|
||||
exit()
|
||||
return
|
||||
|
||||
# Start the head at x=self.snake_length so the tail ends at x=1
|
||||
snake = [[self.screen_height // 2, self.snake_length - i] for i in range(self.snake_length)]
|
||||
# Set timeout based on snake speed
|
||||
self.stdscr.timeout(SPEED_TO_MS[self.snake_speed])
|
||||
|
||||
# Initial food
|
||||
def get_new_food():
|
||||
# Initialize snake at center
|
||||
snake: List[List[int]] = [
|
||||
[self.screen_height // 2, self.snake_length - i]
|
||||
for i in range(self.snake_length)
|
||||
]
|
||||
|
||||
# Get food
|
||||
def get_new_food() -> List[int]:
|
||||
while True:
|
||||
nf = [random.randint(1, self.screen_height - 2), random.randint(1, self.screen_width - 2)]
|
||||
nf = [
|
||||
random.randint(1, self.screen_height - 2),
|
||||
random.randint(1, self.screen_width - 2)
|
||||
]
|
||||
if nf not in snake:
|
||||
return nf
|
||||
|
||||
food = get_new_food()
|
||||
|
||||
# Initial direction
|
||||
# Game state
|
||||
key = curses.KEY_RIGHT
|
||||
score = self.snake_length
|
||||
self.game_start_time = time.time()
|
||||
|
||||
# Main game loop
|
||||
while True:
|
||||
next_key = self.stdscr.getch()
|
||||
# If no key is pressed, next_key is -1
|
||||
# Update direction if it's a valid arrow key and not opposite to current
|
||||
if next_key != -1:
|
||||
# DOWN
|
||||
if next_key == curses.KEY_DOWN and key != curses.KEY_UP:
|
||||
key = next_key
|
||||
# UP
|
||||
elif next_key == curses.KEY_UP and key != curses.KEY_DOWN:
|
||||
key = next_key
|
||||
# LEFT
|
||||
elif next_key == curses.KEY_LEFT and key != curses.KEY_RIGHT:
|
||||
key = next_key
|
||||
# RIGHT
|
||||
elif next_key == curses.KEY_RIGHT and key != curses.KEY_LEFT:
|
||||
key = next_key
|
||||
# ESCAPE Menu
|
||||
elif next_key == 27:
|
||||
previous_key = key
|
||||
if self.enable_music: self.music_pause()
|
||||
self.print_center(f"GAME PAUSED...", 0, self.colors['red'])
|
||||
self.print_center(f"ESC Resume, (R)estart, (Q)uit, (M)enu", 2, self.colors['green'])
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
if key == 27:
|
||||
break
|
||||
elif key == ord('r'):
|
||||
if self.enable_music: self.music_unpause()
|
||||
self.start_game()
|
||||
return
|
||||
elif key == ord('q'):
|
||||
self.exit_game()
|
||||
elif key == ord('m'):
|
||||
self.load()
|
||||
return
|
||||
# Resume
|
||||
key = previous_key
|
||||
if self.enable_music: self.music_unpause()
|
||||
continue
|
||||
# (R)estart
|
||||
elif next_key == ord('r'):
|
||||
self.start_game()
|
||||
return
|
||||
# # (Q)uit
|
||||
# elif next_key == ord('q'):
|
||||
# break
|
||||
|
||||
# Calculate new head
|
||||
# Handle input
|
||||
if next_key != -1:
|
||||
key = self._handle_input(key, next_key)
|
||||
if key is None: # Quit from menu
|
||||
return
|
||||
|
||||
# Calculate new head position
|
||||
head = snake[0]
|
||||
if key == curses.KEY_DOWN:
|
||||
new_head = [head[0] + 1, head[1]]
|
||||
elif key == curses.KEY_UP:
|
||||
new_head = [head[0] - 1, head[1]]
|
||||
elif key == curses.KEY_LEFT:
|
||||
new_head = [head[0], head[1] - 1]
|
||||
elif key == curses.KEY_RIGHT:
|
||||
new_head = [head[0], head[1] + 1]
|
||||
else:
|
||||
new_head = [head[0], head[1]]
|
||||
pass
|
||||
# Should not happen
|
||||
#exit('yyyyy')
|
||||
#new_head = head
|
||||
new_head = self._calculate_new_head(head, key)
|
||||
|
||||
# Check wall collisions
|
||||
if (new_head[0] in [0, self.screen_height - 1] or
|
||||
new_head[1] in [0, self.screen_width - 1]):
|
||||
# Crashed into wall, quit
|
||||
if self._check_wall_collision(new_head):
|
||||
self.effect('wall')
|
||||
break
|
||||
|
||||
# Check self collisions
|
||||
if new_head in snake:
|
||||
# Crashed into self, quit
|
||||
self.effect('self')
|
||||
break
|
||||
|
||||
# Add new head
|
||||
snake.insert(0, new_head)
|
||||
|
||||
# Check if snake ate food
|
||||
# Check if food eaten
|
||||
if snake[0] == food:
|
||||
self.effect('eat')
|
||||
score += 1
|
||||
food = get_new_food()
|
||||
else:
|
||||
snake.pop() # Remove tail
|
||||
snake.pop()
|
||||
|
||||
# Draw everything
|
||||
self.clear_screen()
|
||||
self.stdscr.attron(curses.color_pair(3))
|
||||
self.stdscr.border(0)
|
||||
self.stdscr.attroff(curses.color_pair(3))
|
||||
# Render frame
|
||||
self._render_frame(snake, food, score)
|
||||
|
||||
# Draw snake
|
||||
for i, (y, x) in enumerate(snake):
|
||||
char = self.head_char if i == 0 else self.body_char
|
||||
self.stdscr.addch(y, x, char, curses.color_pair(1))
|
||||
# Game over
|
||||
self._handle_game_over(score, self.snake_length)
|
||||
|
||||
# Draw food
|
||||
self.stdscr.addch(food[0], food[1], self.food_char, curses.color_pair(2))
|
||||
def _handle_input(self, current_key: int, next_key: int) -> Optional[int]:
|
||||
"""Handle player input and return new direction, or None if quitting."""
|
||||
# Arrow key handling
|
||||
if next_key == curses.KEY_DOWN and current_key != curses.KEY_UP:
|
||||
return next_key
|
||||
elif next_key == curses.KEY_UP and current_key != curses.KEY_DOWN:
|
||||
return next_key
|
||||
elif next_key == curses.KEY_LEFT and current_key != curses.KEY_RIGHT:
|
||||
return next_key
|
||||
elif next_key == curses.KEY_RIGHT and current_key != curses.KEY_LEFT:
|
||||
return next_key
|
||||
elif next_key == 27: # ESC - pause menu
|
||||
return self._handle_pause_menu(current_key)
|
||||
elif next_key == ord('r'): # Restart
|
||||
self.start_game()
|
||||
return None
|
||||
|
||||
# Draw score
|
||||
self.stdscr.addstr(0, 2, f' Length: {score} @ {self.snake_speed}MPH ({self.screen_width}x{self.screen_height}) ', curses.color_pair(3))
|
||||
return current_key
|
||||
|
||||
self.refresh_screen()
|
||||
def _handle_pause_menu(self, previous_key: int) -> Optional[int]:
|
||||
"""Handle pause menu. Returns new key or None if quitting."""
|
||||
self._apply_music_state(paused=True)
|
||||
|
||||
# Game Over screen
|
||||
duration = int(time.time() - self.game_start_time)
|
||||
menu_text = f"GAME PAUSED (elapsed: {duration}s)"
|
||||
music_status = "ON" if self.enable_music else "OFF"
|
||||
effects_status = "ON" if self.enable_effects else "OFF"
|
||||
options = f"ESC Resume | R Restart | M Menu | M(U)sic {music_status} | (E)ffects {effects_status} | Q Quit"
|
||||
|
||||
self.print_center(menu_text, 0, self.theme.get_color_pair('food'))
|
||||
self.print_center(options, 2, self.theme.get_color_pair('snake'))
|
||||
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
if key == 27: # ESC - resume
|
||||
self._apply_music_state(paused=False)
|
||||
return previous_key
|
||||
elif key == ord('r'): # Restart
|
||||
self._apply_music_state(paused=False)
|
||||
self.start_game()
|
||||
return None
|
||||
elif key == ord('m'): # Menu
|
||||
self.load()
|
||||
return None
|
||||
elif key == ord('u'): # Toggle music
|
||||
self.enable_music = not self.enable_music
|
||||
self.config.set('enable_music', self.enable_music)
|
||||
self._apply_music_state(paused=True)
|
||||
music_status = "ON" if self.enable_music else "OFF"
|
||||
effects_status = "ON" if self.enable_effects else "OFF"
|
||||
options = f"ESC Resume | R Restart | M Menu | M(U)sic {music_status} | (E)ffects {effects_status} | Q Quit"
|
||||
self.print_center(menu_text, 0, self.theme.get_color_pair('food'))
|
||||
self.print_center(options, 2, self.theme.get_color_pair('snake'))
|
||||
elif key == ord('e'): # Toggle effects
|
||||
self.enable_effects = not self.enable_effects
|
||||
self.config.set('enable_effects', self.enable_effects)
|
||||
music_status = "ON" if self.enable_music else "OFF"
|
||||
effects_status = "ON" if self.enable_effects else "OFF"
|
||||
options = f"ESC Resume | R Restart | M Menu | M(U)sic {music_status} | (E)ffects {effects_status} | Q Quit"
|
||||
self.print_center(menu_text, 0, self.theme.get_color_pair('food'))
|
||||
self.print_center(options, 2, self.theme.get_color_pair('snake'))
|
||||
elif key == ord('q'): # Quit
|
||||
self.exit_game()
|
||||
|
||||
def _calculate_new_head(self, head: List[int], key: int) -> List[int]:
|
||||
"""Calculate new head position based on direction."""
|
||||
y, x = head
|
||||
if key == curses.KEY_DOWN:
|
||||
return [y + 1, x]
|
||||
elif key == curses.KEY_UP:
|
||||
return [y - 1, x]
|
||||
elif key == curses.KEY_LEFT:
|
||||
return [y, x - 1]
|
||||
elif key == curses.KEY_RIGHT:
|
||||
return [y, x + 1]
|
||||
return head
|
||||
|
||||
def _check_wall_collision(self, head: List[int]) -> bool:
|
||||
"""Check if head hit a wall."""
|
||||
return (head[0] in [0, self.screen_height - 1] or
|
||||
head[1] in [0, self.screen_width - 1])
|
||||
|
||||
def _render_frame(self, snake: List[List[int]], food: List[int], score: int) -> None:
|
||||
"""Render the game frame."""
|
||||
self.clear_screen()
|
||||
|
||||
# Draw border
|
||||
self.stdscr.attron(curses.color_pair(self.theme.get_color_pair('text')))
|
||||
self.stdscr.border(0)
|
||||
self.stdscr.attroff(curses.color_pair(self.theme.get_color_pair('text')))
|
||||
|
||||
# Draw snake
|
||||
for i, (y, x) in enumerate(snake):
|
||||
char = self.head_char if i == 0 else self.body_char
|
||||
self.stdscr.addch(y, x, char, curses.color_pair(self.theme.get_color_pair('snake')))
|
||||
|
||||
# Draw food
|
||||
self.stdscr.addch(
|
||||
food[0], food[1], self.food_char,
|
||||
curses.color_pair(self.theme.get_color_pair('food'))
|
||||
)
|
||||
|
||||
# Draw score
|
||||
elapsed = int(time.time() - self.game_start_time)
|
||||
score_text = f' {self.player_name} | Length: {score} @ {self.snake_speed}MPH {elapsed}s '
|
||||
self.stdscr.addstr(
|
||||
0, 2, score_text,
|
||||
curses.color_pair(self.theme.get_color_pair('text'))
|
||||
)
|
||||
|
||||
self.refresh_screen()
|
||||
|
||||
def _handle_game_over(self, score: int, initial_length: int) -> None:
|
||||
"""Handle game over screen."""
|
||||
self.effect('over')
|
||||
self.print_center(f"GAME OVER! SCORE: {score}", 0, self.colors['red'])
|
||||
self.print_center(f"(R)estart, (Q)uit, (M)enu", 2, self.colors['green'])
|
||||
|
||||
# Save game
|
||||
self.high_score_manager.save_game(self.player_name, score, initial_length, self.snake_speed)
|
||||
stats = self.high_score_manager.get_player_stats(self.player_name)
|
||||
|
||||
elapsed = int(time.time() - self.game_start_time)
|
||||
self.print_center(
|
||||
f"{self.player_name} - GAME OVER! SCORE: {score} (Time: {elapsed}s)",
|
||||
-1,
|
||||
self.theme.get_color_pair('food')
|
||||
)
|
||||
self.print_center(
|
||||
f"Best: {stats['high_score']} | Games: {stats['total_games']}",
|
||||
0,
|
||||
self.theme.get_color_pair('text')
|
||||
)
|
||||
self.print_center(
|
||||
"(R)estart | (M)enu | (Q)uit",
|
||||
2,
|
||||
self.theme.get_color_pair('snake')
|
||||
)
|
||||
|
||||
self.stdscr.nodelay(0)
|
||||
#if self.enable_music: self.music_pause()
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
if key == ord('r'):
|
||||
#if self.enable_music: self.music_unpause()
|
||||
self.start_game()
|
||||
return
|
||||
elif key == ord('q'):
|
||||
# Game over, exit app!
|
||||
self.exit_game()
|
||||
elif key == ord('m'):
|
||||
self.load()
|
||||
return
|
||||
|
||||
|
||||
def print_center(self, value, y_offset = 0, color = 3):
|
||||
# Calculate center position
|
||||
def print_center(
|
||||
self,
|
||||
value: str,
|
||||
y_offset: int = 0,
|
||||
color: int = 3
|
||||
) -> None:
|
||||
"""Print text centered on screen."""
|
||||
y = self.screen_height // 2
|
||||
x = (self.screen_width - len(value)) // 2
|
||||
if x < 0: x = 1
|
||||
if x < 0:
|
||||
x = 1
|
||||
self.stdscr.addstr(int(y + y_offset), int(x), value, curses.color_pair(color))
|
||||
self.refresh_screen()
|
||||
|
||||
|
||||
def ask_question(self, question: str, value_type: Any, value_default: Any, value_min: int = 0, value_max: int = 0) -> Any:
|
||||
# Make the cursor visible
|
||||
def ask_question(
|
||||
self,
|
||||
question: str,
|
||||
value_type: type,
|
||||
value_default,
|
||||
value_min: int = 0,
|
||||
value_max: int = 0
|
||||
):
|
||||
"""Ask user a question and validate the response."""
|
||||
curses.curs_set(1)
|
||||
|
||||
# Calculate center position of prompt
|
||||
y = self.screen_height // 2
|
||||
x = (self.screen_width - len(question)) // 2
|
||||
if x < 0: x = 1
|
||||
if x < 0:
|
||||
x = 1
|
||||
|
||||
# Display question and wait for input + ENTER key to finish
|
||||
self.clear_screen()
|
||||
self.stdscr.addstr(y, x, question)
|
||||
self.refresh_screen()
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
|
||||
# Wait for answer with ENTER key to end input
|
||||
answer = ""
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
|
||||
# Check if the Enter key (ASCII 10 or 13) was pressed
|
||||
if key == curses.KEY_ENTER or key in [10, 13]:
|
||||
break
|
||||
# Handle backspace
|
||||
elif key == curses.KEY_BACKSPACE or key == 127:
|
||||
if len(answer) > 0:
|
||||
answer = answer[:-1]
|
||||
# Erase the character from the screen
|
||||
self.stdscr.addch('\b')
|
||||
self.stdscr.addch(' ')
|
||||
self.stdscr.addch('\b')
|
||||
# Handle regular characters (check for printable ASCII range if needed)
|
||||
elif 32 <= key <= 126:
|
||||
answer += chr(key)
|
||||
self.stdscr.addch(key) # Manually echo the character
|
||||
|
||||
self.clear_screen()
|
||||
self.stdscr.addstr(y, x, question)
|
||||
self.refresh_screen()
|
||||
|
||||
# Validate input
|
||||
if value_type == int:
|
||||
try:
|
||||
answer = int(answer)
|
||||
except Exception as e:
|
||||
answer = value_default
|
||||
if answer < value_min or answer > value_max:
|
||||
# Invalid integer, ask again
|
||||
self.print_center(f"ERROR: Must be an integer between {value_min} and {value_max}", 2, self.colors['red'])
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
|
||||
answer = ""
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
|
||||
if key == curses.KEY_ENTER or key in [10, 13]:
|
||||
break
|
||||
elif key == curses.KEY_BACKSPACE or key == 127:
|
||||
if len(answer) > 0:
|
||||
answer = answer[:-1]
|
||||
self.stdscr.addch(ord('\b'))
|
||||
self.stdscr.addch(ord(' '))
|
||||
self.stdscr.addch(ord('\b'))
|
||||
elif 32 <= key <= 126:
|
||||
answer += chr(key)
|
||||
self.stdscr.addch(key)
|
||||
|
||||
self.refresh_screen()
|
||||
|
||||
# Validate input
|
||||
if value_type == int:
|
||||
if not answer: # Empty answer, use default
|
||||
curses.curs_set(0)
|
||||
return value_default
|
||||
try:
|
||||
answer = int(answer)
|
||||
if value_min <= answer <= value_max:
|
||||
curses.curs_set(0)
|
||||
return answer
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Invalid input, show error and retry
|
||||
self.print_center(
|
||||
f"ERROR: Must be {value_min}-{value_max}",
|
||||
1,
|
||||
self.theme.get_color_pair('food')
|
||||
)
|
||||
time.sleep(2)
|
||||
self.ask_question(question, value_type, value_min, value_max)
|
||||
else:
|
||||
if not answer:
|
||||
answer = value_default
|
||||
else:
|
||||
# String input
|
||||
if not answer:
|
||||
answer = value_default
|
||||
curses.curs_set(0)
|
||||
return answer
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def load_welcome(self):
|
||||
def load_welcome(self) -> None:
|
||||
"""Show welcome screen."""
|
||||
self.hide_cursor()
|
||||
self.clear_screen()
|
||||
self.print_center("█▓▒░ Welcome to Curses Python!!! ░▒▓█", 0, self.colors['red'])
|
||||
self.print_center("by mReschke Productions", 2, self.colors['green'])
|
||||
self.print_center("█▓▒░ Welcome to Snake CLI! ░▒▓█", 0, self.theme.get_color_pair('food'))
|
||||
self.print_center("by mReschke Productions", 2, self.theme.get_color_pair('snake'))
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def hide_cursor(self):
|
||||
def hide_cursor(self) -> None:
|
||||
"""Hide the cursor."""
|
||||
curses.curs_set(0)
|
||||
|
||||
|
||||
def show_cursor(self):
|
||||
def show_cursor(self) -> None:
|
||||
"""Show the cursor."""
|
||||
curses.curs_set(1)
|
||||
|
||||
|
||||
def clear_screen(self):
|
||||
def clear_screen(self) -> None:
|
||||
"""Clear the screen."""
|
||||
self.stdscr.clear()
|
||||
|
||||
|
||||
def refresh_screen(self):
|
||||
def refresh_screen(self) -> None:
|
||||
"""Refresh the screen."""
|
||||
self.stdscr.refresh()
|
||||
|
||||
def exit_game(self):
|
||||
def exit_game(self) -> None:
|
||||
"""Exit the game."""
|
||||
self.queue.put('quit')
|
||||
exit()
|
||||
exit(0)
|
||||
|
||||
def effect(self, effect):
|
||||
def effect(self, effect: str) -> None:
|
||||
"""Play a sound effect."""
|
||||
if self.enable_effects:
|
||||
self.queue.put(effect)
|
||||
|
||||
def music_start(self):
|
||||
def _apply_music_state(self, paused: bool = False) -> None:
|
||||
"""Apply current music setting and requested pause state."""
|
||||
if not self.enable_music:
|
||||
self.music_pause()
|
||||
return
|
||||
|
||||
if not self.music_started_once:
|
||||
self.music_start()
|
||||
if paused:
|
||||
self.music_pause()
|
||||
return
|
||||
|
||||
if paused:
|
||||
self.music_pause()
|
||||
else:
|
||||
self.music_unpause()
|
||||
|
||||
def music_start(self) -> None:
|
||||
"""Start background music."""
|
||||
self.music_started_once = True
|
||||
self.queue.put('music.start')
|
||||
|
||||
def music_stop(self):
|
||||
self.queue.put('music.stop')
|
||||
|
||||
def music_pause(self):
|
||||
def music_pause(self) -> None:
|
||||
"""Pause music."""
|
||||
self.queue.put('music.pause')
|
||||
|
||||
def music_unpause(self):
|
||||
def music_unpause(self) -> None:
|
||||
"""Resume music."""
|
||||
self.queue.put('music.unpause')
|
||||
|
||||
|
||||
def main(stdscr, queue):
|
||||
game = Game(stdscr, queue)
|
||||
game.load()
|
||||
|
||||
|
||||
def sound_process(sound_queue):
|
||||
# Initialize pygame mixer in the new process
|
||||
pygame.mixer.init()
|
||||
|
||||
effects = {
|
||||
'eat': pygame.mixer.Sound('effects/bing.mp3'),
|
||||
'wall': pygame.mixer.Sound('effects/wall.mp3'),
|
||||
'self': pygame.mixer.Sound('effects/cut.mp3'),
|
||||
'over': pygame.mixer.Sound('effects/over.mp3'),
|
||||
'start': pygame.mixer.Sound('effects/start2.mp3'),
|
||||
}
|
||||
|
||||
# Load sounds/music (adjust file paths as needed)
|
||||
# Use .wav or .ogg for better compatibility
|
||||
#effect = pygame.mixer.Sound('effects/bing.mp3')
|
||||
#music_file = 'music/QonsVivaEveryone.mp3'
|
||||
#music_file = 'music/Aztec.mp3'
|
||||
#music_file = 'music/Snug_Neon_Artery.mp3'
|
||||
music_file = 'music/bubbles.ogg'
|
||||
#music_file = 'music/s.mp3'
|
||||
|
||||
while True:
|
||||
if not sound_queue.empty():
|
||||
command = sound_queue.get()
|
||||
|
||||
# Play effects if requested
|
||||
for effect, sound in effects.items():
|
||||
if command == effect:
|
||||
sound.play()
|
||||
|
||||
# Play music
|
||||
if command == 'music.start':
|
||||
pygame.mixer.music.load(music_file)
|
||||
pygame.mixer.music.set_volume(0.5)
|
||||
pygame.mixer.music.play(-1) # Play music indefinitely
|
||||
elif command == 'music.stop':
|
||||
pygame.mixer.music.stop()
|
||||
elif command == 'music.pause':
|
||||
pygame.mixer.music.pause();
|
||||
elif command == 'music.unpause':
|
||||
pygame.mixer.music.unpause();
|
||||
|
||||
# Quit came
|
||||
elif command == 'quit':
|
||||
break
|
||||
|
||||
# Small delay to prevent high CPU usage
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main(stdscr, queue: Queue, args: argparse.Namespace) -> None:
|
||||
"""Main entry point."""
|
||||
try:
|
||||
|
||||
# Handle CTRL+C (uvicore also handles with Aborted! but this catches other odd cases)
|
||||
signal.signal(signal.SIGINT, lambda sig, frame: exit())
|
||||
|
||||
# Create a queue for communication
|
||||
queue = multiprocessing.Queue()
|
||||
|
||||
# Start the sound process
|
||||
p = multiprocessing.Process(target=sound_process, args=(queue,))
|
||||
p.start()
|
||||
|
||||
# Start curses game loop
|
||||
game = curses.wrapper(main, queue)
|
||||
game = Game(stdscr, queue, args)
|
||||
game.load()
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(description='Snake CLI Game')
|
||||
parser.add_argument(
|
||||
'--music',
|
||||
action='store_true',
|
||||
help='Enable background music'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-effects',
|
||||
action='store_true',
|
||||
help='Disable sound effects'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-music',
|
||||
action='store_true',
|
||||
help='Disable music option in menu'
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
signal.signal(signal.SIGINT, lambda sig, frame: exit(0))
|
||||
|
||||
args = parse_args()
|
||||
queue = Queue()
|
||||
|
||||
# Start sound process
|
||||
sound_manager = SoundManager()
|
||||
p = Process(target=sound_manager.process_queue, args=(queue,))
|
||||
p.daemon = True
|
||||
p.start()
|
||||
|
||||
# Start game
|
||||
curses.wrapper(main, queue, args)
|
||||
|
||||
# Cleanup
|
||||
queue.put('quit')
|
||||
p.join(timeout=2)
|
||||
if p.is_alive():
|
||||
p.terminate()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user