Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol examples snippets advanced snake.exs
Raw

examples/snippets/advanced/snake.exs

# An example that shows how to make a game with Raxol by using the canvas
# element.
#
# Usage:
# elixir examples/snippets/advanced/snake.exs
defmodule Snake do
# Use the correct Application behaviour and View DSL
use Raxol.Core.Runtime.Application
import Raxol.View.Elements
alias Raxol.Core.Runtime.Events.Subscription
alias Raxol.Core.Events.Event
alias Raxol.Core.Commands.Command
# Arrow key chars might vary by terminal, handle common patterns or codes if needed
@up [:key_up]
@down [:key_down]
@left [:key_left]
@right [:key_right]
# Consider adding WASD or hjkl keys for wider compatibility
@arrows @up ++ @down ++ @left ++ @right
@initial_length 4
@impl true
def init(context) do
Raxol.Core.Runtime.Log.debug("Snake: init/1 context: \#{inspect(context)}")
# Assume context provides initial window dimensions if needed, or use defaults
# Using fixed size for simplicity now, context inspection needed for dynamic size
height = context[:height] || 20
width = context[:width] || 40
# Return :ok tuple
{:ok,
%{
direction: :right,
# Ensure coordinates start within bounds (e.g., 0-based)
chain: Enum.map(@initial_length..1, fn x -> {x, 0} end),
# Ensure food starts within bounds
food: {7, 7},
alive: true,
# Adjust for potential border/padding
height: height - 2,
# Adjust for potential border/padding
width: width - 2
}}
end
@impl true
def update(message, model) do
Raxol.Core.Runtime.Log.debug("Snake: update/2 received message: \#{inspect(message)}")
case message do
# Use Event struct for key presses
%Event{type: :key, data: %{key: key_name}} when key_name in @arrows ->
new_dir = key_to_dir(key_name)
# Return :ok tuple
{:ok, %{model | direction: next_dir(model.direction, new_dir)}, []}
# Handle quit keys
%Event{type: :key, data: %{key: :char, char: "q"}} ->
{:ok, model, [Command.new(:quit)]}
%Event{type: :key, data: %{key: :char, char: "c", ctrl: true}} ->
{:ok, model, [Command.new(:quit)]}
:tick ->
new_model = move_snake(model)
# Return :ok tuple
{:ok, new_model, []}
_ ->
# Return :ok tuple
{:ok, model, []}
end
end
# Renamed from subscribe/1
@impl true
def subscriptions(%{alive: alive}) do
Raxol.Core.Runtime.Log.debug("Snake: subscriptions/1 alive: \#{alive}")
# Only subscribe to ticks if the game is active
if alive do
Subscription.interval(100, :tick)
else
Subscription.none()
end
end
# Renamed from render/1
@impl true
def view(%{chain: chain} = model) do
Raxol.Core.Runtime.Log.debug("Snake: view/1")
score = length(chain) - @initial_length
view do
# Use box instead of panel
box(
title: "SNAKE (Move with Arrows) Score=#{score} | q/Ctrl+C to Quit",
style: [[:height, :fill], [:padding, 0], [:border, :single]]
) do
if model.alive do
render_board(model)
else
# Center Game Over message
box style: [
[:height, :fill],
[:width, :fill],
[:align_items, :center],
[:justify_content, :center]
] do
text(content: "Game Over! Score: #{score}")
end
end
end
end
end
defp render_board(
%{
chain: [{head_x, head_y} | tail],
food: {food_x, food_y}
} = model
) do
# Assuming canvas and its syntax is correct
# Head is often different from tail segments
head_cell = %{
x: head_x,
y: head_y,
char: "@",
style: %{color: :green, bg_color: :dark_green}
}
# Tail segments
tail_cells =
for {x, y} <- tail, do: %{x: x, y: y, char: "o", style: %{color: :green}}
# Food
food_cell = %{x: food_x, y: food_y, char: "X", style: %{color: :red}}
canvas(
height: model.height,
width: model.width,
cells: [food_cell, head_cell | tail_cells]
)
end
defp move_snake(model) do
# Tail used via model later
[head | _tail] = model.chain
next = next_link(head, model.direction)
cond do
not next_valid?(next, model) ->
Raxol.Core.Runtime.Log.info(
"Snake: Game Over - Collision detected at \#{inspect(next)}"
)
%{model | alive: false}
next == model.food ->
Raxol.Core.Runtime.Log.debug("Snake: Food eaten at \#{inspect(next)}")
new_food =
random_food(model.width - 1, model.height - 1, [next | model.chain])
Raxol.Core.Runtime.Log.debug("Snake: New food at \#{inspect(new_food)}")
# Grow snake by prepending head and keeping old tail
%{model | chain: [next | model.chain], food: new_food}
true ->
# Move snake by prepending head and dropping last element of old chain
%{model | chain: [next | Enum.drop(model.chain, -1)]}
end
end
# Ensure new food doesn't spawn on the snake
defp random_food(max_x, max_y, occupied) do
food = {Enum.random(0..max_x), Enum.random(0..max_y)}
if food in occupied do
Raxol.Core.Runtime.Log.debug("Snake: Random food conflict, retrying...")
random_food(max_x, max_y, occupied)
else
food
end
end
# Map key names (atoms) to directions
defp key_to_dir(:key_up), do: :up
defp key_to_dir(:key_down), do: :down
defp key_to_dir(:key_left), do: :left
defp key_to_dir(:key_right), do: :right
# Check bounds and self-collision
defp next_valid?({x, y}, model)
when x < 0 or y < 0 or x >= model.width or y >= model.height do
false
end
defp next_valid?(next, %{chain: chain}), do: next not in chain
# Prevent moving directly opposite
defp next_dir(:up, :down), do: :up
defp next_dir(:down, :up), do: :down
defp next_dir(:left, :right), do: :left
defp next_dir(:right, :left), do: :right
defp next_dir(_current, new), do: new
# Calculate next head position
defp next_link({x, y}, :up), do: {x, y - 1}
defp next_link({x, y}, :down), do: {x, y + 1}
defp next_link({x, y}, :left), do: {x - 1, y}
defp next_link({x, y}, :right), do: {x + 1, y}
end
Raxol.Core.Runtime.Log.info("Snake: Starting Raxol...")
{:ok, _pid} = Raxol.start_link(Snake, [])
Raxol.Core.Runtime.Log.info("Snake: Raxol started. Running...")