From b6379678233f00152c702dc02d303bf1add5bd72 Mon Sep 17 00:00:00 2001 From: Matthew Reschke Date: Wed, 11 Mar 2026 16:12:08 -0600 Subject: [PATCH] Full refactor --- .editorconfig | 47 ++++++ .gitignore | 76 +++++++++ snake.py | 456 ++++++++++++++++++++++++++++++++------------------ 3 files changed, 418 insertions(+), 161 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80f670d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# Global settings +# By default everything should be 4 spaces +[*] +charset = utf-8 +indent_size = 4 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +# Ruby +[*.{rb,ru}] +indent_size = 2 +indent_style = space + +# Yaml and Json +[*.{yaml,yml,json}] +indent_size = 2 +indent_style = space + +# Terraform +[*.tf] +indent_size = 2 +indent_style = space + +# Html, Css, Js, Ts +[*.{js,jsx,ts,tsx,css,scss,saas,html,htm}] +indent_size = 2 +indent_style = space + +# INI and cfg, ensure 4 TABS +[*.{conf,cfg,ini}] +indent_size = 4 +indent_style = tab + +# Markdown specific +[*.{md,markdown}] +indent_size = 4 +indent_style = space +max_line_length = off +trim_trailing_whitespace = false + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2ad499 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# mReschke Python .gitignore file standard +# Latest Version https://git.mreschke.net/-/snippets/4 +# mReschke 2020-11-17 + +# NOTES: +# Folders should be denoted with a trailing / +# /env/ denotes only the root level ./env folder be ignored +# env/ denotes ANY env folder be ignored, even in ./some/deep/folder/env +# Sames goes for files + +# OS Files +.DS_Store +._.DS_Store +._.TemporaryItems +.AppleDouble +Thumbs.db +.nfs* +.swp + +# OS Folders +# + +# IDE Files +# + +# IDE Folders +/.idea/ +/.vscode/ + +# Python Files +/.venv +/.env +/.coverage +*.pyc +*.py[cod] +*.egg +.Python +/.installed.cfg +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt + +# Python Folders +/instance/ +/venv/ +/env/ +/htmlcov/ +/html/ +/sdist/ +/dist/ +/build/ +*.egg-info/ +eggs/ +develop-eggs/ +__pycache__/ +.pytest_cache/ + +# Django Files +# + +# Django Folders +/static/ + +# JavaScript Files +npm-debug.log +yarn-error.log + +# Javascript Folders +/node_modules/ + +# Other Files +/:memory + +# Other Folders +/.vagrant/ diff --git a/snake.py b/snake.py index fc66010..e59f4d9 100644 --- a/snake.py +++ b/snake.py @@ -1,185 +1,319 @@ import curses import random import time +from typing import Any -def main(stdscr, start_length, speed): - # Setup curses - curses.curs_set(0) # Hide cursor - stdscr.nodelay(1) # Non-blocking getch - w = 100 - if speed == 1: w = 250 - if speed == 2: w = 225 - if speed == 3: w = 200 - if speed == 4: w = 175 - if speed == 5: w = 150 - if speed == 6: w = 125 - if speed == 7: w = 100 - if speed == 8: w = 75 - if speed == 9: w = 50 - if speed == 10: w = 25 +class Game: - stdscr.timeout(w) # Wait 100ms for input - # if speed = 50 then timeout = 100 - # if speed = 100 then timeout = 50 - # if speed = 0 then timeout = 1000 + def __init__(self, stdscr): + # Get curses standard screen + self.stdscr = stdscr - # Snake and food characters - head_char = '█' - #head_char = '■' - #head_char = '0' - #head_char_up = '▄' - #head_char_down = '▀' - head_char_up = head_char_down = head_char - body_char = '▓' - #body_char = 'Φ' - #body_char = '■' - #body_char = 'O' - #body_char = '░' - #body_char = '■' - #food_char = '💗' - food_char = '█' - #food_char = '▓' - #food_char = '▒' + # Defaults + self.snake_length = 3 + self.snake_speed = 7 - # Setup colors - curses.start_color() - curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # Snake - curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # Food - curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLACK) # Score/Text - stdscr.bkgd(' ', curses.color_pair(3)) + # 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 = '▒' - # Game constants - h, w = stdscr.getmaxyx() - # Ensure window is large enough - if h < 10 or w < 20: - stdscr.addstr(0, 0, "Terminal window too small!") - stdscr.refresh() - time.sleep(2) - return - # Initial snake: list of [y, x] - # Check if snake fits horizontally - if start_length > w - 2: - stdscr.addstr(h // 2, (w - len(f"Length {start_length} too long for width {w}!")) // 2, f"Length {start_length} too long for width {w}!", curses.color_pair(3)) - stdscr.refresh() - time.sleep(2) - return + def load(self): - # Start the head at x=start_length so the tail ends at x=1 - snake = [[h // 2, start_length - i] for i in range(start_length)] + # 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 - # Initial food - def get_new_food(): while True: - nf = [random.randint(1, h - 2), random.randint(1, w - 2)] - if nf not in snake: - return nf - - food = get_new_food() - - # Initial direction - key = curses.KEY_RIGHT - score = start_len + 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 - hc = head_char + # 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 - while True: - - next_key = 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: - if next_key == curses.KEY_DOWN and key != curses.KEY_UP: - key = next_key - hc = head_char_down - elif next_key == curses.KEY_UP and key != curses.KEY_DOWN: - key = next_key - hc = head_char_up - elif next_key == curses.KEY_LEFT and key != curses.KEY_RIGHT: - key = next_key - hc = head_char - elif next_key == curses.KEY_RIGHT and key != curses.KEY_LEFT: - key = next_key - hc = head_char - elif next_key == ord('q'): # Allow quit + # 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 - # 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: - # Should not happen - new_head = head + snake.insert(0, new_head) - # Check collisions - if (new_head[0] in [0, h - 1] or - new_head[1] in [0, w - 1] or - new_head in snake): - key = stdscr.getch() - break + # Check if snake ate food + if snake[0] == food: + score += 1 + food = get_new_food() + else: + snake.pop() # Remove tail - snake.insert(0, new_head) + # Draw everything + self.clear_screen() + self.stdscr.attron(curses.color_pair(3)) + self.stdscr.border(0) + self.stdscr.attroff(curses.color_pair(3)) - # Check if snake ate food - if snake[0] == food: - score += 1 - food = get_new_food() - else: - snake.pop() # Remove tail + # 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 everything - stdscr.clear() - stdscr.attron(curses.color_pair(3)) - stdscr.border(0) - stdscr.attroff(curses.color_pair(3)) - - # Draw snake - for i, (y, x) in enumerate(snake): - char = hc if i == 0 else body_char - stdscr.addch(y, x, char, curses.color_pair(1)) - - # Draw food - stdscr.addch(food[0], food[1], food_char, curses.color_pair(2)) - - # Draw score - stdscr.addstr(0, 2, f' Length: {score} @ {speed}MPH ({w}x{h}) ', curses.color_pair(3)) - - stdscr.refresh() + # 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() - # Game Over screen - #stdscr.clear() - msg = f"GAME OVER! SCORE: {score}" - stdscr.addstr(h // 2, (w - len(msg)) // 2, msg, curses.color_pair(3)) - stdscr.addstr(h // 2 + 1, (w - 18) // 2, "Press Q to exit", curses.color_pair(3)) - stdscr.nodelay(0) - key = stdscr.getch() - while key != ord('q'): - key = stdscr.getch() - exit('bye') if __name__ == "__main__": - try: - start_len = input("Enter starting snake length (default 3): ") - start_len = int(start_len) if start_len.strip() else 3 - if start_len < 1: - start_len = 3 - except ValueError: - start_len = 3 - - try: - speed = input("Enter snake speed (1 to 10, default 7): ") - speed = int(speed) if speed.strip() else 7 - if speed > 10: speed = 10 - if speed < 1: speed = 1 - except ValueError: - speed = 7 - - curses.wrapper(main, start_len, speed) + game = curses.wrapper(main) -- 2.49.1