delve 0.0.7
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 +17 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +25 -0
- data/README.md +6 -0
- data/ROADMAP.md +92 -0
- data/Rakefile +25 -0
- data/bin/delve +15 -0
- data/delve.gemspec +17 -0
- data/doc/examples/ascii.rb +34 -0
- data/doc/examples/astar.rb +39 -0
- data/doc/examples/border.rb +13 -0
- data/doc/examples/cellular.rb +15 -0
- data/doc/examples/display.rb +27 -0
- data/doc/examples/engine.rb +29 -0
- data/doc/examples/menu.rb +22 -0
- data/doc/examples/noise.rb +37 -0
- data/doc/examples/progress.rb +25 -0
- data/doc/examples/rogue.rb +15 -0
- data/lib/delve.rb +109 -0
- data/lib/delve/component/collision.rb +17 -0
- data/lib/delve/component/movement.rb +78 -0
- data/lib/delve/component/position.rb +30 -0
- data/lib/delve/component/symbol.rb +23 -0
- data/lib/delve/display/curses_renderer.rb +63 -0
- data/lib/delve/display/display.rb +51 -0
- data/lib/delve/engine.rb +30 -0
- data/lib/delve/entity.rb +23 -0
- data/lib/delve/event_queue.rb +48 -0
- data/lib/delve/fov/discrete_shadowcasting.rb +78 -0
- data/lib/delve/fov/fov.rb +83 -0
- data/lib/delve/game.rb +22 -0
- data/lib/delve/generator/cellular.rb +111 -0
- data/lib/delve/generator/dungeon.rb +19 -0
- data/lib/delve/generator/map.rb +30 -0
- data/lib/delve/generator/noise.rb +33 -0
- data/lib/delve/generator/rogue.rb +403 -0
- data/lib/delve/input/curses_input.rb +28 -0
- data/lib/delve/input/input.rb +12 -0
- data/lib/delve/path/astar.rb +83 -0
- data/lib/delve/path/path.rb +70 -0
- data/lib/delve/scheduler/action_scheduler.rb +38 -0
- data/lib/delve/scheduler/scheduler.rb +37 -0
- data/lib/delve/scheduler/simple_scheduler.rb +21 -0
- data/lib/delve/screen_manager.rb +41 -0
- data/lib/delve/widgets/border.rb +44 -0
- data/lib/delve/widgets/key_value.rb +36 -0
- data/lib/delve/widgets/menu.rb +84 -0
- data/lib/delve/widgets/multi_line.rb +51 -0
- data/lib/delve/widgets/progress.rb +37 -0
- data/lib/delve/widgets/text.rb +37 -0
- data/lib/delve/widgets/viewport.rb +67 -0
- data/rot.js.LICENSE +25 -0
- data/templates/Gemfile.erb +4 -0
- data/templates/README.md.erb +22 -0
- data/templates/binfile.erb +21 -0
- data/templates/game_screen.rb.erb +51 -0
- data/templates/gemspec.erb +27 -0
- data/templates/loading_screen.rb.erb +46 -0
- data/templates/player_factory.rb.erb +19 -0
- data/templates/title_screen.rb.erb +42 -0
- data/templates/world.rb.erb +63 -0
- data/test/component/collision_test.rb +39 -0
- data/test/component/movement_test.rb +155 -0
- data/test/component/position_test.rb +43 -0
- data/test/component/symbol_test.rb +30 -0
- data/test/delve_test.rb +61 -0
- data/test/display/curses_renderer_test.rb +60 -0
- data/test/display/display_test.rb +86 -0
- data/test/engine_test.rb +36 -0
- data/test/entity_test.rb +48 -0
- data/test/event_queue_test.rb +86 -0
- data/test/game_test.rb +47 -0
- data/test/generator/dungeon_test.rb +21 -0
- data/test/generator/map_test.rb +31 -0
- data/test/generator/noise_test.rb +49 -0
- data/test/input/curses_input_test.rb +24 -0
- data/test/input/input_test.rb +21 -0
- data/test/path/astar_test.rb +22 -0
- data/test/path/path_test.rb +41 -0
- data/test/scheduler/action_scheduler_test.rb +54 -0
- data/test/scheduler/scheduler_test.rb +50 -0
- data/test/scheduler/simple_scheduler_test.rb +33 -0
- data/test/screen_manager_test.rb +92 -0
- data/test/widgets/border_test.rb +60 -0
- data/test/widgets/key_value_test.rb +68 -0
- data/test/widgets/menu_test.rb +103 -0
- data/test/widgets/multi_line_test.rb +73 -0
- data/test/widgets/progress_test.rb +96 -0
- data/test/widgets/text_test.rb +66 -0
- data/test/widgets/viewport_test.rb +154 -0
- metadata +193 -0
data/lib/delve/entity.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
class Entity
|
2
|
+
def initialize
|
3
|
+
@components = Hash.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def has?(component_id)
|
7
|
+
@components.keys.include? component_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(component)
|
11
|
+
raise 'Cannot add the same component more than once' if has?(component.id)
|
12
|
+
@components[component.id] = component
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(component_id)
|
16
|
+
return nil unless has?(component_id)
|
17
|
+
@components[component_id]
|
18
|
+
end
|
19
|
+
|
20
|
+
def act
|
21
|
+
get(:actor).act if has?(:actor)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class EventQueue
|
2
|
+
def initialize
|
3
|
+
@time = 0
|
4
|
+
@events = Array.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def time
|
8
|
+
@time
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(event, time)
|
12
|
+
raise 'Unable to add a nil event' unless event
|
13
|
+
raise 'Unable to schedule event with no time' unless time
|
14
|
+
|
15
|
+
i = @events.length
|
16
|
+
(0..@events.length - 1).each do |e|
|
17
|
+
if @events[e][:time] > time
|
18
|
+
i = e
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@events.insert(i, { :event => event, :time => time })
|
24
|
+
end
|
25
|
+
|
26
|
+
def get
|
27
|
+
return nil unless @events.length > 0
|
28
|
+
|
29
|
+
e = @events.shift
|
30
|
+
|
31
|
+
if e[:time] > 0
|
32
|
+
@time += e[:time]
|
33
|
+
@events.each { |x| x[:time] -= e[:time] }
|
34
|
+
end
|
35
|
+
e[:event]
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove(event)
|
39
|
+
index = @events.index { |e| e[:event] == event }
|
40
|
+
return false unless index
|
41
|
+
@events.delete_at index
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear
|
46
|
+
@events = Array.new
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'delve/fov/fov'
|
2
|
+
|
3
|
+
class DiscreteShadowCasting < FieldOfView
|
4
|
+
|
5
|
+
def initialize(options=Hash.new, block&)
|
6
|
+
super options, block&
|
7
|
+
end
|
8
|
+
|
9
|
+
def compute(x, y, radius)
|
10
|
+
raise 'Cannot call compute without a block' unless block_given?
|
11
|
+
center = @coords
|
12
|
+
map = @map
|
13
|
+
|
14
|
+
yield x, y, 0, nil
|
15
|
+
|
16
|
+
return nil unless light_passes? x, y
|
17
|
+
#TODO: need to create a light_passes? method on the base class, that calls into the bllock thats passed in. The block should be a class that exposes a light_passes? method
|
18
|
+
|
19
|
+
data = Arrray.new
|
20
|
+
|
21
|
+
a, b, cx, cy, blocks = nil
|
22
|
+
|
23
|
+
(1..radius-1).each do |r|
|
24
|
+
neighbours = get_circle x, y, r
|
25
|
+
angle = 360 / neighbours.length
|
26
|
+
|
27
|
+
(0..neighbours.length-1).each do |i|
|
28
|
+
cx = neighbours[i][0]
|
29
|
+
cy = neighbours[i][1]
|
30
|
+
a = angle * (i - 0.5)
|
31
|
+
b = a + angle
|
32
|
+
|
33
|
+
blocks = !light_passes? cx, cy
|
34
|
+
yield cx, cy, r, 1 if visible_coords a.floor, b.ceil, blocks, data
|
35
|
+
|
36
|
+
return nil if data.length == 2 and data[0] == 0 && data[1] == 360
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def visible_coords(a, b, blocks, data)
|
43
|
+
#TODO: How to deal with the arguments.calee section?
|
44
|
+
|
45
|
+
index = 0
|
46
|
+
index += 1 while index < data.length and data[index] < a
|
47
|
+
|
48
|
+
if index == data.length
|
49
|
+
if blocks
|
50
|
+
data << a
|
51
|
+
data << b
|
52
|
+
end
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
|
56
|
+
count = 0
|
57
|
+
|
58
|
+
if index % 2 == 0
|
59
|
+
while index < data.length and data[index] < b
|
60
|
+
index += 1
|
61
|
+
count += 1
|
62
|
+
end
|
63
|
+
|
64
|
+
return false if count == 0
|
65
|
+
|
66
|
+
if blocks
|
67
|
+
if count % 2 == 0
|
68
|
+
#TODO Splice needs to be rubified
|
69
|
+
data.splice index-count, count, b
|
70
|
+
else
|
71
|
+
data.splice index-count, count
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
return true
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class FieldOfView
|
2
|
+
|
3
|
+
def initialize(options=Hash.new, block&)
|
4
|
+
@light_passes = block&
|
5
|
+
@options = { :topology => :eight }
|
6
|
+
options.each { |k, v| @options[k] = v }
|
7
|
+
end
|
8
|
+
|
9
|
+
def compute(x, y, r, block&)
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def get_circle(cx, cy, r)
|
15
|
+
result = Array.new
|
16
|
+
dirs, count_factor, start_offset = nil
|
17
|
+
|
18
|
+
if @options[:topology] == :four
|
19
|
+
count_factor = 1
|
20
|
+
start_offset = [0, 1]
|
21
|
+
dirs = [
|
22
|
+
directions[:eight][7],
|
23
|
+
directions[:eight][1],
|
24
|
+
directions[:eight][3],
|
25
|
+
directions[:eight][5]
|
26
|
+
]
|
27
|
+
elsif @options[:topology] == :six
|
28
|
+
dirs = directions[:six]
|
29
|
+
count_factor = 1
|
30
|
+
start_offset = [-1, 1]
|
31
|
+
elsif @options[:topology] == :eight
|
32
|
+
dirs = directions[:four]
|
33
|
+
count_factor = 2
|
34
|
+
start_offset = [-1, 1]
|
35
|
+
end
|
36
|
+
|
37
|
+
x = cx + (start_offset[0] * r)
|
38
|
+
y = cy + (start_offset[1] * r)
|
39
|
+
|
40
|
+
(0..dirs.length-1).each do |i|
|
41
|
+
(0..((r*count_factor) - 1)).each do |j|
|
42
|
+
result << [x, y]
|
43
|
+
x += dirs[i][0]
|
44
|
+
y += dirs[i][1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def directions(v)
|
52
|
+
dirs = {
|
53
|
+
:four => [
|
54
|
+
[ 0, -1],
|
55
|
+
[ 1, 0],
|
56
|
+
[ 0, 1],
|
57
|
+
[-1, 0]
|
58
|
+
],
|
59
|
+
:eight => [
|
60
|
+
[ 0, -1],
|
61
|
+
[ 1, -1],
|
62
|
+
[ 1, 0],
|
63
|
+
[ 1, 1],
|
64
|
+
[ 0, 1],
|
65
|
+
[-1, 1],
|
66
|
+
[-1, 0],
|
67
|
+
[-1, -1]
|
68
|
+
],
|
69
|
+
:six => [
|
70
|
+
[-1, -1],
|
71
|
+
[ 1, -1],
|
72
|
+
[ 2, 0],
|
73
|
+
[ 1, 1],
|
74
|
+
[-1, 1],
|
75
|
+
[-2, 0]
|
76
|
+
]
|
77
|
+
}
|
78
|
+
|
79
|
+
dirs[v]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
data/lib/delve/game.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class Game
|
2
|
+
def initialize(display, screen_manager, input)
|
3
|
+
raise 'Unable to initialize game when display is nil' unless display
|
4
|
+
raise 'Unable to initialize game when screen manager is nil' unless screen_manager
|
5
|
+
raise 'Unable to initalize game when input is nil' unless input
|
6
|
+
@display = display
|
7
|
+
@screen_manager = screen_manager
|
8
|
+
@input = input
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
raise 'Unable to start game when screen_manager is empty' if @screen_manager.empty?
|
13
|
+
quit = false
|
14
|
+
while !quit
|
15
|
+
@screen_manager.render @display
|
16
|
+
@display.render
|
17
|
+
|
18
|
+
quit = @screen_manager.update @input
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'delve/generator/map'
|
2
|
+
|
3
|
+
class CellularGenerator < Map
|
4
|
+
|
5
|
+
def initialize(width=nil, height=nil, opts=Hash.new)
|
6
|
+
super width, height
|
7
|
+
@options = {
|
8
|
+
born: [5, 6, 7, 8],
|
9
|
+
survive: [4, 5, 6, 7, 8],
|
10
|
+
topology: :eight
|
11
|
+
}
|
12
|
+
|
13
|
+
set_options opts
|
14
|
+
|
15
|
+
@dirs = directions @options[:topology]
|
16
|
+
@map = fill 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def randomize(probablility)
|
20
|
+
(0..@width-1).each do |i|
|
21
|
+
(0..@height-1).each do |j|
|
22
|
+
@map[i][j] = rand < probablility ? 1 : 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_options(opts)
|
28
|
+
opts.keys.each { |key| @options[key] = opts[key] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def set(x, y, value)
|
32
|
+
@map[x][y] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate
|
36
|
+
new_map = fill 0
|
37
|
+
randomize 0.45
|
38
|
+
born = @options[:born]
|
39
|
+
survive = @options[:survive]
|
40
|
+
|
41
|
+
(0..@height-1).each do |j|
|
42
|
+
width_step = 1
|
43
|
+
width_start = 0
|
44
|
+
if @options[:topology] == 6
|
45
|
+
width_step = 2
|
46
|
+
width_start = j%2
|
47
|
+
end
|
48
|
+
|
49
|
+
width_start.step(@width-1, width_step) do |i|
|
50
|
+
curr = @map[i][j]
|
51
|
+
ncount = get_neighbours i, j
|
52
|
+
|
53
|
+
if curr > 0 and survive.index(ncount) != nil
|
54
|
+
new_map[i][j] = 1
|
55
|
+
elsif curr <= 0 and born.index(ncount) != nil
|
56
|
+
new_map[i][j] = 1
|
57
|
+
end
|
58
|
+
|
59
|
+
yield i, j, new_map[i][j]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@map = new_map
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def get_neighbours cx, cy
|
68
|
+
result = 0
|
69
|
+
@dirs.each do |dir|
|
70
|
+
x = cx + dir[0]
|
71
|
+
y = cy + dir[1]
|
72
|
+
|
73
|
+
next if x < 0 or x >= (@width - 1) or y < 0 or y >= (@height - 1)
|
74
|
+
result += @map[x][y] == 1 ? 1 :0
|
75
|
+
end
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
def directions(v)
|
81
|
+
dirs = {
|
82
|
+
:four => [
|
83
|
+
[ 0, -1],
|
84
|
+
[ 1, 0],
|
85
|
+
[ 0, 1],
|
86
|
+
[-1, 0]
|
87
|
+
],
|
88
|
+
:eight => [
|
89
|
+
[ 0, -1],
|
90
|
+
[ 1, -1],
|
91
|
+
[ 1, 0],
|
92
|
+
[ 1, 1],
|
93
|
+
[ 0, 1],
|
94
|
+
[-1, 1],
|
95
|
+
[-1, 0],
|
96
|
+
[-1, -1]
|
97
|
+
],
|
98
|
+
:six => [
|
99
|
+
[-1, -1],
|
100
|
+
[ 1, -1],
|
101
|
+
[ 2, 0],
|
102
|
+
[ 1, 1],
|
103
|
+
[-1, 1],
|
104
|
+
[-2, 0]
|
105
|
+
]
|
106
|
+
}
|
107
|
+
|
108
|
+
dirs[v]
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'delve/generator/map'
|
2
|
+
|
3
|
+
class Dungeon < Map
|
4
|
+
|
5
|
+
def initialize(width=nil, height=nil)
|
6
|
+
super width, height
|
7
|
+
@rooms = Array.new
|
8
|
+
@corridors = Array.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def rooms
|
12
|
+
@rooms
|
13
|
+
end
|
14
|
+
|
15
|
+
def corridors
|
16
|
+
@corridors
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Map
|
2
|
+
|
3
|
+
@@default_width = 80
|
4
|
+
@@default_height = 24
|
5
|
+
|
6
|
+
def initialize(width=nil, height=nil)
|
7
|
+
@width = width || @@default_width
|
8
|
+
@height = height || @@default_height
|
9
|
+
end
|
10
|
+
|
11
|
+
def width
|
12
|
+
@width
|
13
|
+
end
|
14
|
+
|
15
|
+
def height
|
16
|
+
@height
|
17
|
+
end
|
18
|
+
|
19
|
+
def fill(value)
|
20
|
+
map = Array.new
|
21
|
+
(0..@width-1).each do |x|
|
22
|
+
map << Array.new
|
23
|
+
(0..@height-1).each do
|
24
|
+
map[x] << value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
map
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'perlin_noise'
|
2
|
+
require 'delve/generator/map'
|
3
|
+
|
4
|
+
class Noise < Map
|
5
|
+
|
6
|
+
@@grains = {
|
7
|
+
:fine => 0.03,
|
8
|
+
:coarse => 0.1
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize width, height, grain
|
12
|
+
raise 'Cannot initialize noise generator when width is less than zero' if width < 0
|
13
|
+
raise 'Cannot initialize noise generator when height is less than zero' if height < 0
|
14
|
+
raise 'Cannot initialize noise generator when grain is not defined' unless grain
|
15
|
+
raise 'Cannot initialize noise generator with unknown grain' unless @@grains.include?(grain)
|
16
|
+
|
17
|
+
super width, height
|
18
|
+
|
19
|
+
@grain = @@grains[grain]
|
20
|
+
@inverse = 1/@grain
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate
|
24
|
+
noise = Perlin::Noise.new 2
|
25
|
+
|
26
|
+
0.step((@width-1) * @grain, @grain) do |x|
|
27
|
+
0.step((@height-1) * @grain, @grain).each do |y|
|
28
|
+
yield(x*@inverse, y*@inverse, noise[x, y])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|