Current section
Files
Jump to
Current section
Files
src/examples/snake.gleam
import etch/command
import etch/event.{Char, DownArrow, Key, LeftArrow, RightArrow, UpArrow}
import gleam/dict.{type Dict}
import gleam/erlang/process
import gleam/int
import gleam/list
import gleam/option.{None, Some}
import gleam/string
import gleam/string_tree as stree
import etch/stdout
import etch/style
import etch/terminal
@external(erlang, "erlang", "halt")
fn halt(n: Int) -> Nil
type Direction {
Up
Down
Left
Right
}
type State {
State(
grid: Dict(Int, Int),
snake: List(Int),
rows: Int,
columns: Int,
direction: Direction,
score: Int,
)
}
pub fn main() {
stdout.execute([
command.EnterRaw,
command.EnterAlternateScreen,
command.Clear(terminal.All),
command.HideCursor,
command.DisableLineWrap,
])
let #(columns, rows) = terminal.window_size()
let #(columns, rows) = #(columns - 2, rows - 2)
let grid = make_grid(columns, rows)
let snake_center = case int.is_even(rows) {
True -> rows * columns / 2 + columns / 2
False -> rows * columns / 2
}
let state =
State(
grid,
[snake_center, snake_center - 1, snake_center - 2],
rows,
columns,
Right,
0,
)
let state = spawn_fruit(state)
event.init_event_server()
loop(state)
}
fn make_grid(columns: Int, rows: Int) -> Dict(Int, Int) {
let x =
list.range(0, columns * rows)
|> list.zip(list.repeat(0, columns * rows))
let d = dict.from_list(x)
case int.is_even(rows) {
True -> dict.insert(d, { rows * columns / 2 } + columns / 2, 1)
False -> dict.insert(d, rows * columns / 2, 1)
}
}
fn loop(state: State) {
process.sleep(200)
let state = handle_input(state)
let state = update_state(state)
draw(state)
loop(state)
}
fn update_state(state: State) -> State {
case state.direction {
Up -> move_up(state)
Down -> move_down(state)
Left -> move_left(state)
Right -> move_right(state)
}
}
fn spawn_fruit(state: State) -> State {
let f = int.random(state.rows * state.columns)
case dict.get(state.grid, f) {
Ok(1) -> spawn_fruit(state)
Ok(0) -> {
let grid = dict.insert(state.grid, f, 2)
State(..state, grid: grid)
}
_ -> panic as "Unreachable"
}
}
fn handle_input(state: State) -> State {
case event.poll(1), state.direction {
Some(Ok(Key(e))), Down if e.code == Char("w") || e.code == UpArrow -> state
Some(Ok(Key(e))), _ if e.code == Char("w") || e.code == UpArrow ->
State(..state, direction: Up)
Some(Ok(Key(e))), Right if e.code == Char("a") || e.code == LeftArrow ->
state
Some(Ok(Key(e))), _ if e.code == Char("a") || e.code == LeftArrow ->
State(..state, direction: Left)
Some(Ok(Key(e))), Up if e.code == Char("s") || e.code == DownArrow -> state
Some(Ok(Key(e))), _ if e.code == Char("s") || e.code == DownArrow ->
State(..state, direction: Down)
Some(Ok(Key(e))), Left if e.code == Char("d") || e.code == RightArrow ->
state
Some(Ok(Key(e))), _ if e.code == Char("d") || e.code == RightArrow ->
State(..state, direction: Right)
Some(_), _ -> state
None, _ -> state
}
}
fn move_right(state: State) -> State {
let assert Ok(head) = list.first(state.snake)
let new_head = case head {
n if n % state.columns == state.columns - 1 -> {
lose(state)
0
}
n -> n + 1
}
handle_new_head(state, new_head)
}
fn move_down(state: State) -> State {
let assert Ok(head) = list.first(state.snake)
let new_head = case head + state.columns {
n if n > state.columns * state.rows -> {
lose(state)
0
}
n -> n
}
handle_new_head(state, new_head)
}
fn move_left(state: State) -> State {
let assert Ok(head) = list.first(state.snake)
let new_head = case head {
n if n % state.columns == 0 -> {
lose(state)
0
}
n -> n - 1
}
handle_new_head(state, new_head)
}
fn move_up(state: State) -> State {
let assert Ok(head) = list.first(state.snake)
let new_head = case head - state.columns {
n if n < 0 -> {
lose(state)
0
}
n -> n
}
handle_new_head(state, new_head)
}
fn remove_last_snake_block(state: State, new_head: Int) -> State {
let snake = [new_head, ..state.snake] |> list.reverse()
let #(last, snake) = case snake {
[last, ..rest] -> #(last, rest)
_ -> panic as "Unreachable"
}
let grid = dict.insert(state.grid, last, 0)
let grid = dict.insert(grid, new_head, 1)
let snake = list.reverse(snake)
State(..state, snake: snake, grid: grid)
}
fn handle_new_head(state: State, new_head: Int) -> State {
case dict.get(state.grid, new_head) {
Ok(1) -> {
lose(state)
state
}
Ok(2) -> {
let state = State(..state, score: state.score + 1)
let _ = case state.score + 3 == state.rows * state.columns {
True -> win(state)
False -> Nil
}
let snake = [new_head, ..state.snake]
let grid = dict.insert(state.grid, new_head, 1)
let state = State(..state, grid: grid, snake: snake)
spawn_fruit(state)
}
Ok(0) -> {
remove_last_snake_block(state, new_head)
}
_ -> panic as "Unreachable"
}
}
fn draw(state: State) {
// convert dict to list and sort it. must be sorted so it prints correctly.
let l = dict.to_list(state.grid)
let l = list.sort(l, fn(x, y) { int.compare(x.0, y.0) })
let score_string = "Score: " <> int.to_string(state.score)
let score_string_len = string.length(score_string)
let upper_border =
"┌"
<> score_string
<> string.repeat("─", state.columns - score_string_len)
<> "┐"
// make 2 accumulators, one for list of commands, the other one is for string acc.
// string represents a single row
// command is Println
// notice that we store pos as Int, not #(Int, Int). It is easier but we must
// convert Int to #(x,y) by using % division
let q = stdout.Queue([command.MoveTo(0, 0)])
let strings =
list.fold(l, #([], stree.new()), fn(q, cell) {
case cell {
// if we hit the end, add command to print the row with right border
#(pos, 0) if pos % state.columns == state.columns - 1 -> {
#([stree.append(q.1, " │") |> stree.to_string, ..q.0], stree.new())
}
#(pos, 1) if pos % state.columns == state.columns - 1 -> {
#([stree.append(q.1, "@│") |> stree.to_string, ..q.0], stree.new())
}
#(pos, 2) if pos % state.columns == state.columns - 1 -> {
#([stree.append(q.1, "$│") |> stree.to_string, ..q.0], stree.new())
}
// if we are at the start, print the cell with left border
#(pos, 0) if pos % state.columns == 0 -> {
#(q.0, stree.append(q.1, "│ "))
}
#(pos, 1) if pos % state.columns == 0 -> {
#(q.0, stree.append(q.1, "│@"))
}
#(pos, 2) if pos % state.columns == 0 -> {
#(q.0, stree.append(q.1, "│$"))
}
// if we are not at the start, just print the symbol
#(_, 0) -> #(q.0, stree.append(q.1, " "))
#(_, 1) -> #(q.0, stree.append(q.1, "@"))
#(_, 2) -> #(q.0, stree.append(q.1, "$"))
#(_, _) -> {
panic as "Unreachable"
}
}
})
let lower_border = "└" <> string.repeat("─", state.columns) <> "┘"
let strings = [lower_border, ..strings.0]
let strings = list.reverse(strings) |> list.prepend(upper_border)
let q1 = colorize(strings)
let q = stdout.queue(q, q1)
// let q = stdout.queue(q, q1)
let q = stdout.queue(q, [command.ResetStyle])
stdout.flush(q)
}
fn colorize(strings: List(String)) -> List(command.Command) {
list.index_map(strings, fn(str, i) {
command.Println(colorize_line(str, i % 2))
})
}
fn colorize_line(str: String, offset: Int) -> String {
string.to_graphemes(str)
|> list.index_map(fn(ch, i) {
let bg = case { i + offset } % 2 {
0 -> style.AnsiValue(254)
1 -> style.AnsiValue(188)
_ -> panic as "Unreachable"
}
case ch {
"@" -> style.with_on(ch, style.BrightGreen, bg)
"$" -> style.with_on(ch, style.Red, bg)
_ -> style.with_on(ch, style.Default, bg)
}
})
|> string.join("")
}
fn print_centered_colored_block(
state: State,
s: String,
c: style.Color,
) -> List(command.Command) {
let len = string.length(s)
let s = "│" <> s <> "│"
let x = state.columns
let y = state.rows / 2
let x_offset = len / 2 - 1
let x = x / 2 - x_offset
let upper_border = "┌" <> string.repeat("─", len) <> "┐"
let lower_border = "└" <> string.repeat("─", len) <> "┘"
[
command.SetForegroundColor(c),
command.MoveTo(x, y - 1),
command.Print(upper_border),
command.MoveTo(x, y),
command.Print(s),
command.MoveTo(x, y + 1),
command.Print(lower_border),
command.ResetStyle,
]
}
fn lose(state: State) {
stdout.execute(print_centered_colored_block(state, "You Lose", style.Red))
process.sleep(2000)
stdout.execute([command.Clear(terminal.All), command.LeaveAlternateScreen])
halt(1)
}
fn win(state: State) {
stdout.execute(print_centered_colored_block(state, "You Win", style.Green))
process.sleep(2000)
stdout.execute([command.Clear(terminal.All), command.LeaveAlternateScreen])
halt(0)
}