442 lines
14 KiB
Python
442 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']))
|
|
|
|
# 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/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.25)
|
|
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)
|
|
|
|
|