Snake

Snakes need to eat, too!

Connecting...

Explanation

However impractical it might seem, we wanted to demonstrate that StimulusReflex can emit updates fast enough to manage 60fps on a local connection. While you probably shouldn't port Doom, please consider yourself dared.

At this time, this is a desktop-only demo as it relies on the arrow keys to steer.

Code (177 LOC)

CSS (13 LOC)
app/javascript/stylesheets/snake.scss (13 LOC)
rect.segment {
width: 10px;
height: 10px;
fill: black;
&.head {
fill: red;
}
}
rect.food {
width: 10px;
height: 10px;
fill: green;
}
ERB (34 LOC)
app/views/snakes/_demo.html.erb (34 LOC)
<div data-controller="snake" data-action="<% if session[:clock] && session[:alive] %>keydown@window->snake#turn<% end %> beforeunload@window->snake#stop">
<svg width="<%= @grid_x %>" height="<%= @grid_y %>">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="gray" stroke-width="0.5"/>
</pattern>
</defs>
<polygon points="<%= "0,0 0,#{@grid_y} #{@grid_x},#{@grid_y} #{@grid_x},0" %>" fill="none" stroke="black" />
<rect width="100%" height="100%" style="fill:url(#grid)" />
<% if session[:food].present? %>
<rect class="food" x="<%= session[:food][0] %>" y="<%= session[:food][1] %>" />
<% end %>
<% session[:snake].each do |s| %>
<rect class="segment <%= "head" if s == session[:snake].last %>" x="<%= s[0] %>" y="<%= s[1] %>" />
<% end %>
</svg>
<div>
<% if @stimulus_reflex %>
<% if session[:alive] %>
<button data-reflex="click->SnakeReflex#start_stop" class="btn btn-primary mt-3">
<%= session[:clock] ? "Pause" : "Start" %>
</button>
<% else %>
<h1 class="mt-3 text-danger">DEATH</h1>
<a href="<%= request.path_info %>" data-turbolinks="false">Try again?</a>
<% end %>
<% else %>
<h3 class="mt-3 text-info" data-target="snake.connecting">Connecting...</h3>
<button data-reflex="click->SnakeReflex#start_stop" data-target="snake.start" class="btn btn-primary mt-3" hidden>
Start
</button>
<% end %>
</div>
</div>
JavaScript (23 LOC)
app/javascript/controllers/snake_controller.js (23 LOC)
import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'
export default class extends Controller {
static targets = ['start', 'connecting']
connect () {
StimulusReflex.register(this)
this.StimulusReflex.subscription.consumer.connection.webSocket.addEventListener(
'open',
this.enableStart.bind(this)
)
}
turn (event) {
this.stimulate('SnakeReflex#turn', event.which)
event.preventDefault()
}
stop () {
this.stimulate('SnakeReflex#stop')
}
enableStart () {
this.connectingTarget.hidden = true
this.startTarget.hidden = false
}
}
Ruby (107 LOC)
app/controllers/snakes_controller.rb (19 LOC)
class SnakesController < ApplicationController
def show
unless @stimulus_reflex
session[:direction] = "right"
session[:clock] = false
session[:speed] = 0.2
session[:grid_x] = 30
session[:grid_y] = 30
session[:length] = 4
session[:snake] = []
session[:start_x] = 50
session[:start_y] = 50
session[:food] = []
session[:alive] = true
end
@grid_x = session[:grid_x] * 10 + 1
@grid_y = session[:grid_y] * 10 + 1
end
end
app/reflexes/snake_reflex.rb (88 LOC)
class SnakeReflex < ApplicationReflex
DIRECTIONS = {37 => "left", 38 => "up", 39 => "right", 40 => "down"}
def turn(code)
if DIRECTIONS.key?(code)
if (["left", "right"].include?(session[:direction]) && ["up", "down"].include?(DIRECTIONS[code])) || (["up", "down"].include?(session[:direction]) && ["left", "right"].include?(DIRECTIONS[code]))
session[:direction] = DIRECTIONS[code]
end
end
end
def start_stop
session[:clock] = session[:clock] ? !session[:clock] : true
wait_for_it(:tick) do
hatch if session[:snake].empty?
sprout if session[:food].empty?
[]
end
end
def tick
if session[:clock] && session[:alive]
wait_for_it(:tick) do
sleep session[:speed]
eat
move
eat
session[:alive] = survive?
[]
end
else
session[:clock] = false
end
end
def stop
session[:clock] = false
end
def sprout
session[:food] = []
session[:food] << rand(0...session[:grid_x]) * 10
session[:food] << rand(0...session[:grid_y]) * 10
end
def hatch
session[:length].times do |i|
session[:snake] << [session[:start_x] + (10 * i), session[:start_y]]
end
end
def eat
if session[:snake].last == session[:food]
session[:snake] << session[:food]
session[:speed] -= 0.01 if session[:speed] > 0.05
sprout
end
end
def move
x, y = session[:snake].last
case session[:direction]
when "left"
x -= 10
when "up"
y -= 10
when "right"
x += 10
when "down"
y += 10
end
session[:snake].shift
session[:snake] << [x, y]
end
def survive?
x, y = session[:snake].last
return false if x < 0 || y < 0 || x > session[:grid_x] * 10 - 10 || y > session[:grid_y] * 10 - 10
return false if session[:snake][0..-3].include? [x, y]
true
end
private
def wait_for_it(target)
return unless respond_to? target
if block_given?
Thread.new do
channel.receive({
"target" => "#{self.class}##{target}",
"args" => yield,
"url" => url,
"attrs" => element.attributes.to_h,
"selectors" => selectors
})
end
end
end
end