├── .gitignore ├── sounds ├── death.mp3 └── collect.mp3 ├── README.textile └── snake.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /sounds/death.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/snake-shoes/master/sounds/death.mp3 -------------------------------------------------------------------------------- /sounds/collect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/snake-shoes/master/sounds/collect.mp3 -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. Snake in Shoes 2 | 3 | This is a simple Snake game written with Ruby and Shoes for a series of tutorials on "Quite Useful":http://quiteuseful.co.uk 4 | 5 | -------------------------------------------------------------------------------- /snake.rb: -------------------------------------------------------------------------------- 1 | class GameItem 2 | attr_accessor :x, :y 3 | 4 | def initialize(app, x, y, colour = '#fff', fill = '#000000') 5 | @app = app 6 | @x = x 7 | @y = y 8 | @app.stroke colour 9 | @app.fill fill 10 | @rect = app.rect x * cell_size, y * cell_size, cell_size - 1, cell_size - 1 11 | end 12 | 13 | def move_to(x, y) 14 | @x = x 15 | @y = y 16 | move_rect 17 | end 18 | 19 | def move_by(x, y) 20 | @x += x 21 | @y += y 22 | move_rect 23 | end 24 | 25 | def cell_size 26 | 10 27 | end 28 | 29 | # This removes the item from the game 30 | def remove 31 | @rect.hide 32 | true 33 | end 34 | 35 | private 36 | 37 | def move_rect 38 | @rect.move @x * cell_size, @y * cell_size 39 | end 40 | end 41 | 42 | class Snake 43 | attr_accessor :segments, :x, :y, :dead 44 | 45 | class Segment < GameItem 46 | end 47 | 48 | def initialize(app, x, y) 49 | @app = app 50 | change_direction :left 51 | @segments = [] 52 | @x = x 53 | @y = y 54 | @dead = false 55 | 56 | add_segment 57 | end 58 | 59 | def length 60 | @segments.size 61 | end 62 | 63 | def add_segment 64 | segment = nil 65 | if @segments.empty? 66 | segment = Segment.new @app, @x, @y, '#ff0000' 67 | else 68 | segment = Segment.new @app, @segments.last.x, @segments.last.y 69 | segment.move_by @direction_x * -1, @direction_y * -1 70 | end 71 | 72 | # Make sure the segment is added to the back 73 | @segments << segment 74 | end 75 | 76 | def move 77 | @segments.reverse.each_with_index do |segment, i| 78 | if i + 1 == length 79 | segment.move_by @direction_x, @direction_y 80 | else 81 | next_segment = @segments.reverse[i + 1] 82 | segment.move_to next_segment.x, next_segment.y 83 | end 84 | end 85 | 86 | @x = @segments.first.x 87 | @y = @segments.first.y 88 | end 89 | 90 | def change_direction(direction) 91 | @direction = direction 92 | case direction 93 | when :up 94 | @direction_x = 0 95 | @direction_y = -1 96 | when :down 97 | @direction_x = 0 98 | @direction_y = 1 99 | when :left 100 | @direction_x = -1 101 | @direction_y = 0 102 | when :right 103 | @direction_x = 1 104 | @direction_y = 0 105 | end 106 | end 107 | end 108 | 109 | class GameBoard 110 | class Food < GameItem 111 | def initialize(app, x, y, colour = '#00ff00') 112 | super 113 | end 114 | end 115 | 116 | class Brick < GameItem 117 | def initialize(app, x, y, colour = '#6666ff', fill = '#000099') 118 | super 119 | end 120 | end 121 | 122 | def initialize(app, snake) 123 | @food_items = [] 124 | @bricks = [] 125 | @app = app 126 | @snake = snake 127 | 128 | @boundary_x = [1, 58] 129 | @boundary_y = [4, 48] 130 | end 131 | 132 | def random_coordinate 133 | [rand(@boundary_x.last - @boundary_x.first - 1) + @boundary_x.first + 1, 134 | rand(@boundary_y.last - @boundary_y.first - 1) + @boundary_y.first + 1] 135 | end 136 | 137 | def add_food(x, y) 138 | # Don't add food over anything 139 | unless collision_with_anything? x, y 140 | @food_items << Food.new(@app, x, y) 141 | end 142 | end 143 | 144 | def add_brick(x, y) 145 | # Don't add bricks over anything 146 | unless collision_with_anything? x, y 147 | @bricks << Brick.new(@app, x, y) 148 | end 149 | end 150 | 151 | # Has the snake eaten some food? 152 | def food_eaten? 153 | collision? @snake.x, @snake.y, @food_items 154 | end 155 | 156 | # Add a segment to the snake and remove food at the snake's head 157 | def eat_food 158 | @snake.add_segment 159 | remove_food_at @snake.x, @snake.y 160 | add_food *random_coordinate 161 | end 162 | 163 | def add_brick_border 164 | # Add bricks but don't record them for collision detection 165 | # This draws the bricks for the horizontal bars 166 | @boundary_y.each do |y| 167 | @boundary_x.last.times do |x| 168 | Brick.new(@app, x + 1, y) 169 | end 170 | end 171 | 172 | # This draws the bricks for the vertical bars 173 | @boundary_x.each do |x| 174 | (@boundary_y.last - @boundary_y.first).times do |y| 175 | Brick.new(@app, x, y + @boundary_y.first) 176 | end 177 | end 178 | end 179 | 180 | # Has the snake crashed into a brick? 181 | def crashed_into_brick? 182 | # Is there a boundary collision? 183 | return true if @snake.x == @boundary_x.first or @snake.x == @boundary_x.last 184 | return true if @snake.y == @boundary_y.first or @snake.y == @boundary_y.last 185 | 186 | collision? @snake.x, @snake.y, @bricks 187 | end 188 | 189 | # Has the snake crashed into itself? Check the snake's head against the rest of its segments 190 | def crashed_into_self? 191 | collision? @snake.x, @snake.y, @snake.segments[1..-1] 192 | end 193 | 194 | # Is there a collision between any block in the game? Used when positioning blocks 195 | def collision_with_anything?(x, y) 196 | collision?(x, y, @bricks) or collision?(x, y, @food_items) or collision?(x, y, @snake.segments) 197 | end 198 | 199 | # Is there a collision for the x, y co-ords and a list of items that respond to x and y 200 | def collision?(x, y, items) 201 | items.find do |item| 202 | collision_at? x, y, item 203 | end 204 | end 205 | 206 | # Is there a collision at x, y and item (which responds to x and y) 207 | def collision_at?(x, y, item) 208 | item.x == x and item.y == y 209 | end 210 | 211 | def remove_food_at(x, y) 212 | @food_items.delete_if do |item| 213 | if item.x == x and item.y == y 214 | item.remove 215 | end 216 | end 217 | end 218 | 219 | # Calculate the score 220 | def score 221 | @snake.segments.length * 10 222 | end 223 | end 224 | 225 | class Sounds 226 | def initialize(app) 227 | @app = app 228 | @sounds = {} 229 | end 230 | 231 | def add_sound(sound_name, file_name) 232 | @sounds[sound_name] = @app.video("sounds/#{file_name}", :top => 0, :left => 0) 233 | end 234 | 235 | def play(sound_name) 236 | stop_all 237 | @sounds[sound_name].play 238 | end 239 | 240 | def playing? 241 | @sounds.find { |sound_name, sound| sound.playing? } 242 | end 243 | 244 | def stop_all 245 | @sounds.each do |sound_name, sound| 246 | sound.stop if sound.playing? 247 | end 248 | end 249 | end 250 | 251 | Shoes.app do 252 | @sounds = Sounds.new self 253 | @sounds.add_sound :collect, 'collect.mp3' 254 | @sounds.add_sound :death, 'death.mp3' 255 | 256 | def setup_game(app) 257 | app.background '#000' 258 | 259 | @snake = Snake.new app, 25, 25 260 | @board = GameBoard.new app, @snake 261 | 262 | stroke '#fff' 263 | 264 | # Add some food at random positions 265 | 50.times do 266 | @board.add_food *@board.random_coordinate 267 | end 268 | 269 | 50.times do 270 | @board.add_brick *@board.random_coordinate 271 | end 272 | 273 | @board.add_brick_border 274 | 275 | # Add the score text 276 | @score = para "Score: 0", :top => 5, :left => 5, :stroke => '#fff' 277 | end 278 | 279 | setup_game self 280 | 281 | @anim = animate 5 do 282 | keypress do |key| 283 | case key 284 | when :up, :down, :left, :right 285 | @snake.change_direction key 286 | when 'r' 287 | if @snake.dead 288 | clear 289 | setup_game self 290 | @anim.start 291 | end 292 | end 293 | end 294 | 295 | @snake.move 296 | 297 | if @board.food_eaten? 298 | @board.eat_food 299 | @score.text = "Score: #{@board.score}" 300 | @sounds.play :collect 301 | elsif @board.crashed_into_brick? or @board.crashed_into_self? 302 | @snake.dead = true 303 | @anim.stop 304 | @sounds.play :death 305 | banner "Game Over", :top => 190, :left => 180, :stroke => '#fff', :fill => '#000', :size => 32 306 | para "Press 'r' to play again", :top => 250, :left => 210, :stroke => '#fff', :fill => '#000' 307 | end 308 | end 309 | end 310 | --------------------------------------------------------------------------------