Snake
Grid logic with a unit-tested core and speed that ramps as you eat.
Arrows/WASD · Space restarts
Source: examples/snake/main.cr
examples/snake/main.cr
require "../../src/eagle"
require "./snake"
include Eagle
class SnakeApp < App
CELL = 32
@game = SnakeGame.new(24, 16)
@timer = 0_f32
@speed = 8_f32 # cells per second
@snd : Sound? = nil
@snd_die : Sound? = nil
@best = 0
def load
Input.map "up", Key::Up, Key::W, Input.axis(GamepadAxis::LeftY, -1)
Input.map "down", Key::Down, Key::S, Input.axis(GamepadAxis::LeftY, 1)
Input.map "left", Key::Left, Key::A, Input.axis(GamepadAxis::LeftX, -1)
Input.map "right", Key::Right, Key::D, Input.axis(GamepadAxis::LeftX, 1)
@snd = Sound.tone(700, 0.06, Sound::Wave::Sine, 0.3)
@snd_die = Sound.tone(110, 0.5, Sound::Wave::Saw, 0.3)
end
def update(dt : Float32)
@game.turn(SnakeGame::Dir::Up) if Input.pressed?("up")
@game.turn(SnakeGame::Dir::Down) if Input.pressed?("down")
@game.turn(SnakeGame::Dir::Left) if Input.pressed?("left")
@game.turn(SnakeGame::Dir::Right) if Input.pressed?("right")
if @game.game_over?
if Input.pressed?(Key::Space) || Input.pressed?(Key::Enter)
@game = SnakeGame.new(24, 16); @speed = 8_f32
end
return
end
@timer += dt
while @timer >= 1 / @speed
@timer -= 1 / @speed
case @game.step
when :ate
@snd.try(&.play(pitch: 1 + @game.score * 0.02))
@speed += 0.25
@best = Math.max(@best, @game.score)
when :died then @snd_die.try(&.play)
end
end
Eagle.quit if Input.pressed?(Key::Escape)
end
def draw(g : Graphics)
ox = (Window.width - @game.width * CELL) / 2; oy = 50
g.rect(ox - 2, oy - 2, @game.width * CELL + 4, @game.height * CELL + 4, DrawMode::Line, Color.gray(0.4))
@game.height.times { |y| @game.width.times { |x| g.rect(ox + x * CELL, oy + y * CELL, CELL, CELL, color: (x + y).even? ? Color.hex("#1e2230") : Color.hex("#222739")) } }
fx, fy = @game.food
g.circle(ox + fx * CELL + CELL / 2, oy + fy * CELL + CELL / 2, CELL * 0.35, color: Color.hex("#ff5d73"))
n = @game.length
@game.body.each_with_index do |(x, y), i|
t = i / n.to_f
c = Color.hex("#7bed9f").lerp(Color.hex("#2ed573"), t)
g.rounded_rect(ox + x * CELL + 2, oy + y * CELL + 2, CELL - 4, CELL - 4, 6, color: c)
end
g.print("score #{@game.score} best #{@best} length #{n} speed #{@speed.round(1)}", ox, 14, Color::WHITE)
if @game.game_over?
g.rect(0, 0, Window.width, Window.height, color: Color.new(0, 0, 0, 0.5))
g.print("GAME OVER", Window.width / 2, Window.height / 2 - 30, Color::RED, scale: 2, align: TextAlign::Center)
g.print("press space to restart", Window.width / 2, Window.height / 2 + 20, Color::WHITE, align: TextAlign::Center)
end
end
end
Eagle.run(SnakeApp, title: "Eagle Snake", width: 24 * SnakeApp::CELL + 64, height: 16 * SnakeApp::CELL + 100)
examples/snake/snake.cr
# Snake game logic, independent of rendering.
class SnakeGame
enum Dir
Up; Down; Left; Right
def vec : {Int32, Int32}
case self
in Up then {0, -1}
in Down then {0, 1}
in Left then {-1, 0}
in Right then {1, 0}
end
end
def opposite?(o : Dir) : Bool
v = vec; w = o.vec
v[0] == -w[0] && v[1] == -w[1]
end
end
getter width : Int32
getter height : Int32
getter body : Array({Int32, Int32}) # head first
getter dir : Dir = Dir::Right
getter food : {Int32, Int32}
getter score = 0
getter? game_over = false
@next_dir : Dir = Dir::Right
@grow = 0
@rng : Random
def initialize(@width : Int32 = 20, @height : Int32 = 15, seed : Int32? = nil)
@rng = seed ? Random.new(seed) : Random.new
cx = @width // 2; cy = @height // 2
@body = [{cx, cy}, {cx - 1, cy}, {cx - 2, cy}]
@food = {0, 0}
place_food
end
def head : {Int32, Int32}; @body.first; end
# Queue a direction change (ignored if it reverses the snake).
def turn(d : Dir) : Nil
@next_dir = d unless d.opposite?(@dir)
end
def place_food : Nil
free = [] of {Int32, Int32}
@height.times { |y| @width.times { |x| free << {x, y} unless @body.includes?({x, y}) } }
if free.empty?
@game_over = true
else
@food = free[@rng.rand(free.size)]
end
end
# Advance one cell. Returns :moved, :ate, or :died.
def step : Symbol
return :died if @game_over
@dir = @next_dir
dx, dy = @dir.vec
nx = head[0] + dx; ny = head[1] + dy
if nx < 0 || ny < 0 || nx >= @width || ny >= @height
@game_over = true
return :died
end
tail_moves = @grow == 0
if @body.includes?({nx, ny}) && !(tail_moves && @body.last == {nx, ny})
@game_over = true
return :died
end
@body.unshift({nx, ny})
if @grow > 0
@grow -= 1
else
@body.pop
end
if {nx, ny} == @food
@score += 1
@grow += 2
place_food
return :ate
end
:moved
end
def length : Int32; @body.size; end
end