31
Snake Game by Glimmer DSL for LibUI
I mentioned in a previous blog post that I received an issue request to build games in Glimmer DSL for LibUI as examples.
Three games were built to fully address that issue request: Tetris, Tic-Tac-Toe, and most recently Snake.
In fact, Snake has been built test-first following the MVP (Model / View / Presenter) architectural pattern.
The View code written in Glimmer DSL for LibUI is very simple and short:
# From: https://github.com/AndyObtiva/glimmer-dsl-libui#snake
require 'glimmer-dsl-libui'
require 'glimmer/data_binding/observer'
require_relative 'snake/presenter/grid'
class Snake
CELL_SIZE = 15
SNAKE_MOVE_DELAY = 0.1
include Glimmer
def initialize
@game = Model::Game.new
@grid = Presenter::Grid.new(@game)
@game.start
create_gui
register_observers
end
def launch
@main_window.show
end
def register_observers
@game.height.times do |row|
@game.width.times do |column|
Glimmer::DataBinding::Observer.proc do |new_color|
@cell_grid[row][column].fill = new_color
end.observe(@grid.cells[row][column], :color)
end
end
Glimmer::DataBinding::Observer.proc do |game_over|
Glimmer::LibUI.queue_main do
if game_over
msg_box('Game Over!', "Score: #{@game.score} | High Score: #{@game.high_score}")
@game.start
end
end
end.observe(@game, :over)
Glimmer::LibUI.timer(SNAKE_MOVE_DELAY) do
unless @game.over?
@game.snake.move
@main_window.title = "Glimmer Snake (Score: #{@game.score} | High Score: #{@game.high_score})"
end
end
end
def create_gui
@cell_grid = []
@main_window = window('Glimmer Snake', @game.width * CELL_SIZE, @game.height * CELL_SIZE) {
resizable false
vertical_box {
padded false
@game.height.times do |row|
@cell_grid << []
horizontal_box {
padded false
@game.width.times do |column|
area {
@cell_grid.last << path {
square(0, 0, CELL_SIZE)
fill Presenter::Cell::COLOR_CLEAR
}
on_key_up do |area_key_event|
orientation_and_key = [@game.snake.head.orientation, area_key_event[:ext_key]]
case orientation_and_key
in [:north, :right] | [:east, :down] | [:south, :left] | [:west, :up]
@game.snake.turn_right
in [:north, :left] | [:west, :down] | [:south, :right] | [:east, :up]
@game.snake.turn_left
else
# No Op
end
end
}
end
}
end
}
}
end
end
Snake.new.launch
Basically, the game consists of the following models in the Model layer:
- Game: general manager of the game including scoring and game over state
- Snake: handles snake movement including vertebra locations and collided state
- Vertebra: represents a small part of a snake's body that gets added every time the snake eats an apple
- Apple: represents the apple that is generated at random locations while the snake is moving
Additionally, the game has the following presenters in the Presenter layer:
- Grid: contains all colored 40x40 cells that are shown in the View. The Grid basically monitors the Game Snake and Apple locations and updates its cell colors accordingly following the Observer pattern.
- Cell: represents a single cell with its color that will be shown in the View
Finally, the View layer is simply the Glimmer DSL for LibUI Snake example app, which wires everything together.
Here are the game specs (spec/examples/snake/model/game_spec.rb), which start by gradually testing the movement of a bodyless snake head and then test adding vertabrae bit by bit by eating apples (you may skip if you want to check out the Model and Presenter code included after the specs):
# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/spec/examples/snake/model/game_spec.rb
require 'spec_helper'
require 'examples/snake/model/game'
RSpec.describe Snake::Model::Game do
it 'has a grid of vertebrae of width of 40 and height of 40' do
expect(subject).to be_a(Snake::Model::Game)
expect(subject.width).to eq(40)
expect(subject.height).to eq(40)
end
it 'starts game by generating snake and apple in random locations' do
subject.start
expect(subject).to_not be_over
expect(subject.score).to eq(0)
expect(subject.snake).to be_a(Snake::Model::Snake)
expect(subject.snake.length).to eq(1)
expect(subject.snake.head).to be_a(Snake::Model::Vertebra)
expect(subject.snake.head).to eq(subject.snake.vertebrae.last)
expect(subject.snake.head.row).to be_between(0, subject.height)
expect(subject.snake.head.column).to be_between(0, subject.width)
expect(Snake::Model::Vertebra::ORIENTATIONS).to include(subject.snake.head.orientation)
expect(subject.snake.length).to eq(1)
expect(subject.apple).to be_a(Snake::Model::Apple)
expect(subject.snake.vertebrae.map {|v| [v.row, v.column]}).to_not include([subject.apple.row, subject.apple.column])
expect(subject.apple.row).to be_between(0, subject.height)
expect(subject.apple.column).to be_between(0, subject.width)
end
it 'moves snake of length 1 east without going through a wall' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(1)
end
it 'moves snake of length 1 east going through a wall' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(39)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
end
it 'moves snake of length 1 west without going through a wall' do
direction = :west
subject.start
subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(39)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(38)
end
it 'moves snake of length 1 west going through a wall' do
direction = :west
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(39)
end
it 'moves snake of length 1 south without going through a wall' do
direction = :south
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(1)
expect(subject.snake.head.column).to eq(0)
end
it 'moves snake of length 1 south going through a wall' do
direction = :south
subject.start
subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(39)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
end
it 'moves snake of length 1 north without going through a wall' do
direction = :north
subject.start
subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(39)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(38)
expect(subject.snake.head.column).to eq(0)
end
it 'moves snake of length 1 north going through a wall' do
direction = :north
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
expect(subject.snake.head.row).to eq(0)
expect(subject.snake.head.column).to eq(0)
expect(subject.snake.head.orientation).to eq(direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
expect(subject.apple.row).to eq(20)
expect(subject.apple.column).to eq(20)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(39)
expect(subject.snake.head.column).to eq(0)
end
it 'starts snake going east, moves, turns right south, and moves south' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :south
subject.snake.move
subject.snake.turn_right
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(1)
expect(subject.snake.head.column).to eq(1)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going west, moves, turns right north, and moves south' do
direction = :west
subject.start
subject.snake.generate(initial_row: 39, initial_column: 39, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :north
subject.snake.move
subject.snake.turn_right
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(38)
expect(subject.snake.head.column).to eq(38)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going south, moves, turns right west, and moves south' do
direction = :south
subject.start
subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :west
subject.snake.move
subject.snake.turn_right
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(1)
expect(subject.snake.head.column).to eq(38)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going north, moves, turns right east, and moves south' do
direction = :north
subject.start
subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :east
subject.snake.move
subject.snake.turn_right
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(38)
expect(subject.snake.head.column).to eq(1)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going east, moves, turns left north, and moves south' do
direction = :east
subject.start
subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :north
subject.snake.move
subject.snake.turn_left
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(38)
expect(subject.snake.head.column).to eq(1)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going west, moves, turns left south, and moves south' do
direction = :west
subject.start
subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :south
subject.snake.move
subject.snake.turn_left
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(1)
expect(subject.snake.head.column).to eq(38)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going south, moves, turns left east, and moves south' do
direction = :south
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :east
subject.snake.move
subject.snake.turn_left
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(1)
expect(subject.snake.head.column).to eq(1)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going north, moves, turns left west, and moves south' do
direction = :north
subject.start
subject.snake.generate(initial_row: 39, initial_column: 39, initial_orientation: direction)
subject.apple.generate(initial_row: 20, initial_column: 20)
new_direction = :west
subject.snake.move
subject.snake.turn_left
expect(subject.snake.head.orientation).to eq(new_direction)
subject.snake.move
expect(subject.snake.length).to eq(1)
expect(subject.snake.head.row).to eq(38)
expect(subject.snake.head.column).to eq(38)
expect(subject.snake.head.orientation).to eq(new_direction)
end
it 'starts snake going east, moves, turns right south, and eats apple while moving south' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 1, initial_column: 1)
new_direction = :south
subject.snake.move
subject.snake.turn_right
subject.snake.move
expect(subject.snake.length).to eq(2)
expect(subject.snake.vertebrae[0].row).to eq(0)
expect(subject.snake.vertebrae[0].column).to eq(1)
expect(subject.snake.vertebrae[0].orientation).to eq(new_direction)
expect(subject.snake.vertebrae[1].row).to eq(1)
expect(subject.snake.vertebrae[1].column).to eq(1)
expect(subject.snake.vertebrae[1].orientation).to eq(new_direction)
end
it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 1, initial_column: 1)
subject.snake.move
subject.snake.turn_right
subject.snake.move # eats apple
subject.apple.generate(initial_row: 1, initial_column: 2)
subject.snake.turn_left
subject.snake.move # eats apple
expect(subject.snake.length).to eq(3)
expect(subject.snake.vertebrae[0].row).to eq(0)
expect(subject.snake.vertebrae[0].column).to eq(1)
expect(subject.snake.vertebrae[0].orientation).to eq(:south)
expect(subject.snake.vertebrae[1].row).to eq(1)
expect(subject.snake.vertebrae[1].column).to eq(1)
expect(subject.snake.vertebrae[1].orientation).to eq(:east)
expect(subject.snake.vertebrae[2].row).to eq(1)
expect(subject.snake.vertebrae[2].column).to eq(2)
expect(subject.snake.vertebrae[2].orientation).to eq(:east)
end
it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east, turns right, moves south' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 1, initial_column: 1)
subject.snake.move
subject.snake.turn_right
subject.snake.move # eats apple
subject.apple.generate(initial_row: 1, initial_column: 2)
subject.snake.turn_left
subject.snake.move # eats apple
subject.apple.generate(initial_row: 20, initial_column: 20)
subject.snake.turn_right
subject.snake.move
expect(subject.snake.length).to eq(3)
expect(subject.snake.vertebrae[0].row).to eq(1)
expect(subject.snake.vertebrae[0].column).to eq(1)
expect(subject.snake.vertebrae[0].orientation).to eq(:east)
expect(subject.snake.vertebrae[1].row).to eq(1)
expect(subject.snake.vertebrae[1].column).to eq(2)
expect(subject.snake.vertebrae[1].orientation).to eq(:south)
expect(subject.snake.vertebrae[2].row).to eq(2)
expect(subject.snake.vertebrae[2].column).to eq(2)
expect(subject.snake.vertebrae[2].orientation).to eq(:south)
end
it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east, turns left, eats apple while moving north, turns left, collides while moving west and game is over' do
direction = :east
subject.start
subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
subject.apple.generate(initial_row: 1, initial_column: 1)
subject.snake.move # 0, 1
subject.snake.turn_right
subject.snake.move # 1, 1 eats apple
subject.apple.generate(initial_row: 1, initial_column: 2)
subject.snake.turn_left
subject.snake.move # 1, 2 eats apple
subject.apple.generate(initial_row: 1, initial_column: 3)
subject.snake.move # 1, 3 eats apple
subject.apple.generate(initial_row: 1, initial_column: 4)
subject.snake.move # 1, 4 eats apple
subject.snake.turn_left
subject.snake.move # 0, 4
subject.snake.turn_left
subject.snake.move # 0, 3
subject.snake.turn_left
subject.snake.move # 1, 3 (collision)
expect(subject).to be_over
expect(subject.score).to eq(50 * 4)
expect(subject.snake).to be_collided
expect(subject.snake.length).to eq(5)
expect(subject.snake.vertebrae[0].row).to eq(1)
expect(subject.snake.vertebrae[0].column).to eq(2)
expect(subject.snake.vertebrae[0].orientation).to eq(:east)
expect(subject.snake.vertebrae[1].row).to eq(1)
expect(subject.snake.vertebrae[1].column).to eq(3)
expect(subject.snake.vertebrae[1].orientation).to eq(:east)
expect(subject.snake.vertebrae[2].row).to eq(1)
expect(subject.snake.vertebrae[2].column).to eq(4)
expect(subject.snake.vertebrae[2].orientation).to eq(:north)
expect(subject.snake.vertebrae[3].row).to eq(0)
expect(subject.snake.vertebrae[3].column).to eq(4)
expect(subject.snake.vertebrae[3].orientation).to eq(:west)
expect(subject.snake.vertebrae[4].row).to eq(0)
expect(subject.snake.vertebrae[4].column).to eq(3)
expect(subject.snake.vertebrae[4].orientation).to eq(:south)
end
end
Here are the Models:
Game:
require 'fileutils'
require_relative 'snake'
require_relative 'apple'
class Snake
module Model
class Game
WIDTH_DEFAULT = 40
HEIGHT_DEFAULT = 40
FILE_HIGH_SCORE = File.expand_path(File.join(Dir.home, '.glimmer-snake'))
attr_reader :width, :height
attr_accessor :snake, :apple, :over, :score, :high_score
alias over? over
def initialize(width = WIDTH_DEFAULT, height = HEIGHT_DEFAULT)
@width = width
@height = height
@snake = Snake.new(self)
@apple = Apple.new(self)
FileUtils.touch(FILE_HIGH_SCORE)
@high_score = File.read(FILE_HIGH_SCORE).to_i rescue 0
end
def score=(new_score)
@score = new_score
self.high_score = @score if @score > @high_score
end
def high_score=(new_high_score)
@high_score = new_high_score
File.write(FILE_HIGH_SCORE, @high_score.to_s)
rescue => e
puts e.full_message
end
def start
self.over = false
self.score = 0
self.snake.generate
self.apple.generate
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 75]}... >"
end
end
end
end
Snake:
require_relative 'vertebra'
class Snake
module Model
class Snake
SCORE_EAT_APPLE = 50
RIGHT_TURN_MAP = {
north: :east,
east: :south,
south: :west,
west: :north
}
LEFT_TURN_MAP = RIGHT_TURN_MAP.invert
attr_accessor :collided
alias collided? collided
attr_reader :game
# vertebrae and joins are ordered from tail to head
attr_accessor :vertebrae
def initialize(game)
@game = game
end
# generates a new snake location and orientation from scratch or via dependency injection of what head_cell and orientation are (for testing purposes)
def generate(initial_row: nil, initial_column: nil, initial_orientation: nil)
self.collided = false
initial_vertebra = Vertebra.new(snake: self, row: initial_row, column: initial_column, orientation: initial_orientation)
self.vertebrae = [initial_vertebra]
end
def length
@vertebrae.length
end
def head
@vertebrae.last
end
def tail
@vertebrae.first
end
def remove
self.vertebrae.clear
self.joins.clear
end
def move
@old_tail = tail.dup
@new_head = head.dup
case @new_head.orientation
when :east
@new_head.column = (@new_head.column + 1) % @game.width
when :west
@new_head.column = (@new_head.column - 1) % @game.width
when :south
@new_head.row = (@new_head.row + 1) % @game.height
when :north
@new_head.row = (@new_head.row - 1) % @game.height
end
if @vertebrae.map {|v| [v.row, v.column]}.include?([@new_head.row, @new_head.column])
self.collided = true
@game.over = true
else
@vertebrae.append(@new_head)
@vertebrae.delete(tail)
if head.row == @game.apple.row && head.column == @game.apple.column
grow
@game.apple.generate
end
end
end
def turn_right
head.orientation = RIGHT_TURN_MAP[head.orientation]
end
def turn_left
head.orientation = LEFT_TURN_MAP[head.orientation]
end
def grow
@game.score += SCORE_EAT_APPLE
@vertebrae.prepend(@old_tail)
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 150]}... >"
end
end
end
end
Vertebra:
class Snake
module Model
class Vertebra
ORIENTATIONS = %i[north east south west]
# orientation is needed for snake occuppied cells (but not apple cells)
attr_reader :snake
attr_accessor :row, :column, :orientation
def initialize(snake: , row: , column: , orientation: )
@row = row || rand(snake.game.height)
@column = column || rand(snake.game.width)
@orientation = orientation || ORIENTATIONS.sample
@snake = snake
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 150]}... >"
end
end
end
end
Apple:
class Snake
module Model
class Apple
attr_reader :game
attr_accessor :row, :column
def initialize(game)
@game = game
end
# generates a new location from scratch or via dependency injection of what cell is (for testing purposes)
def generate(initial_row: nil, initial_column: nil)
if initial_row && initial_column
self.row, self.column = initial_row, initial_column
else
self.row, self.column = @game.height.times.zip(@game.width.times).reject do |row, column|
@game.snake.vertebrae.map {|v| [v.row, v.column]}.include?([row, column])
end.sample
end
end
def remove
self.row = nil
self.column = nil
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 120]}... >"
end
end
end
end
Here are the Presenters:
Grid:
require 'glimmer/data_binding/observer'
require_relative '../model/game'
require_relative 'cell'
class Snake
module Presenter
class Grid
attr_reader :game, :cells
def initialize(game = Model::Game.new)
@game = game
@cells = @game.height.times.map do |row|
@game.width.times.map do |column|
Cell.new(grid: self, row: row, column: column)
end
end
Glimmer::DataBinding::Observer.proc do |new_vertebrae|
occupied_snake_positions = @game.snake.vertebrae.map {|v| [v.row, v.column]}
@cells.each_with_index do |row_cells, row|
row_cells.each_with_index do |cell, column|
if [@game.apple.row, @game.apple.column] == [row, column]
cell.color = Cell::COLOR_APPLE
elsif occupied_snake_positions.include?([row, column])
cell.color = Cell::COLOR_SNAKE
else
cell.clear
end
end
end
end.observe(@game.snake, :vertebrae)
end
def clear
@cells.each do |row_cells|
row_cells.each do |cell|
cell.clear
end
end
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 75]}... >"
end
end
end
end
Cell:
class Snake
module Presenter
class Cell
COLOR_CLEAR = :white
COLOR_SNAKE = :green
COLOR_APPLE = :red
attr_reader :row, :column, :grid
attr_accessor :color
def initialize(grid: ,row: ,column: )
@row = row
@column = column
@grid = grid
end
def clear
self.color = COLOR_CLEAR unless color == COLOR_CLEAR
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 150]}... >"
end
end
end
end
Happy Glimmering!
31