Eaglev0.1.0

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