Files
snake/snake.py
2026-03-12 07:46:27 -06:00

381 lines
12 KiB
Python

import curses
import random
import time
from typing import Any
import pygame
import multiprocessing
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
# 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']))
# Get terminal (screen) height and width
self.screen_height, self.screen_width = self.stdscr.getmaxyx()
# Start Music
self.queue.put('music.start')
# Show selcome page
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)
# 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 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
}
# if self.snake_speed == 1: wait_for_input_ms = 250
# if self.snake_speed == 2: wait_for_input_ms = 225
# if self.snake_speed == 3: wait_for_input_ms = 200
# if self.snake_speed == 4: wait_for_input_ms = 175
# if self.snake_speed == 5: wait_for_input_ms = 150
# if self.snake_speed == 6: wait_for_input_ms = 125
# if self.snake_speed == 7: wait_for_input_ms = 100
# if self.snake_speed == 8: wait_for_input_ms = 75
# if self.snake_speed == 9: wait_for_input_ms = 50
# if self.snake_speed == 10: wait_for_input_ms = 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
# (P)ause
elif next_key == ord('p'):
previous_key = key
self.print_center("Game Paused. Press P to Resume.")
while True:
key = self.stdscr.getch()
if key == ord('p'):
break
key = previous_key
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 collisions
if (new_head[0] in [0, self.screen_height - 1] or
new_head[1] in [0, self.screen_width - 1] or
new_head in snake):
# Crashed into something, show quit screen
self.queue.put('effect.wall')
break
snake.insert(0, new_head)
# Check if snake ate food
if snake[0] == food:
self.queue.put('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.print_center(f"GAME OVER! SCORE: {score}", 0, self.colors['red'])
self.print_center(f"(R)estart, or (Q)uit", 2, self.colors['green'])
self.stdscr.nodelay(0)
while True:
key = self.stdscr.getch()
if key == ord('r'):
self.start_game()
return
elif key == ord('q'):
# Game over, exit app!
self.exit_game()
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, value_max: int) -> 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)
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 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/cut.mp3'),
'self': pygame.mixer.Sound('effects/cut.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/Guitar_Sound.mp3'
music_file = 'music/s.mp3'
while True:
if not sound_queue.empty():
command = sound_queue.get()
if command == 'effect.eat':
effects['eat'].play()
if command == 'effect.wall':
effects['wall'].play()
if command == 'effect.self':
effects['self'].play()
elif command == 'music.start':
pygame.mixer.music.load(music_file)
pygame.mixer.music.set_volume(0.25)
pygame.mixer.music.play(-1) # Play music indefinitely
elif command == 'music.stop':
pygame.mixer.music.stop()
elif command == 'quit':
break
time.sleep(0.1) # Small delay to prevent high CPU usage
if __name__ == "__main__":
# 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)