import curses import random import time from typing import Any class Game: def __init__(self, stdscr): # Get curses standard screen self.stdscr = stdscr # 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() # 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 break snake.insert(0, new_head) # Check if snake ate food if snake[0] == food: 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! exit() 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 main(stdscr): game = Game(stdscr) game.load() if __name__ == "__main__": game = curses.wrapper(main)