Skip to content

Instantly share code, notes, and snippets.

@abner-math
Last active August 29, 2015 14:06
Show Gist options
  • Save abner-math/300f93a9820c52e6a243 to your computer and use it in GitHub Desktop.
Save abner-math/300f93a9820c52e6a243 to your computer and use it in GitHub Desktop.
A clone of the classic game Snake developed by me in my spare time, using Python 2.7
#!/usr/bin/python
# -*-coding=utf-8 -*-
#-----------------------------------------------------------
# PySnake v2.2
# Created by: Abner Matheus
# E-mail: [email protected]
# Github: http://github.com/picoledelimao
#-----------------------------------------------------------
import os, platform, time, sys, select
from random import randint
"""
Enumerate the directions that a snake can take
"""
class Direction:
forward = 1
backward = 2
upward = 3
downward = 4
"""
Control the movement and position of a snake
"""
class Snake:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
"""
Turn the snake of direction
"""
def turn(self, direction):
self.direction = direction
"""
Move the snake toward its direction
Return false if the movement crossed the wall
"""
def move(self):
if self.direction == Direction.forward:
self.x += 1
if self.x >= self.width:
self.x = 0
return False
elif self.direction == Direction.backward:
self.x -= 1
if self.x < 0:
self.x = self.width - 1
return False
elif self.direction == Direction.upward:
self.y -= 1
if self.y < 0:
self.y = self.height - 1
return False
elif self.direction == Direction.downward:
self.y += 1
if self.y >= self.height:
self.y = 0
return False
return True
"""
Change snake's direction and move it at the same time
"""
def turn_and_move(self, direction):
self.turn(direction)
return self.move()
"""
Keep information of a terrain object (fruit or obstacles)
"""
class TerrainObject:
"""
Verify if given position if empty
"""
def __is_empty(self, x, y, context):
try:
for snake in context.snakes:
if snake.x == x and snake.y == y: return False
for obstacle in context.obstacles:
if obstacle.x == x and obstacle.y == y: return False
if context.fruit.x == x and context.fruit.y == y: return False
except AttributeError: pass
return True
"""
Build a object in a random place of the terrain
"""
def __init__(self, context):
while True:
x = randint(0, context.width - 1)
y = randint(0, context.height - 1)
if self.__is_empty(x, y, context): break
self.x = x
self.y = y
"""
Verify if the snake's head hit that object
"""
def hit(self, snake):
return self.x == snake.x and self.y == snake.y
"""
Keep information of the terrain
"""
class Terrain:
__WHITE_SPACE = ' '
__SNAKE_BODY = '0'
__FRUIT = '*'
__OBSTACLE = "~"
__HOR_BOUND = "-"
__VER_BOUND = "|"
"""
Create a terrain of given width and height
"""
def __init__(self, width, height):
self.width = width
self.height = height
"""
Update terrain information using passed objects
"""
def __update(self, snakes, fruit, obstacles):
self.matrix = []
for i in range(self.height):
self.matrix.append([])
for j in range(self.width):
self.matrix[i].append(Terrain.__WHITE_SPACE)
self.matrix[fruit.y][fruit.x] = Terrain.__FRUIT
for snake in snakes:
self.matrix[snake.y][snake.x] = Terrain.__SNAKE_BODY
for obstacle in obstacles:
self.matrix[obstacle.y][obstacle.x] = Terrain.__OBSTACLE
"""
Return a string that shows a visual representation of the terrain
"""
def show(self, snakes, fruit, obstacles):
self.__update(snakes, fruit, obstacles)
horizontal_bound = "." + Terrain.__HOR_BOUND * (self.width) + "." + "\n"
result = horizontal_bound
for line in self.matrix:
result += Terrain.__VER_BOUND + "".join(line) + Terrain.__VER_BOUND + "\n"
result += horizontal_bound
return result
"""
Responsible to show elements in the screen
"""
class View:
LOGO = """
██████╗ ██╗ ██╗███████╗███╗ ██╗ █████╗ ██╗ ██╗███████╗
██╔══██╗╚██╗ ██╔╝██╔════╝████╗ ██║██╔══██╗██║ ██╔╝██╔════╝
██████╔╝ ╚████╔╝ ███████╗██╔██╗ ██║███████║█████╔╝ █████╗
██╔═══╝ ╚██╔╝ ╚════██║██║╚██╗██║██╔══██║██╔═██╗ ██╔══╝
██║ ██║ ███████║██║ ╚████║██║ ██║██║ ██╗███████╗
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
"""
INITIAL = LOGO + """
GAME CONTROLS:
=============================================================
PRESS 'N' TO START A NEW GAME.
'A', 'S', 'D' OR 'W' KEYS TO MOVE THE SNAKE.
ESC TO EXIT GAME.
=============================================================
CREATED BY:
-------------------------------------------------------------
ABNER MATHEUS ([email protected])
"""
DIFFICULTY = LOGO + """
CHOOSE A DIFFICULTY BELOW:
=============================================================
1. VERY EASY
2. MEDIUM
3. HARD
=============================================================
OBJECTS:
-------------------------------------------------------------
* Fruit
~ Obstacle
"""
GAME_OVER = """
▄████ ▄▄▄ ███▄ ▄███▓▓█████ ▒█████ ██▒ █▓▓█████ ██▀███
██▒ ▀█▒▒████▄ ▓██▒▀█▀ ██▒▓█ ▀ ▒██▒ ██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
▒██░▄▄▄░▒██ ▀█▄ ▓██ ▓██░▒███ ▒██░ ██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
░▓█ ██▓░██▄▄▄▄██ ▒██ ▒██ ▒▓█ ▄ ▒██ ██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
░▒▓███▀▒ ▓█ ▓██▒▒██▒ ░██▒░▒████▒ ░ ████▓▒░ ▒▀█░ ░▒████▒░██▓ ▒██▒
░▒ ▒ ▒▒ ▓▒█░░ ▒░ ░ ░░░ ▒░ ░ ░ ▒░▒░▒░ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ░ ▒ ▒▒ ░░ ░ ░ ░ ░ ░ ░ ▒ ▒░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
PRESS 'N' TO START A NEW GAME.
"""
def __init__(self, context):
self.context = context
self.terrain = Terrain(self.context.width, self.context.height)
"""
Render terrain and game information in the screen
"""
def render_context(self, context):
info = "LIVES: %d SCORE: %d" % (self.context.lives, self.context.score) + "\n"
terrain = self.terrain.show(self.context.snakes, self.context.fruit, self.context.obstacles)
View.render(info + terrain)
""""
Clear the screen (platform dependent)
"""
@staticmethod
def __clear_screen():
if platform.system() == "Windows": os.system("cls")
else: os.system("clear")
"""
Show a message in the screen
"""
@staticmethod
def render(message):
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
View.__clear_screen()
print(message.decode('utf-8'))
"""
Stores the actual state of the game (interface)
"""
class GameState:
def loop(self, controller):
raise NotImplementedError()
def new_game(self):
raise NotImplementedError()
def set_difficulty(self, difficulty):
raise NotImplementedError()
def set_direction(self, direction):
raise NotImplementedError()
"""
Initial state of the game
"""
class StateInitial(GameState):
def __init__(self, context):
self.context = context
def loop(self, controller):
View.render(View.INITIAL)
def new_game(self):
self.context.state = StatePickDifficulty(self.context)
def set_difficulty(self, difficulty): pass
def set_direction(self, direction): pass
"""
Pick difficulty screen
"""
class StatePickDifficulty(GameState):
def __init__(self, context):
self.context = context
"""
Main loop of the game
"""
def loop(self, controller):
View.render(View.DIFFICULTY)
"""
Start a new game
"""
def new_game(self):
self.context.state = StateInitial(self.context)
"""
Set game difficulty
"""
def set_difficulty(self, difficulty):
self.context.difficulty = difficulty
self.context.state = StatePlaying(self.context)
"""
Change snake's direction
"""
def set_direction(self, direction): pass
"""
Here is where the game happens itself
"""
class StatePlaying(GameState):
def __init__(self, context):
self.context = context
self.width = self.context.width
self.height = self.context.height
self.lives = self.context.lives
self.score = 0
self.view = View(self)
self.snakes = [Snake(self.width / 2, self.height / 2, self.width, self.height)]
self.fruit = TerrainObject(self)
self.direction = Direction.forward
self.direction_queue = []
self.snakes_queue = []
self.obstacles = []
number_of_obstacles = randint((context.difficulty - 1) * 2, (self.context.difficulty - 1) * 3)
for i in range(number_of_obstacles):
self.obstacles.append(TerrainObject(self))
"""
Stores snakes' movement in a queue
"""
def __queue_movement(self):
for i in range(1, len(self.snakes)):
self.direction_queue[i-1].append(self.snakes[i-1].direction)
"""
Update the movement queue
"""
def __dequeue_movement(self):
for i in range(1, len(self.snakes)):
self.direction_queue[i-1].pop(0)
"""
Check if snake's head hit some obstacle (including itself)
"""
def __hit_obstacle(self):
for i in range(1, len(self.snakes)):
if self.snakes[0].x == self.snakes[i].x and self.snakes[0].y == self.snakes[i].y:
return True
for obstacle in self.obstacles:
if self.snakes[0].x == obstacle.x and self.snakes[0].y == obstacle.y:
return True
return False
"""
Move all the snake parts towards its direction
"""
def __move(self):
for i in range(1, len(self.snakes)):
self.snakes[i].turn_and_move(self.direction_queue[i-1][0])
success = self.snakes[0].turn_and_move(self.direction)
if self.__hit_obstacle():
self.lives = 0
return False
return success
"""
Makes the snake grow
"""
def __queue_growth(self):
x = self.snakes[0].x
y = self.snakes[0].y
self.snakes_queue.append(Snake(x, y, self.width, self.height))
"""
Check if snake left fruit position (so its new part can be appended)
"""
def __is_free(self, queued_snake):
for existing_snake in self.snakes:
if existing_snake.x == queued_snake.x and existing_snake.y == queued_snake.y:
return False
return True
"""
Append a snake's part that was in queue
"""
def __dequeue_growth(self):
for i in range(len(self.snakes_queue)-1,-1,-1):
if self.__is_free(self.snakes_queue[i]):
self.snakes.append(self.snakes_queue[i])
self.snakes_queue.pop(i)
self.direction_queue.append([])
def loop(self, controller):
if controller.speed > 40:
controller.speed -= 1
if self.fruit.hit(self.snakes[0]):
self.fruit = TerrainObject(self)
self.score += 1
self.__queue_growth()
self.__queue_movement()
if not self.__move():
self.lives -= 1
if self.lives < 0:
self.context.state = StateGameOver(self.context)
controller.speed = 300
return
self.__dequeue_movement()
self.__dequeue_growth()
self.view.render_context(self)
def new_game(self):
self.context.state = StateInitial(self.context)
def set_difficulty(self, difficulty): pass
def set_direction(self, direction):
self.direction = direction
"""
Game over screen
"""
class StateGameOver(GameState):
def __init__(self, context):
self.context = context
def loop(self, controller):
View.render(View.GAME_OVER)
def new_game(self):
self.context.state = StatePickDifficulty(self.context)
def set_difficulty(self, difficulty): pass
def set_direction(self, direction): pass
class Game:
def __init__(self, width, height, lives):
self.width = width
self.height = height
self.lives = lives
self.state = StateInitial(self)
def loop(self, controller):
self.state.loop(controller)
def new_game(self):
self.state.new_game()
def set_difficulty(self, difficulty):
self.state.set_difficulty(difficulty)
def set_direction(self, direction):
self.state.set_direction(direction)
#-------------------------------
# IO MANAGER
#--------------------------------
def controller_windows():
import Tkinter
class Controller:
def __init__(self):
self.game = Game(30, 15, 3)
self.speed = 300
self.start_game()
def press_key(self, event):
key = event.keysym.lower()
if key == "escape": #ESC
return False
elif key == "n": #Enter
self.game.new_game()
elif key == "1" or key == "2" or key == "3":
self.game.set_difficulty(int(key))
elif key == "d": #Right arrow
self.game.set_direction(Direction.forward)
elif key == "a": #Left arrow
self.game.set_direction(Direction.backward)
elif key == "w": #Up arrow
self.game.set_direction(Direction.upward)
elif key == "s": #Down arrow
self.game.set_direction(Direction.downward)
return True
def loop(self):
self.game.loop(self)
self.console.after(self.speed, self.loop)
def start_game(self):
self.console = Tkinter.Tk()
self.console.bind_all('<Key>', self.press_key)
self.console.withdraw()
try:
self.console.after(self.speed, self.loop)
self.console.mainloop()
except KeyboardInterrupt: pass
Controller()
def controller_unix():
import termios, tty, thread
class NonBlockingConsole(object):
def __enter__(self):
self.old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
return self
def __exit__(self, type, value, traceback):
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
def get_data(self):
if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
return sys.stdin.read(1)
return False
class Controller:
def __init__(self):
self.game = Game(30, 15, 3)
self.speed = 300
self.start_game()
def press_key(self, nbc):
key = str(nbc.get_data())
if key == '\x1b': #ESC
return False
elif key == 'n': #Enter
self.game.new_game()
elif key == '1' or key == '2' or key == '3':
self.game.set_difficulty(int(key))
elif key == 'd': #Right arrow
self.game.set_direction(Direction.forward)
elif key == 'a': #Left arrow
self.game.set_direction(Direction.backward)
elif key == 'w': #Up arrow
self.game.set_direction(Direction.upward)
elif key == 's': #Down arrow
self.game.set_direction(Direction.downward)
return True
def loop(self, threadName):
while self.running:
time.sleep(self.speed/1000.0)
self.game.loop(self)
def start_game(self):
self.running = True
thread.start_new_thread(self.loop, ("Thread-1",))
try:
with NonBlockingConsole() as nbc:
while self.press_key(nbc): pass
except KeyboardInterrupt: pass
self.running = False
Controller()
if __name__ == '__main__':
if platform.system() == "Windows":
controller_windows()
else:
controller_unix()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment