#!/usr/bin/python
# -*-coding=utf-8 -*-
#-----------------------------------------------------------
# PySnake v2.2
# Created by: Abner Matheus
# E-mail: abner.math.c@gmail.com
# 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 (abner.math.c@gmail.com)
"""
    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()