import curses
import random
import time
import threading
from collections import deque
import shutil
[docs]
class SnakeGame:
"""
A terminal-based Snake game implemented using the curses library.
Attributes:
width (int): Width of the game area.
height (int): Height of the game area.
snake (deque): Deque representing the snake's body coordinates.
direction (str): Current moving direction of the snake.
next_direction (str): Next direction input from the user.
food (tuple or None): Coordinates of the current food item.
score (int): Player's current score.
game_over (bool): Flag indicating if the game has ended.
running (bool): Flag indicating if the game loop is running.
delay (int): Delay between snake moves in milliseconds.
stdscr (curses.window): The main curses screen.
game_win (curses.window): Window displaying the game area.
test_mode (bool): Whether running in test mode (no curses initialization).
"""
def __init__(self, test_mode=False):
"""
Initialize the Snake game with default settings.
Args:
test_mode (bool): If True, skip curses initialization for testing.
Sets up game dimensions, initial snake state, score, and
initializes the curses display (unless in test mode). Also validates terminal size.
"""
# Game dimensions
self.width = 40
self.height = 20
# Game state
self.snake = deque()
self.direction = "RIGHT"
self.next_direction = "RIGHT"
self.food = None
self.score = 0
self.game_over = False
self.running = True
self.test_mode = test_mode
# Game speed (delay between moves in milliseconds)
self.delay = 150
# Initialize curses and UI elements only if not in test mode
self.stdscr = None
self.game_win = None
if not test_mode:
self.window_terminal_validity()
self.init_curses()
[docs]
def window_terminal_validity(self):
"""
Check if the current terminal size is sufficient for the game.
Validates that the terminal dimensions are large enough to display
the game area plus borders and UI elements. The minimum required
size accounts for the game area plus 4 additional characters for
borders and spacing.
Raises:
Exception: If the terminal is smaller than required dimensions
(height+4 x width+4). Includes current and required
dimensions in the error message.
"""
size = shutil.get_terminal_size(fallback=(80, 24))
terminal_width = size.columns
terminal_height = size.lines
if terminal_height < self.height + 4 or terminal_width < self.width + 4:
raise Exception(
f"Terminal too small! Minimum required: "
f"{self.height+4}x{self.width+4}, "
f"Current: {terminal_height}x{terminal_width}\n"
"Please expand terminal size"
)
[docs]
def init_curses(self):
"""
Initialize the curses display and configure game window.
Sets up:
- Main screen with proper input/output settings
- Input and display options (no echo, non-blocking input)
- Color pairs for snake, food, border, and text elements
- Game window with borders and keypad support
- Non-blocking input for real-time gameplay
Color pairs defined:
1: Green snake on default background
2: Red food on default background
3: Green border on default background
4: White text on default background
5: Yellow game over text on default background
"""
if self.test_mode:
return
self.stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
curses.curs_set(0)
# Enable colors
curses.start_color()
curses.use_default_colors()
# Define color pairs
curses.init_pair(1, curses.COLOR_GREEN, -1) # Snake (green)
curses.init_pair(2, curses.COLOR_RED, -1) # Food (red)
curses.init_pair(3, curses.COLOR_GREEN, -1) # Border (dark green)
curses.init_pair(4, curses.COLOR_WHITE, -1) # Score text (white)
curses.init_pair(5, curses.COLOR_YELLOW, -1) # Game over text (yellow)
# Create game window with border
self.game_win = curses.newwin(self.height + 2, self.width + 2, 2, 2)
self.game_win.keypad(True)
self.game_win.nodelay(True)
# Get screen dimensions for score placement
screen_height, screen_width = self.stdscr.getmaxyx()
[docs]
def draw_border(self):
"""
Draw the dark green border around the game area.
Creates a visual boundary for the playing field using curses
border characters. The border is drawn in green with bold
formatting to make it clearly visible and distinguish the
game area from the surrounding terminal space.
"""
if self.test_mode or not self.game_win:
return
self.game_win.attron(curses.color_pair(3) | curses.A_BOLD)
self.game_win.border()
self.game_win.attroff(curses.color_pair(3) | curses.A_BOLD)
[docs]
def init_snake(self):
"""
Initialize the snake at the center of the screen.
Creates a new snake with 3 segments positioned horizontally
in the center of the game area. The snake starts facing right
with the head at the center and two body segments trailing
to the left. This provides a consistent starting state for
each new game.
"""
center_x = self.width // 2
center_y = self.height // 2
# Snake starts with 3 segments
self.snake = deque([(center_x, center_y), (center_x - 1, center_y), (center_x - 2, center_y)])
[docs]
def spawn_food(self):
"""
Spawn food at a random empty location within the game area.
Continuously generates random coordinates within the playable
area (excluding border positions) until finding a location
that doesn't overlap with any part of the snake's body.
This ensures food is always accessible and visible to the player.
The food coordinates are stored in self.food as a tuple (x, y).
"""
while True:
x = random.randint(1, self.width - 2)
y = random.randint(1, self.height - 2)
if (x, y) not in self.snake:
self.food = (x, y)
break
[docs]
def draw_snake(self):
"""
Draw the green snake on the game area.
Renders each segment of the snake using the block character "█"
in green color with bold formatting. Iterates through all
segments in the snake deque and draws them at their respective
coordinates within the game window.
"""
if self.test_mode or not self.game_win:
return
for segment in self.snake:
x, y = segment
self.game_win.addch(y, x, "█", curses.color_pair(1) | curses.A_BOLD)
[docs]
def draw_food(self):
"""
Draw the red food item on the game area.
Renders the food as a circular bullet character "●" in red
color with bold formatting. Only draws the food if it exists
(self.food is not None), positioning it at the stored
coordinates within the game window.
"""
if self.test_mode or not self.game_win:
return
if self.food:
x, y = self.food
self.game_win.addch(y, x, "●", curses.color_pair(2) | curses.A_BOLD)
[docs]
def draw_score(self):
"""
Draw the current score at the bottom right outside the game border.
Displays the score in the format "Score: X" positioned below
and to the right of the game area. The text is right-aligned
and rendered in white with bold formatting for visibility.
Position calculation accounts for:
- Game window position and dimensions
- Text length for proper alignment
- Border spacing for clean layout
"""
if self.test_mode or not self.stdscr:
return
score_text = f"Score: {self.score}"
# Position: bottom right corner outside the game border
score_y = 2 + self.height + 2 # Below the game window
score_x = 2 + self.width + 2 - len(score_text) # Right aligned
self.stdscr.addstr(score_y, score_x, score_text, curses.color_pair(4) | curses.A_BOLD)
[docs]
def draw_instructions(self):
"""
Draw game control instructions to the right of the game area.
Displays a list of available controls and commands:
- Movement controls (WASD or Arrow Keys)
- Quit command ('q')
- Restart command ('r')
Instructions are positioned to the right of the game border
in white text, providing players with easily accessible
reference for game controls.
"""
if self.test_mode or not self.stdscr:
return
instructions = ["Use WASD or Arrow Keys to move", "Press 'q' to quit", "Press 'r' to restart"]
start_y = 2
for i, instruction in enumerate(instructions):
self.stdscr.addstr(start_y + i, 2 + self.width + 5, instruction, curses.color_pair(4))
[docs]
def move_snake(self):
"""
Move the snake in the current direction and handle game logic.
Performs the core game movement logic:
1. Calculates new head position based on current direction
2. Checks for collisions with walls or snake body
3. Adds new head to snake
4. Handles food consumption (grows snake, increases score, spawns new food)
5. Removes tail segment if no food was eaten
6. Increases game speed slightly when food is consumed
Sets game_over flag to True if collision is detected.
Updates score and respawns food when food is consumed.
Implements speed increase mechanism for progressive difficulty.
"""
head_x, head_y = self.snake[0]
# Calculate new head position
if self.direction == "UP":
new_head = (head_x, head_y - 1)
elif self.direction == "DOWN":
new_head = (head_x, head_y + 1)
elif self.direction == "LEFT":
new_head = (head_x - 1, head_y)
elif self.direction == "RIGHT":
new_head = (head_x + 1, head_y)
# Check for collisions
if self.check_collision(new_head):
self.game_over = True
return
# Add new head
self.snake.appendleft(new_head)
# Check if food is eaten
if new_head == self.food:
self.score += 1
self.spawn_food()
# Increase speed slightly
if self.delay > 50:
self.delay = max(50, self.delay - 2)
else:
# Remove tail if no food eaten
self.snake.pop()
[docs]
def check_collision(self, position):
"""
Check if a given position results in a collision.
Args:
position (tuple): The (x, y) coordinates to check for collision.
Returns:
bool: True if collision detected, False otherwise.
Collision detection includes:
- Wall collision: position is at or beyond game area boundaries
- Self collision: position overlaps with any part of snake body
The method checks boundaries against the playable area (excluding
the border positions) and iterates through the snake deque to
detect self-intersection.
"""
x, y = position
# Check wall collision
if x <= 0 or x >= self.width - 1 or y <= 0 or y >= self.height - 1:
return True
# Check self collision
if position in self.snake:
return True
return False
[docs]
def restart_game(self):
"""
Reset the game to its initial state for a fresh start.
Clears all game state and reinitializes core components:
- Clears snake body
- Resets score to 0
- Sets direction back to RIGHT
- Clears game over flag
- Resets game speed to initial value
- Reinitializes snake position
- Spawns new food
This method is called when the player presses 'R' during
the game over screen, allowing for immediate replay without
restarting the entire program.
"""
self.snake.clear()
self.score = 0
self.direction = "RIGHT"
self.next_direction = "RIGHT"
self.game_over = False
self.delay = 150
self.init_snake()
self.spawn_food()
[docs]
def draw_game_over(self):
"""
Draw the game over screen with final score and restart options.
Displays centered text on the game area including:
- "GAME OVER!" message in yellow with bold formatting
- Final score display in white
- Restart/quit instructions in white
All text is centered both horizontally and vertically within
the game window to create a clear, prominent game over screen
that provides the player with their final score and next steps.
"""
if self.test_mode or not self.game_win:
return
# Calculate center position
center_y = self.height // 2
center_x = self.width // 2
game_over_text = "GAME OVER!"
restart_text = "Press 'r' to restart or 'q' to quit"
final_score_text = f"Final Score: {self.score}"
# Draw game over messages
self.game_win.addstr(
center_y - 1, center_x - len(game_over_text) // 2, game_over_text, curses.color_pair(5) | curses.A_BOLD
)
self.game_win.addstr(center_y, center_x - len(final_score_text) // 2, final_score_text, curses.color_pair(4))
self.game_win.addstr(center_y + 1, center_x - len(restart_text) // 2, restart_text, curses.color_pair(4))
[docs]
def draw_welcome(self):
"""
Draw the welcome screen and wait for player input to start.
Displays the initial game screen with:
- "SNAKE GAME" title in green with bold formatting
- "Press any key to start!" instruction in white
- Game border for visual context
The method temporarily disables non-blocking input to wait
for a key press before starting the game, then re-enables
non-blocking mode for gameplay. All text is centered within
the game window for an attractive welcome presentation.
"""
if self.test_mode or not self.game_win:
return
welcome_text = "SNAKE GAME"
start_text = "Press any key to start!"
center_y = self.height // 2
center_x = self.width // 2
self.draw_border()
self.game_win.addstr(
center_y - 1, center_x - len(welcome_text) // 2, welcome_text, curses.color_pair(1) | curses.A_BOLD
)
self.game_win.addstr(center_y + 1, center_x - len(start_text) // 2, start_text, curses.color_pair(4))
self.game_win.refresh()
# Wait for key press
self.game_win.nodelay(False)
self.game_win.getch()
self.game_win.nodelay(True)
[docs]
def update_display(self):
"""
Update the entire game display with current game state.
Performs a complete refresh of all visual elements:
1. Clears the game area (preserving border)
2. Draws game border
3. Draws snake at current position
4. Draws food at current position
5. Shows game over screen if applicable
6. Updates score display
7. Updates instruction display
8. Refreshes both main screen and game window
This method is called every game loop iteration to ensure
the display accurately reflects the current game state.
The clearing and redrawing approach prevents visual artifacts
and ensures clean animation.
"""
if self.test_mode or not self.game_win:
return
# Clear the game area (not the border)
for y in range(1, self.height - 1):
for x in range(1, self.width - 1):
self.game_win.addch(y, x, " ")
# Draw game elements
self.draw_border()
self.draw_snake()
self.draw_food()
if self.game_over:
self.draw_game_over()
# Update score and instructions
self.stdscr.clear()
self.draw_score()
self.draw_instructions()
# Refresh windows
self.stdscr.refresh()
self.game_win.refresh()
[docs]
def run(self):
"""
Main game execution method that runs the complete game loop.
Orchestrates the entire game flow:
1. Shows welcome screen and waits for start
2. Initializes snake and food
3. Starts input handling thread for real-time controls
4. Runs main game loop until quit/exit:
- Updates snake direction from input
- Moves snake (if game not over)
- Updates display
- Controls game speed with delay
5. Handles cleanup on exit
Exception Handling:
- Catches KeyboardInterrupt (Ctrl+C) for graceful exit.
- Ensures proper curses cleanup in finally block.
The method uses threading for input handling to maintain
responsive controls while managing game timing and display
updates in the main thread.
"""
try:
# Show welcome screen
self.draw_welcome()
# Initialize game
self.init_snake()
self.spawn_food()
# Start input handling thread
input_thread = threading.Thread(target=self.handle_input, daemon=True)
input_thread.start()
# Main game loop
while self.running:
if not self.game_over:
# Update direction
self.direction = self.next_direction
# Move snake
self.move_snake()
# Update display
self.update_display()
# Game speed control
time.sleep(self.delay / 1000.0)
except KeyboardInterrupt:
self.running = False
finally:
self.cleanup()
[docs]
def cleanup(self):
"""
Clean up curses environment and restore terminal state.
Properly shuts down the curses interface by:
- Disabling cbreak mode (restoring line buffering)
- Re-enabling echo for normal terminal input
- Restoring cursor visibility
- Ending the curses session
This method is essential for leaving the terminal in a
usable state after the game exits. Called automatically
in the finally block of the run() method to ensure
cleanup occurs even if the game exits unexpectedly.
"""
if self.test_mode or not self.stdscr:
return
curses.nocbreak()
curses.echo()
curses.curs_set(1)
curses.endwin()