Last active
August 29, 2015 14:06
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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