import curses import random import time from typing import Any import pygame import multiprocessing import signal class Game: def __init__(self, stdscr, queue): # Get curses standard screen self.stdscr = stdscr # Multiprocessing Queue self.queue = queue # Defaults self.snake_length = 3 self.snake_speed = 7 self.enable_effects = True self.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 = '▒' def load(self): # 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'])) curses.set_escdelay(1) # Get terminal (screen) height and width self.screen_height, self.screen_width = self.stdscr.getmaxyx() # Show selcome page self.effect('start') self.load_welcome() # 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) # Ask for snake speed self.snake_speed = self.ask_question(f"Snake Speed (1 to 10, Default {self.snake_speed}): ", int, 7, 1, 10) # Ask for Music self.enable_music = (self.ask_question(f"Turn on Music (y/N): ", str, 'N')).upper() == 'Y' # Ask for Effects self.enable_effects = (self.ask_question(f"Turn on Sound Effects (Y/n): ", str, 'Y')).upper() == 'Y' # 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) # Start music if self.enable_music: if self.music_started_once: self.music_unpause() else: self.music_start() else: self.music_pause() # Start game!!! self.start_game() def start_game(self): # Hide cursor 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!") time.sleep(2) exit() # 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)] # Initial food def get_new_food(): while True: 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 key = curses.KEY_RIGHT score = self.snake_length 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 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 # 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 self.effect('wall') break # Check self collisions if new_head in snake: # Crashed into self, quit self.effect('self') break snake.insert(0, new_head) # Check if snake ate food if snake[0] == food: self.effect('eat') score += 1 food = get_new_food() else: snake.pop() # Remove tail # Draw everything self.clear_screen() self.stdscr.attron(curses.color_pair(3)) self.stdscr.border(0) self.stdscr.attroff(curses.color_pair(3)) # 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)) # Draw food self.stdscr.addch(food[0], food[1], self.food_char, curses.color_pair(2)) # 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)) self.refresh_screen() # 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']) 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 y = self.screen_height // 2 x = (self.screen_width - len(value)) // 2 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 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 # 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.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']) time.sleep(2) self.ask_question(question, value_type, value_min, value_max) else: if not answer: answer = value_default return answer def load_welcome(self): 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']) time.sleep(2) def hide_cursor(self): curses.curs_set(0) def show_cursor(self): curses.curs_set(1) def clear_screen(self): self.stdscr.clear() def refresh_screen(self): self.stdscr.refresh() def exit_game(self): self.queue.put('quit') exit() def effect(self, effect): if self.enable_effects: self.queue.put(effect) def music_start(self): self.music_started_once = True self.queue.put('music.start') def music_stop(self): self.queue.put('music.stop') def music_pause(self): self.queue.put('music.pause') def music_unpause(self): 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__": 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) except KeyboardInterrupt: exit(0)