snakegame 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +30 -0
- data/Rakefile +6 -0
- data/bin/snake +4 -0
- data/board.rb +61 -0
- data/lib/board.rb +56 -0
- data/lib/game.rb +29 -0
- data/lib/point.rb +36 -0
- data/lib/snake.rb +113 -0
- data/test/board_test.rb +11 -0
- data/test/snake_test.rb +17 -0
- data/test/test_helper.rb +7 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1374a26425313391ad96b5778f0bdb4e033e8348
|
4
|
+
data.tar.gz: a9f34aa408f67733705f5e1d4dcc503f0925c3ef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 34ab19addd0ec055b412f0a1727def6c77d3d38108c85fd4297b8e178008382a43a0f1e9e8f105b7cdb20d3c6dce19e8202fb024dd5d2821e36ab679ec891df3
|
7
|
+
data.tar.gz: 0dce5b151508924eff1136f6110dd25e60e75f4a673bab0a9a034ccca184aa90ce1e2bc07b0d8978a0f213c7a6098ac46aec2844bc5332fef2700b624b979c65
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.1.0)
|
5
|
+
curses (1.0.1)
|
6
|
+
method_source (0.8.2)
|
7
|
+
minitest (5.8.1)
|
8
|
+
pry (0.10.2)
|
9
|
+
coderay (~> 1.1.0)
|
10
|
+
method_source (~> 0.8.1)
|
11
|
+
slop (~> 3.4)
|
12
|
+
pry-doc (0.8.0)
|
13
|
+
pry (~> 0.9)
|
14
|
+
yard (~> 0.8)
|
15
|
+
rake (10.4.2)
|
16
|
+
slop (3.6.0)
|
17
|
+
yard (0.8.7.6)
|
18
|
+
|
19
|
+
PLATFORMS
|
20
|
+
ruby
|
21
|
+
|
22
|
+
DEPENDENCIES
|
23
|
+
curses
|
24
|
+
minitest
|
25
|
+
pry
|
26
|
+
pry-doc
|
27
|
+
rake
|
28
|
+
|
29
|
+
BUNDLED WITH
|
30
|
+
1.10.5
|
data/Rakefile
ADDED
data/bin/snake
ADDED
data/board.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class Board
|
2
|
+
class CursesBoard
|
3
|
+
attr_accessor :win
|
4
|
+
def initialize
|
5
|
+
Curses.stdscr.keypad(true)
|
6
|
+
@win = Curses::Window.new(height, width, 0, 0)
|
7
|
+
Curses.stdscr.keypad(true)
|
8
|
+
@win.box('|', '-')
|
9
|
+
Curses.init_screen
|
10
|
+
Curses.curs_set(0)
|
11
|
+
Curses.crmode
|
12
|
+
Curses.noecho
|
13
|
+
Curses.stdscr.nodelay = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
attr_accessor :width, :height
|
17
|
+
def initialize(width: 60, height: 30)
|
18
|
+
@top_right = Point.new(width, 0)
|
19
|
+
@bottom_left = Point.new(0, height)
|
20
|
+
|
21
|
+
@width = width
|
22
|
+
@height = height
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize_snake
|
26
|
+
x, y = @width / 2, @height / 2
|
27
|
+
# TODO Make this adjust based on survival likelihood
|
28
|
+
return { starting_position: Point.new(x, y), vector: Point.new(0, 1) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_legal?(positions)
|
32
|
+
positions.all? do |position|
|
33
|
+
position.x > @bottom_left.x &&
|
34
|
+
position.x < @top_right.x - 1 &&
|
35
|
+
position.y > @top_right.y &&
|
36
|
+
position.y < @bottom_left.y - 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def draw(snake:, fruit:)
|
41
|
+
@curses_board ||= CursesBoard.new(@height, @width)
|
42
|
+
@curses_board.win.clear
|
43
|
+
@curses_board.win.box('|', '-')
|
44
|
+
Curses.init_screen
|
45
|
+
|
46
|
+
current_pos = snake.head
|
47
|
+
@curses_board.win.setpos(*current_pos.graphics_to_a)
|
48
|
+
@curses_board.win << snake.head_direction
|
49
|
+
snake.body.each do |body_part|
|
50
|
+
@curses_board.win.setpos(*body_part.graphics_to_a)
|
51
|
+
@curses_board.win << 'o'
|
52
|
+
end
|
53
|
+
@curses_board.win.setpos(*fruit.graphics_to_a)
|
54
|
+
@curses_board.win << '🍎'
|
55
|
+
@curses_board.win.refresh
|
56
|
+
end
|
57
|
+
|
58
|
+
def random_position
|
59
|
+
Point.new(Random.rand(width - 2) + 1, Random.rand(height - 2) + 1)
|
60
|
+
end
|
61
|
+
end
|
data/lib/board.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
class Board
|
2
|
+
attr_accessor :width, :height
|
3
|
+
def initialize(width: 60, height: 30)
|
4
|
+
@top_right = Point.new(width, 0)
|
5
|
+
@bottom_left = Point.new(0, height)
|
6
|
+
|
7
|
+
@width = width
|
8
|
+
@height = height
|
9
|
+
@_game_board = Hash.new {|hash, key| hash[key] = {}}
|
10
|
+
@win = Curses::Window.new(height, width, 0, 0)
|
11
|
+
Curses.stdscr.keypad(true)
|
12
|
+
@win.box('|', '-')
|
13
|
+
Curses.init_screen
|
14
|
+
Curses.curs_set(0)
|
15
|
+
Curses.crmode
|
16
|
+
Curses.noecho
|
17
|
+
Curses.stdscr.nodelay = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_snake
|
21
|
+
x, y = @width / 2, @height / 2
|
22
|
+
# TODO Make this adjust based on survival likelihood
|
23
|
+
@_game_board[x][y] = "^"
|
24
|
+
return { starting_position: Point.new(x, y), vector: Point.new(0, 1) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_legal?(positions)
|
28
|
+
positions.all? do |position|
|
29
|
+
position.x > @bottom_left.x &&
|
30
|
+
position.x < @top_right.x - 1 &&
|
31
|
+
position.y > @top_right.y &&
|
32
|
+
position.y < @bottom_left.y - 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def draw(snake:, fruit:)
|
37
|
+
@win.clear
|
38
|
+
@win.box('|', '-')
|
39
|
+
Curses.init_screen
|
40
|
+
|
41
|
+
current_pos = snake.head
|
42
|
+
@win.setpos(*current_pos.graphics_to_a)
|
43
|
+
@win << snake.head_direction
|
44
|
+
snake.body.each do |body_part|
|
45
|
+
@win.setpos(*body_part.graphics_to_a)
|
46
|
+
@win << 'o'
|
47
|
+
end
|
48
|
+
@win.setpos(*fruit.graphics_to_a)
|
49
|
+
@win << '🍎'
|
50
|
+
@win.refresh
|
51
|
+
end
|
52
|
+
|
53
|
+
def random_position
|
54
|
+
Point.new(Random.rand(width - 2) + 1, Random.rand(height - 2) + 1)
|
55
|
+
end
|
56
|
+
end
|
data/lib/game.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'snake'
|
2
|
+
require_relative 'board'
|
3
|
+
require_relative 'point'
|
4
|
+
require 'curses'
|
5
|
+
|
6
|
+
class IllegalStateError < StandardError; end
|
7
|
+
|
8
|
+
class Game
|
9
|
+
def initialize
|
10
|
+
@board = Board.new
|
11
|
+
@snake = Snake.new(@board.initialize_snake)
|
12
|
+
end
|
13
|
+
def play
|
14
|
+
food = @board.random_position
|
15
|
+
while true
|
16
|
+
change = Curses.getch
|
17
|
+
@snake.process_direction(change)
|
18
|
+
begin
|
19
|
+
food = @snake.tick(food)
|
20
|
+
raise unless @board.is_legal?(@snake.positions)
|
21
|
+
rescue IllegalStateError
|
22
|
+
raise "You died!"
|
23
|
+
end
|
24
|
+
food ||= @board.random_position
|
25
|
+
@board.draw(snake: @snake, fruit: food)
|
26
|
+
sleep 0.1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/point.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Point
|
2
|
+
attr_reader :x, :y
|
3
|
+
def initialize(x, y)
|
4
|
+
@x = x
|
5
|
+
@y = y
|
6
|
+
end
|
7
|
+
|
8
|
+
def +(other)
|
9
|
+
Point.new(x + other.x, y + other.y)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
[x, y]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Yay y,x standard!
|
17
|
+
def graphics_to_a
|
18
|
+
[y, x]
|
19
|
+
end
|
20
|
+
|
21
|
+
def hash
|
22
|
+
[x.hash, y.hash].hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def eql?(other)
|
26
|
+
x == other.x && y == other.y
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
eql?(other)
|
31
|
+
end
|
32
|
+
|
33
|
+
def -(other)
|
34
|
+
Point.new(x - other.x, y - other.y)
|
35
|
+
end
|
36
|
+
end
|
data/lib/snake.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'forwardable'
|
3
|
+
require_relative 'point'
|
4
|
+
class Snake
|
5
|
+
class PreviousVectorRecord
|
6
|
+
attr_reader :vector, :remaining
|
7
|
+
def initialize(vector, remaining)
|
8
|
+
@vector = vector
|
9
|
+
@remaining = remaining
|
10
|
+
end
|
11
|
+
|
12
|
+
def decrement
|
13
|
+
@remaining -= 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def increment
|
17
|
+
@remaining += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
@remaining > 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class BodyPart
|
26
|
+
extend Forwardable
|
27
|
+
def_delegators(:@position, :x, :y, :graphics_to_a)
|
28
|
+
attr_accessor :position, :vector
|
29
|
+
def initialize(position, vector)
|
30
|
+
@position = position
|
31
|
+
@vector = vector
|
32
|
+
end
|
33
|
+
end
|
34
|
+
DIRECTION_HASH = {
|
35
|
+
# We are in graphics land, so 0, 0 is the origin,
|
36
|
+
# and the y axis goes down _positively_
|
37
|
+
Curses::KEY_UP => Point.new(0, -1),
|
38
|
+
Curses::KEY_DOWN => Point.new(0, 1),
|
39
|
+
Curses::KEY_RIGHT => Point.new(1, 0),
|
40
|
+
Curses::KEY_LEFT => Point.new(-1, 0)
|
41
|
+
}
|
42
|
+
attr_accessor :head, :body
|
43
|
+
|
44
|
+
def initialize(starting_position:, vector:)
|
45
|
+
@head = starting_position
|
46
|
+
@vector = vector
|
47
|
+
@current_direction = DIRECTION_HASH.invert[vector]
|
48
|
+
@body = []
|
49
|
+
@previous_vector_changes = {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def positions
|
53
|
+
[@head] + @body
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_body_part
|
57
|
+
if @body.empty?
|
58
|
+
@body << BodyPart.new(@head - @vector, @vector)
|
59
|
+
else
|
60
|
+
@body << BodyPart.new(@body.last.position - @body.last.vector, @body.last.vector)
|
61
|
+
end
|
62
|
+
@previous_vector_changes.values.each(&:increment)
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_food(food, next_head_position)
|
66
|
+
if next_head_position.eql? food
|
67
|
+
5.times { add_body_part }
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
food
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_body
|
74
|
+
@body.map do |part|
|
75
|
+
if new_vector_record = @previous_vector_changes[part.position]
|
76
|
+
part.vector = new_vector_record.vector
|
77
|
+
end
|
78
|
+
part.position = part.position + part.vector
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def tick(food)
|
83
|
+
next_head_position = head + @vector
|
84
|
+
raise IllegalStateError if body.any? {|part| part.position == next_head_position }
|
85
|
+
food = handle_food(food, next_head_position)
|
86
|
+
update_body
|
87
|
+
@head = next_head_position
|
88
|
+
@previous_vector_changes.values.each(&:decrement)
|
89
|
+
@previous_vector_changes.select! { |key, val| val.valid? }
|
90
|
+
food
|
91
|
+
end
|
92
|
+
|
93
|
+
def process_direction(direction)
|
94
|
+
return if direction.nil?
|
95
|
+
@current_direction = direction if DIRECTION_HASH.keys.include?(direction)
|
96
|
+
@vector = DIRECTION_HASH[direction] || @vector
|
97
|
+
5.times { add_body_part } if direction == 'a'
|
98
|
+
@previous_vector_changes[head] = PreviousVectorRecord.new(@vector, @body.length + 1)
|
99
|
+
end
|
100
|
+
|
101
|
+
def head_direction
|
102
|
+
case @current_direction
|
103
|
+
when Curses::KEY_UP
|
104
|
+
'^'
|
105
|
+
when Curses::KEY_DOWN
|
106
|
+
'v'
|
107
|
+
when Curses::KEY_RIGHT
|
108
|
+
'>'
|
109
|
+
when Curses::KEY_LEFT
|
110
|
+
'<'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/test/board_test.rb
ADDED
data/test/snake_test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Snake do
|
4
|
+
let(:point_one) { Point.new(5, 5) }
|
5
|
+
let(:point_two) { Point.new(3, 5) }
|
6
|
+
let(:snake) { Snake.new(starting_position: Point.new(5, 5), vector: Point.new(1, 1)) }
|
7
|
+
it 'tests handle_food with food' do
|
8
|
+
food = snake.handle_food(point_one, point_one)
|
9
|
+
assert food.nil?
|
10
|
+
assert snake.body.length > 1
|
11
|
+
end
|
12
|
+
it 'tests handle_food without food' do
|
13
|
+
food = snake.handle_food(point_one, point_two)
|
14
|
+
assert food == point_one
|
15
|
+
snake.body.length.must_equal 0
|
16
|
+
end
|
17
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snakegame
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Steger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-12 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Play snake!
|
14
|
+
email:
|
15
|
+
- mjsteger1@gmail.com
|
16
|
+
executables:
|
17
|
+
- snake
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".ruby-version"
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
24
|
+
- Rakefile
|
25
|
+
- bin/snake
|
26
|
+
- board.rb
|
27
|
+
- lib/board.rb
|
28
|
+
- lib/game.rb
|
29
|
+
- lib/point.rb
|
30
|
+
- lib/snake.rb
|
31
|
+
- test/board_test.rb
|
32
|
+
- test/snake_test.rb
|
33
|
+
- test/test_helper.rb
|
34
|
+
homepage: http://stegerwerks.org
|
35
|
+
licenses: []
|
36
|
+
metadata: {}
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 2.4.5
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: snake game with curses
|
57
|
+
test_files:
|
58
|
+
- test/board_test.rb
|
59
|
+
- test/snake_test.rb
|
60
|
+
- test/test_helper.rb
|
61
|
+
has_rdoc:
|