#!/usr/bin/ruby

# Author: Daniel "Trizen" Șuteu
# License: GPLv3
# Date: 25 June 2015
# Website: https://github.com/trizen

# The snake game. (with colors + Unicode)

const readkey = frequire('Term::ReadKey');
const ansi    = frequire('Term::ANSIColor');

enum(
    VOID
    HEAD
    BODY
    TAIL
    FOOD
)

define (
    LEFT  = [+0, -1]
    RIGHT = [+0, +1]
    UP    = [-1, +0]
    DOWN  = [+1, +0]
)

const (
    BG_COLOR    = "on_black"
    FOOD_COLOR  = ("red"        + " " + BG_COLOR)
    SNAKE_COLOR = ("bold green" + " " + BG_COLOR)

    U_HEAD = ansi.colored('▲', SNAKE_COLOR)
    D_HEAD = ansi.colored('▼', SNAKE_COLOR)
    L_HEAD = ansi.colored('◀', SNAKE_COLOR)
    R_HEAD = ansi.colored('▶', SNAKE_COLOR)

    U_BODY = ansi.colored('╹', SNAKE_COLOR)
    D_BODY = ansi.colored('╻', SNAKE_COLOR)
    L_BODY = ansi.colored('╴', SNAKE_COLOR)
    R_BODY = ansi.colored('╶', SNAKE_COLOR)

    U_TAIL = ansi.colored('╽', SNAKE_COLOR)
    D_TAIL = ansi.colored('╿', SNAKE_COLOR)
    L_TAIL = ansi.colored('╼', SNAKE_COLOR)
    R_TAIL = ansi.colored('╾', SNAKE_COLOR)

    A_VOID = ansi.colored(' ', BG_COLOR)
    A_FOOD = ansi.colored('❇', FOOD_COLOR)
)

var sleep    = 0.02;   # sleep duration between updates
var food_num = 10;     # number of initial food sources

var w = Number(`tput cols`  || 80)
var h = Number(`tput lines` || 24)
var r = "\033[H";

var dir = LEFT;
var grid = h.of { w.of { Array.new(VOID) } };

var head_pos = [h>>1, w>>1];
var tail_pos = [head_pos[0], head_pos[1]+1];

grid[head_pos[0]][head_pos[1]] = [HEAD, dir];    # head
grid[tail_pos[0]][tail_pos[1]] = [TAIL, dir];    # tail

func make_food {
    var (food_x, food_y);

    do {
        food_x = w.rand.int;
        food_y = h.rand.int;
    } while (grid[food_y][food_x][0] != VOID);

    grid[food_y][food_x][0] = FOOD;
}

{ make_food() } * food_num;

func display {
    static i = 0;
    static s = [UP, DOWN, LEFT, RIGHT];

    print(r, grid.map { |row|
        row.map { |cell|
            if (cell[0] != VOID) {
                i = s.index(cell[1])
            }
            given (cell[0]) {
                when (VOID) { A_VOID }
                when (FOOD) { A_FOOD }
                when (BODY) { [U_BODY, D_BODY, L_BODY, R_BODY][i] }
                when (HEAD) { [U_HEAD, D_HEAD, L_HEAD, R_HEAD][i] }
                when (TAIL) { [U_TAIL, D_TAIL, L_TAIL, R_TAIL][i] }
            }

          }.join('')
        }.join("\n")
    );
}

func move {
    var grew = false;

    # Move the head
    var (y, x) = head_pos...;

    var new_y = (y+dir[0] % h);
    var new_x = (x+dir[1] % w);

    var cell = grid[new_y][new_x];

    given (cell[0]) {
        when (BODY) { die "\nYou just bit your own body!\n" }
        when (TAIL) { die "\nYou just bit your own tail!\n" }
        when (FOOD) { grew = true; make_food()              }
    }

    # Create a new head
    grid[new_y][new_x] = [HEAD, dir];

    # Replace the current head with body
    grid[y][x] = [BODY, dir];

    # Update the head position
    head_pos = [new_y, new_x];

    # Move the tail
    if (!grew) {
        var (y, x) = tail_pos...;

        var pos   = grid[y][x][1];
        var new_y = (y+pos[0] % h);
        var new_x = (x+pos[1] % w);

        grid[y][x][0]         = VOID;    # erase the current tail
        grid[new_y][new_x][0] = TAIL;    # create a new tail

        tail_pos = [new_y, new_x];
    }
}

readkey.ReadMode(3);
STDOUT.autoflush(true);

loop {
    var key;
    while (!defined(key = readkey.ReadLine(-1))) {
        move();
        display();
        Sys.sleep(sleep);
    }

    given (key) {
        when ("\e[A") { if (dir != DOWN ) { dir = UP    } }
        when ("\e[B") { if (dir != UP   ) { dir = DOWN  } }
        when ("\e[C") { if (dir != LEFT ) { dir = RIGHT } }
        when ("\e[D") { if (dir != RIGHT) { dir = LEFT  } }
    }
}