Snake
in Posts on Python
Messing around making a snake game when I couldn’t sleep
I couldn’t sleep yesterday, so I decided to have a crack at making my own version of the classic game Snake. Like many children of the 90s, I spent a lot of time playing this on Nokias. There’s a gist with the same content here
Similar to (I think it was) Snake II, I’ve added “super” foods, and some teleportation mechanics.
from unicurses import *
from random import randint
from time import sleep
def main():
# Initialize screen
stdscr = initscr()
curs_set(0) # Hide the cursor
noecho() # Turn off automatic echoing of input
nodelay(stdscr, True) # Non-blocking input
keypad(stdscr, True) # Enable arrow keys
timeout(100) # Default refresh timeout, will be adjusted per axis
# Initialize color support
start_color()
init_pair(1, COLOR_RED, COLOR_BLACK) # Red superfood
init_pair(2, COLOR_YELLOW, COLOR_BLACK) # Yellow superfood
init_pair(3, COLOR_CYAN, COLOR_BLACK) # Cyan superfood
# Show splash screen
attron(COLOR_PAIR(3))
display_splashscreen()
attroff(COLOR_PAIR(3))
# Get screen dimensions
sh, sw = getmaxyx(stdscr) # Screen height and width
# Initialize snake and food
snake = [[sh // 2, sw // 2 + i] for i in range(3)] # Snake body
food = [randint(1, sh - 2), randint(1, sw - 2)] # Food position
mvaddch(food[0], food[1], ord('O')) # Display food
# Initialize super food variables
super_food = None
super_food_time = 0
# Initial direction
key = KEY_LEFT
last_key = key
while True:
# Get updated screen dimensions
sh, sw = getmaxyx(stdscr) # Update screen height and width
# Draw walls (hashes for the edges)
for i in range(1, sw-1):
mvaddch(0, i, ord('#')) # Top wall
mvaddch(sh-1, i, ord('#')) # Bottom wall
for i in range(1, sh-1):
mvaddch(i, 0, ord('#')) # Left wall
mvaddch(i, sw-1, ord('#')) # Right wall
# Draw portals (5 chars wide and 3 chars tall for the left/right walls)
mvaddstr(0, sw // 2 - 2, " ") # Top wall portal
mvaddstr(sh - 1, sw // 2 - 2, " ") # Bottom wall portal
for i in range(1, 4):
mvaddch(sh // 2 - 2 + i, 0, ord(' ')) # Left wall portal (3 tall)
mvaddch(sh // 2 - 2 + i, sw - 1, ord(' ')) # Right wall portal (3 tall)
# Get user input
next_key = getch()
key = key if next_key == -1 else next_key
# Prevent "double-back" moves/deaths
if last_key == KEY_LEFT and key == KEY_RIGHT:
key = KEY_LEFT
elif last_key == KEY_RIGHT and key == KEY_LEFT:
key = KEY_RIGHT
elif last_key == KEY_UP and key == KEY_DOWN:
key = KEY_UP
elif last_key == KEY_DOWN and key == KEY_UP:
key = KEY_DOWN
last_key = key
# Compute new head position based on direction
head = snake[0]
if key == KEY_DOWN:
new_head = [head[0] + 1, head[1]]
elif key == KEY_UP:
new_head = [head[0] - 1, head[1]]
elif key == KEY_LEFT:
new_head = [head[0], head[1] - 1]
elif key == KEY_RIGHT:
new_head = [head[0], head[1] + 1]
else:
continue # Ignore invalid keys
# Check for self-collision (before adding the new head)
if new_head in snake:
break # End game if snake crashes into itself
# Check for portal mechanics (teleporting when hitting a portal)
# Top and bottom wall portals
if new_head[0] == 0 and sw // 2 - 2 <= new_head[1] <= sw // 2 + 2: # Top wall portal
new_head = [sh - 2, new_head[1]] # Teleport to bottom
elif new_head[0] == sh - 1 and sw // 2 - 2 <= new_head[1] <= sw // 2 + 2: # Bottom wall portal
new_head = [1, new_head[1]] # Teleport to top
# Left and right wall portals
elif new_head[1] == 0 and sh // 2 - 1 <= new_head[0] <= sh // 2 + 1: # Left wall portal
new_head = [new_head[0], sw - 2] # Teleport to right
elif new_head[1] == sw - 1 and sh // 2 - 1 <= new_head[0] <= sh // 2 + 1: # Right wall portal
new_head = [new_head[0], 1] # Teleport to left
# Check for collision with regular walls (after portal check)
if (
new_head[0] in [0, sh-1] or
new_head[1] in [0, sw-1] and
not (new_head[0] == sh // 2 and sw // 2 - 2 <= new_head[1] <= sw // 2 + 2) # Exclude portal zones
):
break # End game on collision with a wall (not a portal)
# Add the new head to the snake
snake.insert(0, new_head)
# Check if food is eaten
if new_head == food:
food = [randint(1, sh - 2), randint(1, sw - 2)]
mvaddch(food[0], food[1], ord('O'))
else:
# Remove tail
tail = snake.pop()
mvaddch(tail[0], tail[1], ' ')
# Check if super food is eaten
if super_food and new_head == super_food:
# Extend snake by 10 units
for _ in range(10):
snake.append(snake[-1]) # Add new segments to snake body
super_food = None # Remove super food
# Display the snake
for segment in snake:
mvaddch(segment[0], segment[1], ord('X'))
mvaddch(food[0], food[1], ord('O')) # Ensure food is always drawn
# Display the super food (if any)
if super_food:
# Flash the super food in different colors
if super_food_time % 2 == 0:
attron(COLOR_PAIR(1))
mvaddch(super_food[0], super_food[1], ord('S'))
attroff(COLOR_PAIR(1))
else:
attron(COLOR_PAIR(2))
mvaddch(super_food[0], super_food[1], ord('@'))
attroff(COLOR_PAIR(2))
super_food_time += 1
if super_food_time > 100: # Super food lasts for 10 seconds (20 timeouts)
mvaddch(super_food[0], super_food[1], ' ')
super_food = None
super_food_time = 0
refresh()
snake_length = len(snake)
if snake_length > 400:
snake_length = 400
x_scale = (sw / (sw + sh)) * (1 - (snake_length / 400))
y_scale = (sh / (sw + sh)) * (1 - (snake_length / 400))
# Adjust the refresh timeout based on direction
if key == KEY_LEFT or key == KEY_RIGHT:
timeout(int(100 * y_scale))
elif key == KEY_UP or key == KEY_DOWN:
timeout(int(100 * x_scale))
# Chance to spawn super food, but only if none exists already
if not super_food and randint(1, 100) <= 5: # 5% chance per frame
super_food = [randint(1, sh - 2), randint(1, sw - 2)]
# Ensure the super food does not spawn on the snake or regular food
while super_food in snake or super_food == food:
super_food = [randint(1, sh - 2), randint(1, sw - 2)]
# Display "Game Over" message
display_game_over(stdscr, sh, sw, len(snake))
# End the game
endwin()
def display_game_over(stdscr, sh, sw, score=-1):
"""Displays a centered 'Game Over' message."""
clear() # Clear the screen
box_width = 20
box_height = 6
box_start_y = (sh // 2) - (box_height // 2)
box_start_x = (sw // 2) - (box_width // 2)
# Draw box
for i in range(box_height):
mvaddch(box_start_y + i, box_start_x, '|')
mvaddch(box_start_y + i, box_start_x + box_width - 1, '|')
for j in range(box_width):
mvaddch(box_start_y, box_start_x + j, '-')
mvaddch(box_start_y + box_height - 1, box_start_x + j, '-')
# Display text
message = " GAME OVER "
mvaddstr(box_start_y + 2, box_start_x + (box_width // 2 - len(message) // 2), message)
message = " SCORE : " + str(score)
mvaddstr(box_start_y + 3, box_start_x + (box_width // 2 - len(message) // 2), message)
refresh()
sleep(5)
clear() # Clear the screen after game over
def display_splashscreen():
"""Displays a splash screen with ASCII art and the game title."""
splash_text = """
$$$$$$\ $$$$$$\ $$\ $$\ $$\ $$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$$$\
$$ __$$\ $$ __$$\ $$$\ $$$ |$$$\ $$$ |\$$\ $$ | $$ __$$\ $$$\ $$ |$$ __$$\ $$ | $$ |$$ _____|
$$ / \__|$$ / $$ |$$$$\ $$$$ |$$$$\ $$$$ | \$$\ $$ / $$ / \__|$$$$\ $$ |$$ / $$ |$$ |$$ / $$ |
\$$$$$$\ $$$$$$$$ |$$\$$\$$ $$ |$$\$$\$$ $$ | \$$$$ / \$$$$$$\ $$ $$\$$ |$$$$$$$$ |$$$$$ / $$$$$\
\____$$\ $$ __$$ |$$ \$$$ $$ |$$ \$$$ $$ | \$$ / \____$$\ $$ \$$$$ |$$ __$$ |$$ $$< $$ __|
$$\ $$ |$$ | $$ |$$ |\$ /$$ |$$ |\$ /$$ | $$ | $$\ $$ |$$ |\$$$ |$$ | $$ |$$ |\$$\ $$ |
\$$$$$$ |$$ | $$ |$$ | \_/ $$ |$$ | \_/ $$ | $$ | \$$$$$$ |$$ | \$$ |$$ | $$ |$$ | \$$\ $$$$$$$$\
\______/ \__| \__|\__| \__|\__| \__| \__| \______/ \__| \__|\__| \__|\__| \__|\________|
"""
snake_art = r"""
/^\/^\
_|__| O|
\/ /~ \_/ \
\____|__________/ \
\_______ \
`\ \ \
| | \
/ / \
/ / \\
/ / \ \
/ / \ \
/ / _----_ \ \
/ / _-~ ~-_ | |
( ( _-~ _--_ ~-_ _/ |
\ ~-____-~ _-~ ~-_ ~-_-~ /
~-_ _-~ ~-_ _-~
~--______-~ ~-___-~
"""
# Combine splash text and snake art
splash_screen = splash_text + "\n\n" + snake_art
clear() # Clear the screen before displaying splash screen
mvaddstr(0, 0, splash_screen) # Print the splash screen at the top left
refresh() # Refresh to display the content
sleep(2) # Wait for 2 seconds before moving on
clear() # Clear the screen after splash screen
if __name__ == "__main__":
main()
endwin()