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
- 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
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:
# 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 | |
# TODO implement scoring on snake eating apples | |
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!
No comments:
Post a Comment