RubyMan 0.0.2
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/.gitignore +15 -0
- data/Gemfile +6 -0
- data/Rakefile +2 -0
- data/RubyMan.gemspec +24 -0
- data/bin/rubyman +4 -0
- data/lib/ruby_man.rb +5 -0
- data/lib/ruby_man/GameObjects/door.rb +11 -0
- data/lib/ruby_man/GameObjects/food.rb +24 -0
- data/lib/ruby_man/GameObjects/game_object.rb +29 -0
- data/lib/ruby_man/GameObjects/game_over.rb +44 -0
- data/lib/ruby_man/GameObjects/ghost.rb +123 -0
- data/lib/ruby_man/GameObjects/ghost_corpse.rb +30 -0
- data/lib/ruby_man/GameObjects/grid_object.rb +69 -0
- data/lib/ruby_man/GameObjects/hud.rb +26 -0
- data/lib/ruby_man/GameObjects/menu.rb +82 -0
- data/lib/ruby_man/GameObjects/moving_object.rb +50 -0
- data/lib/ruby_man/GameObjects/player.rb +90 -0
- data/lib/ruby_man/GameObjects/super_food.rb +16 -0
- data/lib/ruby_man/GameObjects/wall.rb +24 -0
- data/lib/ruby_man/aabb.rb +17 -0
- data/lib/ruby_man/direction.rb +102 -0
- data/lib/ruby_man/game_main.rb +213 -0
- data/lib/ruby_man/level_loader.rb +49 -0
- data/lib/ruby_man/resource_manager.rb +103 -0
- data/lib/ruby_man/sprite.rb +26 -0
- data/lib/ruby_man/version.rb +4 -0
- data/media/door_strip2.png +0 -0
- data/media/gem_strip9.png +0 -0
- data/media/ghost_death_strip15.png +0 -0
- data/media/ghost_down_strip2.png +0 -0
- data/media/ghost_left_strip2.png +0 -0
- data/media/ghost_right_strip2.png +0 -0
- data/media/ghost_scared_strip2.png +0 -0
- data/media/ghost_stopscared_strip3.png +0 -0
- data/media/ghost_up_strip2.png +0 -0
- data/media/levellist.txt +4 -0
- data/media/levels/level_1.txt +19 -0
- data/media/levels/level_2.txt +19 -0
- data/media/levels/level_3.txt +19 -0
- data/media/levels/level_4.txt +19 -0
- data/media/levels/level_empty.txt +19 -0
- data/media/logo.png +0 -0
- data/media/resource.cfg +18 -0
- data/media/rubyman_strip11.png +0 -0
- data/media/sfx/pacman_beginning.wav +0 -0
- data/media/sfx/pacman_chomp.wav +0 -0
- data/media/sfx/pacman_death.wav +0 -0
- data/media/sfx/pacman_eatghost.wav +0 -0
- data/media/walls_strip16.png +0 -0
- data/test/aabb_spec.rb +41 -0
- data/test/direction_spec.rb +48 -0
- data/test/game_main_spec.rb +126 -0
- metadata +142 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'ruby_man/GameObjects/grid_object'
|
2
|
+
require 'ruby_man/direction'
|
3
|
+
|
4
|
+
module RubyMan
|
5
|
+
# GridObject with the ability to move smoothly in 4 directions
|
6
|
+
class MovingObject < GridObject
|
7
|
+
attr_accessor :direction, :phase, :speed
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super
|
11
|
+
@direction = Direction.none
|
12
|
+
@phase = 0
|
13
|
+
@speed = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def update(delta)
|
17
|
+
super
|
18
|
+
@phase += @speed * delta
|
19
|
+
while @phase > 1
|
20
|
+
@phase -= 1
|
21
|
+
@grid_x += @direction.rel_x
|
22
|
+
@grid_y += @direction.rel_y
|
23
|
+
grid_step
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def grid_step
|
28
|
+
end
|
29
|
+
|
30
|
+
def collision(_other)
|
31
|
+
end
|
32
|
+
|
33
|
+
def change_direction(direction)
|
34
|
+
@direction = direction
|
35
|
+
@image_angle = direction.to_angle
|
36
|
+
end
|
37
|
+
|
38
|
+
def direction_free(direction)
|
39
|
+
@game.place_free(@grid_x + direction.rel_x, @grid_y + direction.rel_y)
|
40
|
+
end
|
41
|
+
|
42
|
+
def pixel_x
|
43
|
+
(@grid_x + @direction.rel_x * @phase) * 32
|
44
|
+
end
|
45
|
+
|
46
|
+
def pixel_y
|
47
|
+
(@grid_y + @direction.rel_y * @phase) * 32
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'ruby_man/GameObjects/moving_object'
|
3
|
+
require 'ruby_man/GameObjects/food'
|
4
|
+
require 'ruby_man/GameObjects/ghost'
|
5
|
+
require 'ruby_man/GameObjects/door'
|
6
|
+
|
7
|
+
module RubyMan
|
8
|
+
# player (RubyMan) object
|
9
|
+
class Player < MovingObject
|
10
|
+
def created
|
11
|
+
self.sprite = Sprite['rubyman']
|
12
|
+
@image_speed = 3
|
13
|
+
@next_direction = Direction.none
|
14
|
+
@speed = 3
|
15
|
+
@is_dead = false
|
16
|
+
@game.player = self
|
17
|
+
end
|
18
|
+
|
19
|
+
def button_down(id)
|
20
|
+
case id
|
21
|
+
when Gosu::KbLeft then @next_direction = Direction.left
|
22
|
+
when Gosu::KbUp then @next_direction = Direction.up
|
23
|
+
when Gosu::KbRight then @next_direction = Direction.right
|
24
|
+
when Gosu::KbDown then @next_direction = Direction.down
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(delta)
|
29
|
+
dead if @is_dead
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def grid_step
|
34
|
+
eat_food
|
35
|
+
change_direction(@next_direction)
|
36
|
+
@direction = Direction.none unless direction_free(@direction)
|
37
|
+
end
|
38
|
+
|
39
|
+
def direction_free(direction)
|
40
|
+
x = @grid_x + direction.rel_x
|
41
|
+
y = @grid_y + direction.rel_y
|
42
|
+
@game.objects_at(x, y).each do |obj|
|
43
|
+
return false if obj.solid || obj.is_a?(Door)
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def collision(obj)
|
49
|
+
return unless obj.is_a?(Ghost)
|
50
|
+
return unless distance_to(obj) <= 16
|
51
|
+
if obj.scared?
|
52
|
+
obj.die
|
53
|
+
else
|
54
|
+
die
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def die
|
59
|
+
ResourceManager['sfx_death'].play
|
60
|
+
@game.pause(2000)
|
61
|
+
@is_dead = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def dead
|
65
|
+
if @game.lives > 0
|
66
|
+
@game.lives -= 1
|
67
|
+
@game.restart
|
68
|
+
else
|
69
|
+
@game.game_over
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def eat_food
|
74
|
+
@game.objects_at(@grid_x, @grid_y).each do |obj|
|
75
|
+
next unless obj.is_a?(Food)
|
76
|
+
obj.destroy
|
77
|
+
@game.score += 10
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def change_direction(direction)
|
82
|
+
dir_x = @grid_x + direction.rel_x
|
83
|
+
dir_y = @grid_y + direction.rel_y
|
84
|
+
@game.objects_at(dir_x, dir_y).each do |obj|
|
85
|
+
return if obj.solid || obj.is_a?(Door)
|
86
|
+
end
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'ruby_man/GameObjects/food'
|
2
|
+
|
3
|
+
module RubyMan
|
4
|
+
# food that makes ghosts vulnerable
|
5
|
+
class SuperFood < Food
|
6
|
+
def created
|
7
|
+
super
|
8
|
+
@scale = 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroyed
|
12
|
+
super
|
13
|
+
@game.moving_objects.select { |obj| obj.is_a?(Ghost) }.each(&:scare)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'ruby_man/GameObjects/grid_object'
|
2
|
+
|
3
|
+
module RubyMan
|
4
|
+
# impassable wall
|
5
|
+
class Wall < GridObject
|
6
|
+
def initialize(type)
|
7
|
+
super()
|
8
|
+
self.sprite = Sprite['walls']
|
9
|
+
@image_index = type
|
10
|
+
@image_speed = 0
|
11
|
+
@solid = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.chars
|
15
|
+
'+T34F/\\JL-|oAV<>'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_char(char)
|
19
|
+
index = chars.index(char)
|
20
|
+
return if index.nil?
|
21
|
+
Wall.new(index)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RubyMan
|
2
|
+
# Axis-Aligned Bounding Box
|
3
|
+
class AABB
|
4
|
+
attr_reader :x_beg, :y_beg
|
5
|
+
attr_reader :x_end, :y_end
|
6
|
+
|
7
|
+
def initialize(x_beg, y_beg, x_end, y_end)
|
8
|
+
@x_beg, @y_beg = x_beg, y_beg
|
9
|
+
@x_end, @y_end = x_end, y_end
|
10
|
+
end
|
11
|
+
|
12
|
+
def intersects?(other)
|
13
|
+
@x_beg < other.x_end && @x_end > other.x_beg &&
|
14
|
+
@y_beg < other.y_end && @y_end > other.y_beg
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module RubyMan
|
2
|
+
# Class representing left/right/up/down/none direction in 2D space
|
3
|
+
class Direction
|
4
|
+
def initialize(dir = :none)
|
5
|
+
@direction = dir
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.none
|
9
|
+
Direction.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.left
|
13
|
+
Direction.new(:left)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.right
|
17
|
+
Direction.new(:right)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.up
|
21
|
+
Direction.new(:up)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
Direction.new(:down)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.each
|
29
|
+
return to_enum unless block_given?
|
30
|
+
yield Direction.right
|
31
|
+
yield Direction.down
|
32
|
+
yield Direction.left
|
33
|
+
yield Direction.up
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_i
|
37
|
+
case @direction
|
38
|
+
when :right
|
39
|
+
return 0
|
40
|
+
when :down
|
41
|
+
return 1
|
42
|
+
when :left
|
43
|
+
return 2
|
44
|
+
when :up
|
45
|
+
return 3
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.from_i(num)
|
50
|
+
case num % 4
|
51
|
+
when 0
|
52
|
+
return Direction.right
|
53
|
+
when 1
|
54
|
+
return Direction.down
|
55
|
+
when 2
|
56
|
+
return Direction.left
|
57
|
+
when 3
|
58
|
+
return Direction.up
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def rel_x
|
63
|
+
i = to_i
|
64
|
+
return 0 unless i
|
65
|
+
(1 - i) * ((i + 1) % 2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def rel_y
|
69
|
+
i = to_i
|
70
|
+
return 0 unless i
|
71
|
+
(2 - i) * (i % 2)
|
72
|
+
end
|
73
|
+
|
74
|
+
def opposite
|
75
|
+
i = to_i
|
76
|
+
return Direction.none unless i
|
77
|
+
Direction.from_i(to_i + 2)
|
78
|
+
end
|
79
|
+
|
80
|
+
def inverted
|
81
|
+
Direction.each.select { |dir| dir != self }
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_angle
|
85
|
+
i = to_i
|
86
|
+
return 0 unless i
|
87
|
+
i * 90
|
88
|
+
end
|
89
|
+
|
90
|
+
def ==(other)
|
91
|
+
to_i == other.to_i
|
92
|
+
end
|
93
|
+
|
94
|
+
def eql?(other)
|
95
|
+
self == other
|
96
|
+
end
|
97
|
+
|
98
|
+
def hash
|
99
|
+
@direction.hash
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'ruby_man/resource_manager'
|
3
|
+
require 'ruby_man/level_loader'
|
4
|
+
require 'ruby_man/GameObjects/menu'
|
5
|
+
require 'ruby_man/GameObjects/game_over'
|
6
|
+
|
7
|
+
module RubyMan
|
8
|
+
# Game window and the game itself
|
9
|
+
class GameMain < Gosu::Window
|
10
|
+
include LevelLoader
|
11
|
+
|
12
|
+
attr_reader :x_res, :y_res
|
13
|
+
attr_accessor :lives, :score
|
14
|
+
attr_reader :scores
|
15
|
+
attr_accessor :player
|
16
|
+
attr_reader :objects, :moving_objects
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@x_res, @y_res = 800, 640
|
20
|
+
super(@x_res, @y_res, false)
|
21
|
+
@resmgr = ResourceManager.new(self)
|
22
|
+
@objects, @moving_objects = [], []
|
23
|
+
@last_milliseconds = Gosu.milliseconds
|
24
|
+
@food_left = 0
|
25
|
+
@pause_time = -0.5
|
26
|
+
self.caption = 'RubyMan'
|
27
|
+
load_scores
|
28
|
+
load_level_list
|
29
|
+
main_menu
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_scores
|
33
|
+
@config_file_name = "#{Dir.home}/.rubyman.cfg"
|
34
|
+
@scores = []
|
35
|
+
@score = 0
|
36
|
+
return unless File.exist?(@config_file_name)
|
37
|
+
File.open(@config_file_name).each do |line|
|
38
|
+
entry = line.strip.split(';')
|
39
|
+
entry[2] = entry[2].to_i
|
40
|
+
@scores.push(entry)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def save_scores
|
45
|
+
file = File.open("#{Dir.home}/.rubyman.cfg", 'w')
|
46
|
+
count = 0
|
47
|
+
@scores.each do |entry|
|
48
|
+
count += 1
|
49
|
+
break if count > 10
|
50
|
+
line = "#{entry[0]};#{entry[1]};#{entry[2]}"
|
51
|
+
file.puts(line)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_score(name)
|
56
|
+
entry = [name, Time.new.strftime('%Y-%m-%d %H:%M'), @score]
|
57
|
+
@scores.push(entry)
|
58
|
+
sort_scores
|
59
|
+
save_scores
|
60
|
+
end
|
61
|
+
|
62
|
+
def sort_scores
|
63
|
+
@scores.sort! do |a, b|
|
64
|
+
a[2] <=> b[2]
|
65
|
+
end
|
66
|
+
@scores.reverse!
|
67
|
+
@scores = @scores.first(10)
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_level_list
|
71
|
+
@levels = []
|
72
|
+
@level = 0
|
73
|
+
File.open(ResourceManager.media('levellist.txt')).each do |line|
|
74
|
+
@levels.push(line.strip)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def start
|
79
|
+
@lives = 3
|
80
|
+
@score = 0
|
81
|
+
@level = -1
|
82
|
+
next_level
|
83
|
+
end
|
84
|
+
|
85
|
+
def restart
|
86
|
+
persistent = @objects.select { |obj| obj.class.persistent }
|
87
|
+
persistent_moving = @moving_objects.select { |obj| obj.class.persistent }
|
88
|
+
load_level(@level, true)
|
89
|
+
@objects += persistent
|
90
|
+
@moving_objects += persistent_moving
|
91
|
+
pause(1000)
|
92
|
+
end
|
93
|
+
|
94
|
+
def next_level
|
95
|
+
if @level < @levels.size - 1
|
96
|
+
@level += 1
|
97
|
+
load_level(@level, false)
|
98
|
+
ResourceManager['sfx_begin'].play
|
99
|
+
pause(3000)
|
100
|
+
else
|
101
|
+
game_over
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def end_level
|
106
|
+
@objects.clear
|
107
|
+
@moving_objects.clear
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_level(num, restart)
|
111
|
+
end_level
|
112
|
+
load_level_file(@levels[num], restart)
|
113
|
+
end
|
114
|
+
|
115
|
+
def level_loaded
|
116
|
+
# reevaluate ghost directions after level is fully loaded
|
117
|
+
moving_objects.each { |obj| obj.select_direction! if obj.is_a?(Ghost) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def game_over
|
121
|
+
end_level
|
122
|
+
add_object(GameOver.new)
|
123
|
+
end
|
124
|
+
|
125
|
+
def main_menu
|
126
|
+
end_level
|
127
|
+
add_object(Menu.new)
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_object(obj)
|
131
|
+
@objects.push(obj)
|
132
|
+
obj.game = self
|
133
|
+
@moving_objects.push(obj) if obj.is_a?(MovingObject)
|
134
|
+
obj.created
|
135
|
+
end
|
136
|
+
|
137
|
+
def destroy_object(obj)
|
138
|
+
obj.destroyed
|
139
|
+
@objects.delete(obj)
|
140
|
+
@moving_objects.delete(obj) if obj.is_a?(MovingObject)
|
141
|
+
end
|
142
|
+
|
143
|
+
def button_down(id)
|
144
|
+
close if id == Gosu::KbEscape
|
145
|
+
pause if id == Gosu::KbSpace
|
146
|
+
@objects.each { |obj| obj.button_down(id) }
|
147
|
+
end
|
148
|
+
|
149
|
+
def pause(ms = -1000.0)
|
150
|
+
if paused
|
151
|
+
@pause_time = -0.5
|
152
|
+
else
|
153
|
+
@pause_time = ms / 1000.0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def paused
|
158
|
+
return true if @pause_time > 0 || @pause_time == -1
|
159
|
+
false
|
160
|
+
end
|
161
|
+
|
162
|
+
def update
|
163
|
+
current_time = Gosu.milliseconds / 1000.0
|
164
|
+
delta = [current_time - @last_milliseconds, 0.25].min
|
165
|
+
@last_milliseconds = current_time
|
166
|
+
|
167
|
+
update_delta(delta)
|
168
|
+
end
|
169
|
+
|
170
|
+
def update_delta(delta)
|
171
|
+
@pause_time -= delta if @pause_time > 0
|
172
|
+
|
173
|
+
return if paused
|
174
|
+
@objects.each { |obj| obj.update(delta) }
|
175
|
+
check_collisions
|
176
|
+
end
|
177
|
+
|
178
|
+
def draw
|
179
|
+
super
|
180
|
+
@objects.each(&:draw)
|
181
|
+
end
|
182
|
+
|
183
|
+
def check_collisions
|
184
|
+
@moving_objects.each do |obj1|
|
185
|
+
bbox = obj1.bbox
|
186
|
+
@moving_objects.each do |obj2|
|
187
|
+
obj2.collision(obj1) if bbox.intersects?(obj2.bbox)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def place_free(x, y)
|
193
|
+
objects_at(x, y).each { |obj| return false if obj.solid }
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
def objects_at(x, y)
|
198
|
+
@objects.select do |obj|
|
199
|
+
next unless obj.is_a?(GridObject)
|
200
|
+
obj.grid_x == x && obj.grid_y == y
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def food_created
|
205
|
+
@food_left += 1
|
206
|
+
end
|
207
|
+
|
208
|
+
def food_destroyed
|
209
|
+
@food_left -= 1
|
210
|
+
next_level if @food_left == 0
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|