Files
snake/snake.py

447 lines
14 KiB
Python

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)