Snake

class SnakeGame(w, h) {
    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],
    )

    define BG_COLOR    = "on_black"
    define FOOD_COLOR  = ("red"        + " " + BG_COLOR)
    define SNAKE_COLOR = ("bold green" + " " + BG_COLOR)
    define SLEEP_SEC   = 0.02

    const (
        A_VOID  = ansi.colored(' ', BG_COLOR),
        A_FOOD  = ansi.colored('❇', FOOD_COLOR),
        A_BLOCK = ansi.colored('■', SNAKE_COLOR),
    )

    has dir = LEFT
    has grid = [[]]
    has head_pos = [0, 0]
    has tail_pos = [0, 0]

    method init {
        grid = h.of { w.of { [VOID] } }

        head_pos = [h//2, w//2]
        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

        self.make_food()
    }

    method 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
    }

    method display {
        print("\033[H", grid.map { |row|
            row.map { |cell|
                given (cell[0]) {
                    when (VOID) { A_VOID }
                    when (FOOD) { A_FOOD }
                    default     { A_BLOCK }
                }
              }.join('')
            }.join("\n")
        )
    }

    method 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; self.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]
        }
    }

    method play {
        STDOUT.autoflush(true)
        readkey.ReadMode(3)

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

                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  } }
                }
            }
        }
        catch {
            readkey.ReadMode(0)
        }
    }
}

var w = `tput cols`.to_i
var h = `tput lines`.to_i

SnakeGame(w || 80, h || 24).play