ruby-perlin-2D-map-generator 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +135 -44
- data/lib/CLI/command.rb +80 -12
- data/lib/ansi_colours.rb +3 -0
- data/lib/biome.rb +10 -0
- data/lib/map.rb +22 -1
- data/lib/map_config.rb +34 -8
- data/lib/pathfinding/a_star_finder.rb +57 -0
- data/lib/pathfinding/grid.rb +78 -0
- data/lib/road_generator.rb +63 -0
- data/lib/tile.rb +62 -4
- metadata +29 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a4341c2583eba83e95d6ea4a9c1ee05a857bc1f5c77c6d3aeeba41f6995a656
|
4
|
+
data.tar.gz: 599b47f78c83dc6acd99dcde0e9aadbeb8d445fc7d9b6ef7ba8b1acbb3c15d7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bbabe795544e432d7cf4e5dac2fd6f2b14dd17239d3eb2a59013ce3655bcee1873ef4edb5e6048be372c474af04234e5f7ef33806b517de1b160ea5d59b3d62f
|
7
|
+
data.tar.gz: 246cc08e1413e89c6929eacf4201a7895e64595067367f02ed8e758a239e18b098186e16c1c8a85900e1519fa84eb5ace115d15e094894b62825664b1059e57d
|
data/README.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# Ruby Perlin 2D Map Generator
|
2
|
-
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/ruby-perlin-2D-map-generator.svg)](https://badge.fury.io/rb/ruby-perlin-2D-map-generator)
|
4
|
+
![CI Status](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/actions/workflows/main.yml/badge.svg)
|
5
|
+
![CodeQL](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/workflows/CodeQL/badge.svg)
|
6
|
+
[![Downloads](https://img.shields.io/gem/dt/ruby-perlin-2D-map-generator.svg?style=flat)](https://rubygems.org/gems/ruby-perlin-2D-map-generator)
|
7
|
+
|
8
|
+
A gem that procedurally generates seeded and customizable 2D map with optional roads using perlin noise.
|
3
9
|
|
4
10
|
Include the gem in your project, or use the executable from the command line.
|
5
11
|
|
@@ -28,6 +34,7 @@ gem install ruby-perlin-2D-map-generator
|
|
28
34
|
See Command line Usage for full customization, below are some examples. Alter the temperature, moisture or elevation seeds to alter these maps:
|
29
35
|
|
30
36
|
- Plains with random terrain evens: `ruby-perlin-2D-map-generator render`
|
37
|
+
- Plains with random terrain events and two roads: `ruby-perlin-2D-map-generator render --roads=2`
|
31
38
|
- Desert (increase temperature, decrease moisture): `ruby-perlin-2D-map-generator render --temp=100 --moisture=-100`
|
32
39
|
- Mountainous with lakes (increase elevation, increase moisture) `ruby-perlin-2D-map-generator render --elevation=25 --moisture=25`
|
33
40
|
- Islands (decreaes elevation, increase moisture): `ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25`
|
@@ -38,27 +45,61 @@ See Command line Usage for full customization, below are some examples. Alter th
|
|
38
45
|
--width=int The width of the generated map (default 128)
|
39
46
|
--height=int The height of the generated map (default 128)
|
40
47
|
|
48
|
+
--roads=int Add this many roads through the map,
|
49
|
+
starting and ending at edges
|
50
|
+
(default 0)
|
51
|
+
|
41
52
|
--hs=int The seed for a terrains height perlin generation
|
42
53
|
(default 10)
|
43
54
|
--ms=int The seed for a terrains moist perlin generation
|
44
55
|
(default 300)
|
45
56
|
--ts=int The seed for a terrains temperature perlin generation
|
46
57
|
(default 3000)
|
58
|
+
--rs=int The seed for generating roads
|
59
|
+
(default 100)
|
47
60
|
|
48
|
-
--elevation=float Adjust each generated elevation by this percent (
|
61
|
+
--elevation=float Adjust each generated elevation by this percent (-100 -
|
49
62
|
100) (default 0.0)
|
50
|
-
--moisture=float Adjust each generated moisture by this percent (
|
63
|
+
--moisture=float Adjust each generated moisture by this percent (-100 -
|
51
64
|
100) (default 0.0)
|
52
|
-
--temp=float Adjust each generated temperature by this percent (
|
65
|
+
--temp=float Adjust each generated temperature by this percent (-100
|
53
66
|
- 100) (default 0.0)
|
54
67
|
```
|
55
68
|
|
69
|
+
## Roads and the heuristic
|
70
|
+
Roads can be generated by providing a positive integer to the `roads=` argument. Roads are randomly seeded to begin
|
71
|
+
and start at an axis (but not the same axis).
|
72
|
+
|
73
|
+
A* pathfinding is used to generate the roads with a heuristic that uses manhattan distance, favours existing roads and similar elevations in adjacent tiles.
|
74
|
+
|
75
|
+
Roads can be configured to include/exclude generating paths thorugh water, mountains and flora.
|
76
|
+
|
77
|
+
Tiles containing roads are of type `road`, those without are of type `terrain`.
|
78
|
+
|
79
|
+
The `--roads_to_make` option allows you to specify multiple pairs of coordinates to attempt to build paths, subject to the heuristic and other option constraints. Expects a a single list, but must be sets of 4, example of two roads: `--roads_to_make=0,0,50,50,0,0,75,75`
|
80
|
+
|
56
81
|
# Generate without rendering
|
57
82
|
|
58
|
-
```
|
83
|
+
```irb
|
59
84
|
irb(main):001:0> map = Map.new
|
60
|
-
|
61
|
-
|
85
|
+
```
|
86
|
+
|
87
|
+
Map can then be manipulated via traditional x,y lookup
|
88
|
+
```irb
|
89
|
+
map[x, y].to_h
|
90
|
+
=>
|
91
|
+
{:x=>0,
|
92
|
+
:y=>1,
|
93
|
+
:height=>0.29251394359649563,
|
94
|
+
:moist=>0.29100678755603004,
|
95
|
+
:temp=>0.6034041566100443,
|
96
|
+
:biome=>{:name=>"deep_valley", :flora_range=>1, :colour=>"\e[48;5;47m"},
|
97
|
+
:items=>[]}
|
98
|
+
```
|
99
|
+
or the less intuitative multidimensional lookup (reversed axis):
|
100
|
+
|
101
|
+
```irb
|
102
|
+
map.tiles[y][x].to_h
|
62
103
|
=>
|
63
104
|
{:x=>0,
|
64
105
|
:y=>1,
|
@@ -69,6 +110,20 @@ irb(main):002:0> map.describe[1][0]
|
|
69
110
|
:items=>[]}
|
70
111
|
```
|
71
112
|
|
113
|
+
or from the command line:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
$ ruby-perlin-2D-map-generator describe coordinates=0,1
|
117
|
+
|
118
|
+
{:x=>0,
|
119
|
+
:y=>1,
|
120
|
+
:height=>0.29251394359649563,
|
121
|
+
:moist=>0.29100678755603004,
|
122
|
+
:temp=>0.6034041566100443,
|
123
|
+
:biome=>{:name=>"deep_valley", :flora_range=>1, :colour=>"\e[48;5;47m"},
|
124
|
+
:items=>[]}
|
125
|
+
```
|
126
|
+
|
72
127
|
# Full Command line Usage
|
73
128
|
```bash
|
74
129
|
$ ruby-perlin-2D-map-generator --help
|
@@ -76,50 +131,80 @@ $ ruby-perlin-2D-map-generator --help
|
|
76
131
|
```bash
|
77
132
|
Usage: ruby-perlin-2D-map-generator [OPTIONS] (DESCRIBE | RENDER)
|
78
133
|
|
79
|
-
Generate a seeded customizable procedurally generated 2D map.
|
134
|
+
Generate a seeded customizable procedurally generated 2D map with optional roads.
|
80
135
|
Rendered in the console using ansi colours, or described as a 2D array of
|
81
136
|
hashes with each tiles information.
|
82
137
|
|
83
138
|
Arguments:
|
84
139
|
(DESCRIBE | RENDER) command to run: render prints the map to standard
|
85
|
-
output using ansi colors
|
86
|
-
|
140
|
+
output using ansi colors. describe prints each tiles
|
141
|
+
bionome information in the map, can be combined with the
|
142
|
+
coordinates keyword to print a specific tile.
|
143
|
+
(permitted: describe, render)
|
144
|
+
|
145
|
+
Keywords:
|
146
|
+
COORDINATES=INT_LIST Used with the describe command, only returns the given
|
147
|
+
coordinate tile details
|
87
148
|
|
88
149
|
Options:
|
89
|
-
--elevation=float
|
90
|
-
|
91
|
-
--fhx=float
|
92
|
-
|
93
|
-
--fhy=float
|
94
|
-
|
95
|
-
--fmx=float
|
96
|
-
|
97
|
-
--fmy=float
|
98
|
-
|
99
|
-
--ftx=float
|
100
|
-
|
101
|
-
--fty=float
|
102
|
-
|
103
|
-
--gf=bool
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
--
|
109
|
-
|
110
|
-
--
|
111
|
-
|
112
|
-
--
|
113
|
-
|
114
|
-
--
|
115
|
-
|
116
|
-
--
|
117
|
-
|
118
|
-
--
|
119
|
-
|
120
|
-
--
|
121
|
-
|
122
|
-
--
|
150
|
+
--elevation=float Adjust each generated elevation by
|
151
|
+
this percent (-100 - 100) (default 0.0)
|
152
|
+
--fhx=float The frequency for height generation
|
153
|
+
across the x-axis (default 2.5)
|
154
|
+
--fhy=float The frequency for height generation
|
155
|
+
across the y-axis (default 2.5)
|
156
|
+
--fmx=float The frequency for moist generation
|
157
|
+
across the x-axis (default 2.5)
|
158
|
+
--fmy=float The frequency for moist generation
|
159
|
+
across the y-axis (default 2.5)
|
160
|
+
--ftx=float The frequency for temp generation
|
161
|
+
across the x-axis (default 2.5)
|
162
|
+
--fty=float The frequency for temp generation
|
163
|
+
across the y-axis (default 2.5)
|
164
|
+
--gf=bool Generate flora, significantly affects
|
165
|
+
performance
|
166
|
+
--height=int The height of the generated map
|
167
|
+
(default 128)
|
168
|
+
-h, --help Print usage
|
169
|
+
--hs=int The seed for a terrains height perlin
|
170
|
+
generation (default 10)
|
171
|
+
--moisture=float Adjust each generated moisture by
|
172
|
+
this percent (-100 - 100) (default 0.0)
|
173
|
+
--ms=int The seed for a terrains moist perlin
|
174
|
+
generation (default 300)
|
175
|
+
--oh=int Octaves for height generation
|
176
|
+
(default 3)
|
177
|
+
--om=int Octaves for moist generation (default
|
178
|
+
3)
|
179
|
+
--ot=int Octaves for temp generation (default
|
180
|
+
3)
|
181
|
+
--ph=float Persistance for height generation
|
182
|
+
(default 1.0)
|
183
|
+
--pm=float Persistance for moist generation
|
184
|
+
(default 1.0)
|
185
|
+
--pt=float Persistance for temp generation
|
186
|
+
(default 1.0)
|
187
|
+
--road_exclude_flora_path=bool Controls if roads will run tiles
|
188
|
+
containing flora
|
189
|
+
--road_exclude_mountain_path=bool Controls if roads will run through
|
190
|
+
high mountains
|
191
|
+
--road_exclude_water_path=bool Controls if roads will run through
|
192
|
+
water
|
193
|
+
--roads=int Add this many roads through the map,
|
194
|
+
starting and ending at edges (default
|
195
|
+
0)
|
196
|
+
--roads_to_make ints Attempt to create a road from a start
|
197
|
+
and end point (4 integers), can be
|
198
|
+
supplied multiple paths
|
199
|
+
(default [])
|
200
|
+
--rs=int The seed for generating roads
|
201
|
+
(default 100)
|
202
|
+
--temp=float Adjust each generated temperature by
|
203
|
+
this percent (-100 - 100) (default 0.0)
|
204
|
+
--ts=int The seed for a terrains temperature
|
205
|
+
perlin generation (default 3000)
|
206
|
+
--width=int The width of the generated map
|
207
|
+
(default 128)
|
123
208
|
|
124
209
|
Examples:
|
125
210
|
Render with defaults
|
@@ -127,4 +212,10 @@ Examples:
|
|
127
212
|
|
128
213
|
Render with options
|
129
214
|
$ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1
|
215
|
+
|
216
|
+
Render with roads
|
217
|
+
$ ruby-perlin-2D-map-generator render --roads=2
|
218
|
+
|
219
|
+
Describe tile [1, 1]
|
220
|
+
$ ruby-perlin-2D-map-generator describe coordinates=1,1
|
130
221
|
```
|
data/lib/CLI/command.rb
CHANGED
@@ -12,7 +12,7 @@ module CLI
|
|
12
12
|
|
13
13
|
no_command
|
14
14
|
|
15
|
-
desc 'Generate a seeded customizable procedurally generated 2D map. Rendered in the console ' \
|
15
|
+
desc 'Generate a seeded customizable procedurally generated 2D map with optional roads. Rendered in the console ' \
|
16
16
|
' using ansi colours, or described as a 2D array of hashes with each tiles information.'
|
17
17
|
|
18
18
|
example 'Render with defaults',
|
@@ -20,14 +20,37 @@ module CLI
|
|
20
20
|
|
21
21
|
example 'Render with options',
|
22
22
|
' $ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1'
|
23
|
+
|
24
|
+
example 'Render with roads',
|
25
|
+
' $ ruby-perlin-2D-map-generator render --roads=2'
|
26
|
+
|
27
|
+
example 'Describe tile [1, 1]',
|
28
|
+
' $ ruby-perlin-2D-map-generator describe coordinates=1,1'
|
23
29
|
end
|
24
30
|
|
25
31
|
argument :command do
|
26
32
|
name '(describe | render)'
|
27
33
|
arity one
|
28
|
-
|
29
|
-
desc 'command to run: render prints the map to standard output using ansi colors
|
30
|
-
'
|
34
|
+
permit %w[describe render]
|
35
|
+
desc 'command to run: render prints the map to standard output using ansi colors. ' \
|
36
|
+
'describe prints each tiles bionome information in the map, can be combined with ' \
|
37
|
+
'the coordinates keyword to print a specific tile.'
|
38
|
+
end
|
39
|
+
|
40
|
+
keyword :coordinates do
|
41
|
+
arity one
|
42
|
+
convert :int_list
|
43
|
+
validate ->(v) { v >= 0 }
|
44
|
+
desc 'Used with the describe command, only returns the given coordinate tile details'
|
45
|
+
end
|
46
|
+
|
47
|
+
option :roads_to_make do
|
48
|
+
arity one
|
49
|
+
long '--roads_to_make ints'
|
50
|
+
convert :int_list
|
51
|
+
validate ->(v) { v >= 0 }
|
52
|
+
desc 'Attempt to create a road from a start and end point (4 integers), can be supplied multiple paths'
|
53
|
+
default MapConfig::DEFAULT_ROADS_TO_MAKE
|
31
54
|
end
|
32
55
|
|
33
56
|
option :height_seed do
|
@@ -199,7 +222,7 @@ module CLI
|
|
199
222
|
long '--temp float'
|
200
223
|
long '--temp=float'
|
201
224
|
|
202
|
-
desc 'Adjust each generated temperature by this percent (
|
225
|
+
desc 'Adjust each generated temperature by this percent (-100 - 100)'
|
203
226
|
convert ->(val) { val.to_f / 100.0 }
|
204
227
|
validate ->(val) { val >= -1.0 && val <= 1.0 }
|
205
228
|
default MapConfig::DEFAULT_TEMP_ADJUSTMENT
|
@@ -209,7 +232,7 @@ module CLI
|
|
209
232
|
long '--elevation float'
|
210
233
|
long '--elevation=float'
|
211
234
|
|
212
|
-
desc 'Adjust each generated elevation by this percent (
|
235
|
+
desc 'Adjust each generated elevation by this percent (-100 - 100)'
|
213
236
|
convert ->(val) { val.to_f / 100.0 }
|
214
237
|
validate ->(val) { val >= -1.0 && val <= 1.0 }
|
215
238
|
default MapConfig::DEFAULT_HEIGHT_ADJUSTMENT
|
@@ -219,12 +242,58 @@ module CLI
|
|
219
242
|
long '--moisture float'
|
220
243
|
long '--moisture=float'
|
221
244
|
|
222
|
-
desc 'Adjust each generated moisture by this percent (
|
245
|
+
desc 'Adjust each generated moisture by this percent (-100 - 100)'
|
223
246
|
convert ->(val) { val.to_f / 100.0 }
|
224
247
|
validate ->(val) { val >= -1.0 && val <= 1.0 }
|
225
248
|
default MapConfig::DEFAULT_MOIST_ADJUSTMENT
|
226
249
|
end
|
227
250
|
|
251
|
+
option :roads do
|
252
|
+
long '--roads int'
|
253
|
+
long '--roads=int'
|
254
|
+
|
255
|
+
desc 'Add this many roads through the map, starting and ending at edges'
|
256
|
+
convert Integer
|
257
|
+
validate ->(val) { val >= 0 }
|
258
|
+
default MapConfig::DEFAULT_NUM_OF_ROADS
|
259
|
+
end
|
260
|
+
|
261
|
+
option :road_seed do
|
262
|
+
long '--rs int'
|
263
|
+
long '--rs=int'
|
264
|
+
|
265
|
+
desc 'The seed for generating roads'
|
266
|
+
convert Integer
|
267
|
+
default MapConfig::DEFAULT_ROAD_SEED
|
268
|
+
end
|
269
|
+
|
270
|
+
option :road_exclude_water_path do
|
271
|
+
long '--road_exclude_water_path bool'
|
272
|
+
long '--road_exclude_water_path=bool'
|
273
|
+
|
274
|
+
desc 'Controls if roads will run through water'
|
275
|
+
convert :bool
|
276
|
+
default MapConfig::DEFAULT_ROAD_EXCLUDE_WATER_PATH
|
277
|
+
end
|
278
|
+
|
279
|
+
option :road_exclude_mountain_path do
|
280
|
+
long '--road_exclude_mountain_path bool'
|
281
|
+
long '--road_exclude_mountain_path=bool'
|
282
|
+
|
283
|
+
desc 'Controls if roads will run through high mountains'
|
284
|
+
convert :bool
|
285
|
+
default MapConfig::DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH
|
286
|
+
end
|
287
|
+
|
288
|
+
option :road_exclude_flora_path do
|
289
|
+
long '--road_exclude_flora_path bool'
|
290
|
+
long '--road_exclude_flora_path=bool'
|
291
|
+
|
292
|
+
desc 'Controls if roads will run tiles containing flora'
|
293
|
+
convert :bool
|
294
|
+
default MapConfig::DEFAULT_ROAD_EXCLUDE_FLORA_PATH
|
295
|
+
end
|
296
|
+
|
228
297
|
flag :help do
|
229
298
|
short '-h'
|
230
299
|
long '--help'
|
@@ -248,14 +317,13 @@ module CLI
|
|
248
317
|
map = Map.new(map_config: MapConfig.new(
|
249
318
|
width: params[:width],
|
250
319
|
height: params[:height],
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
generate_flora: params[:generate_flora]
|
320
|
+
all_perlin_configs: MapConfig::AllPerlinConfigs.new(perlin_height_config, perlin_moist_config, perlin_temp_config),
|
321
|
+
generate_flora: params[:generate_flora],
|
322
|
+
road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path, :road_exclude_mountain_path, :road_exclude_flora_path, :roads_to_make).values)
|
255
323
|
))
|
256
324
|
case params[:command]
|
257
325
|
when 'render' then map.render
|
258
|
-
when 'describe' then puts map.describe
|
326
|
+
when 'describe' then puts(!params[:coordinates].nil? ? map[params[:coordinates][0], params[:coordinates][1]].to_h : map.describe)
|
259
327
|
end
|
260
328
|
end
|
261
329
|
|
data/lib/ansi_colours.rb
CHANGED
data/lib/biome.rb
CHANGED
@@ -32,6 +32,10 @@ class Biome
|
|
32
32
|
TAIGA_TERRAIN.include?(self)
|
33
33
|
end
|
34
34
|
|
35
|
+
def high_mountain?
|
36
|
+
HIGH_MOUNTAIN.include?(self)
|
37
|
+
end
|
38
|
+
|
35
39
|
def flora_available
|
36
40
|
!flora_range.nil?
|
37
41
|
end
|
@@ -132,6 +136,12 @@ class Biome
|
|
132
136
|
TAIGA_COAST
|
133
137
|
].freeze
|
134
138
|
|
139
|
+
HIGH_MOUNTAIN = [
|
140
|
+
SNOW,
|
141
|
+
ROCKS,
|
142
|
+
MOUNTAIN
|
143
|
+
].freeze
|
144
|
+
|
135
145
|
LAND_TERRAIN = (ALL_TERRAIN - WATER_TERRAIN).freeze
|
136
146
|
|
137
147
|
class << self
|
data/lib/map.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'map_tile_generator'
|
4
4
|
require 'map_config'
|
5
|
+
require 'road_generator'
|
5
6
|
|
6
7
|
class Map
|
7
8
|
attr_reader :config
|
@@ -21,7 +22,27 @@ class Map
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
25
|
+
# rubocop:disable Naming/MethodParameterName:
|
26
|
+
def [](x, y)
|
27
|
+
raise ArgumentError, 'coordinates out of bounds' if y.nil? || y >= tiles.size || x.nil? || x >= tiles[y].size
|
28
|
+
|
29
|
+
tiles[y][x]
|
30
|
+
end
|
31
|
+
# rubocop:enable Naming/MethodParameterName:
|
32
|
+
|
24
33
|
def tiles
|
25
|
-
@tiles
|
34
|
+
return @tiles if @tiles
|
35
|
+
|
36
|
+
@tiles = generate_tiles
|
37
|
+
road_generator = RoadGenerator.new(@tiles)
|
38
|
+
road_generator.generate_num_of_random_roads(config.road_config)
|
39
|
+
road_generator.generate_roads_from_coordinate_list(config.road_config.roads_to_make)
|
40
|
+
@tiles
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def generate_tiles
|
46
|
+
MapTileGenerator.new(map: self).generate
|
26
47
|
end
|
27
48
|
end
|
data/lib/map_config.rb
CHANGED
@@ -25,25 +25,43 @@ class MapConfig
|
|
25
25
|
DEFAULT_TEMP_X_FREQUENCY = 2.5
|
26
26
|
DEFAULT_TEMP_ADJUSTMENT = 0.0
|
27
27
|
|
28
|
+
DEFAULT_ROAD_SEED = 100
|
29
|
+
DEFAULT_NUM_OF_ROADS = 0
|
30
|
+
DEFAULT_ROAD_EXCLUDE_WATER_PATH = true
|
31
|
+
DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH = true
|
32
|
+
DEFAULT_ROAD_EXCLUDE_FLORA_PATH = true
|
33
|
+
DEFAULT_ROADS_TO_MAKE = [].freeze
|
34
|
+
|
28
35
|
PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze
|
29
|
-
|
36
|
+
ALL_PERLIN_CONFIGS = %i[perlin_height_config perlin_moist_config perlin_temp_config].freeze
|
37
|
+
ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path roads_to_make].freeze
|
30
38
|
|
31
|
-
|
39
|
+
PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS)
|
40
|
+
AllPerlinConfigs = Struct.new(*ALL_PERLIN_CONFIGS)
|
41
|
+
RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS)
|
32
42
|
|
33
|
-
|
34
|
-
height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA)
|
35
|
-
raise ArgumentError unless perlin_height_config.is_a?(PerlinConfig) && perlin_moist_config.is_a?(PerlinConfig)
|
43
|
+
attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :road_config
|
36
44
|
|
45
|
+
def initialize(all_perlin_configs: default_perlin_configs, width: DEFAULT_TILE_COUNT,
|
46
|
+
height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, road_config: default_road_config)
|
47
|
+
validate(all_perlin_configs)
|
37
48
|
@generate_flora = generate_flora
|
38
|
-
@perlin_height_config = perlin_height_config
|
39
|
-
@perlin_moist_config = perlin_moist_config
|
40
|
-
@perlin_temp_config = perlin_temp_config
|
49
|
+
@perlin_height_config = all_perlin_configs.perlin_height_config
|
50
|
+
@perlin_moist_config = all_perlin_configs.perlin_moist_config
|
51
|
+
@perlin_temp_config = all_perlin_configs.perlin_temp_config
|
41
52
|
@width = width
|
42
53
|
@height = height
|
54
|
+
@road_config = road_config
|
43
55
|
end
|
44
56
|
|
45
57
|
private
|
46
58
|
|
59
|
+
def validate(all_perlin_configs)
|
60
|
+
unless all_perlin_configs.perlin_height_config.is_a?(PerlinConfig) && all_perlin_configs.perlin_moist_config.is_a?(PerlinConfig) && all_perlin_configs.perlin_temp_config.is_a?(PerlinConfig)
|
61
|
+
raise ArgumentError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
47
65
|
def default_perlin_height_config
|
48
66
|
PerlinConfig.new(DEFAULT_TILE_COUNT, DEFAULT_TILE_COUNT, DEFAULT_HEIGHT_SEED, DEFAULT_HEIGHT_OCTAVES,
|
49
67
|
DEFAULT_HEIGHT_X_FREQUENCY, DEFAULT_HEIGHT_Y_FREQUENCY, DEFAULT_HEIGHT_PERSISTANCE, DEFAULT_HEIGHT_ADJUSTMENT)
|
@@ -58,4 +76,12 @@ class MapConfig
|
|
58
76
|
PerlinConfig.new(DEFAULT_TILE_COUNT, DEFAULT_TILE_COUNT, DEFAULT_TEMP_SEED, DEFAULT_TEMP_OCTAVES,
|
59
77
|
DEFAULT_TEMP_X_FREQUENCY, DEFAULT_TEMP_Y_FREQUENCY, DEFAULT_TEMP_PERSISTANCE, DEFAULT_TEMP_ADJUSTMENT)
|
60
78
|
end
|
79
|
+
|
80
|
+
def default_road_config
|
81
|
+
RoadConfig.new(DEFAULT_ROAD_SEED, DEFAULT_NUM_OF_ROADS, DEFAULT_ROAD_EXCLUDE_WATER_PATH, DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH, DEFAULT_ROAD_EXCLUDE_FLORA_PATH, DEFAULT_ROADS_TO_MAKE)
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_perlin_configs
|
85
|
+
AllPerlinConfigs.new(default_perlin_height_config, default_perlin_moist_config, default_perlin_temp_config)
|
86
|
+
end
|
61
87
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pathfinding
|
4
|
+
#
|
5
|
+
# An A* Pathfinder to build roads/paths between two coordinates containing
|
6
|
+
# different path costs, the heuristic behaviour that can be altered via configuration
|
7
|
+
#
|
8
|
+
class AStarFinder
|
9
|
+
def find_path(start_node, end_node, grid)
|
10
|
+
open_set = [start_node]
|
11
|
+
came_from = {}
|
12
|
+
g_score = { start_node => 0 }
|
13
|
+
f_score = { start_node => heuristic_cost_estimate(start_node, end_node) }
|
14
|
+
|
15
|
+
until open_set.empty?
|
16
|
+
current_node = open_set.min_by { |node| f_score[node] }
|
17
|
+
|
18
|
+
return reconstruct_path(came_from, current_node) if current_node == end_node
|
19
|
+
|
20
|
+
open_set.delete(current_node)
|
21
|
+
|
22
|
+
grid.neighbors(current_node).each do |neighbor|
|
23
|
+
tentative_g_score = g_score[current_node] + 1
|
24
|
+
|
25
|
+
next unless !g_score[neighbor] || tentative_g_score < g_score[neighbor]
|
26
|
+
|
27
|
+
came_from[neighbor] = current_node
|
28
|
+
g_score[neighbor] = tentative_g_score
|
29
|
+
f_score[neighbor] = g_score[neighbor] + heuristic_cost_estimate(neighbor, end_node)
|
30
|
+
|
31
|
+
open_set << neighbor unless open_set.include?(neighbor)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# No path found
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def heuristic_cost_estimate(node, end_node)
|
42
|
+
(node.x - end_node.x).abs +
|
43
|
+
(node.y - end_node.y).abs +
|
44
|
+
(node.path_heuristic - end_node.path_heuristic) + # elevation for natural roads
|
45
|
+
(node.road? ? 0 : 5) # share existing roads
|
46
|
+
end
|
47
|
+
|
48
|
+
def reconstruct_path(came_from, current_node)
|
49
|
+
path = [current_node]
|
50
|
+
while came_from[current_node]
|
51
|
+
current_node = came_from[current_node]
|
52
|
+
path.unshift(current_node)
|
53
|
+
end
|
54
|
+
path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pathfinding
|
4
|
+
#
|
5
|
+
# Responsible for manipulating and encapsulating behaviour of tiles related
|
6
|
+
# to pathfinding
|
7
|
+
#
|
8
|
+
class Grid
|
9
|
+
attr_reader :nodes
|
10
|
+
|
11
|
+
def initialize(nodes)
|
12
|
+
@nodes = nodes
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Naming/MethodParameterName:
|
16
|
+
def node(x, y)
|
17
|
+
nodes[y][x]
|
18
|
+
end
|
19
|
+
# rubocop:enable Naming/MethodParameterName:
|
20
|
+
|
21
|
+
def neighbors(node)
|
22
|
+
neighbors = []
|
23
|
+
return neighbors unless node.can_contain_road?
|
24
|
+
|
25
|
+
x = node.x
|
26
|
+
y = node.y
|
27
|
+
|
28
|
+
node_lookup = node(x - 1, y) if x.positive?
|
29
|
+
neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
|
30
|
+
node_lookup = node(x + 1, y) if x < @nodes[0].size - 1
|
31
|
+
neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
|
32
|
+
node_lookup = node(x, y - 1) if y.positive?
|
33
|
+
neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
|
34
|
+
node_lookup = node(x, y + 1) if y < @nodes.size - 1
|
35
|
+
neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
|
36
|
+
|
37
|
+
neighbors
|
38
|
+
end
|
39
|
+
|
40
|
+
def min_max_coordinates
|
41
|
+
@min_max_coordinates ||= begin
|
42
|
+
min_x = nil
|
43
|
+
min_y = nil
|
44
|
+
max_x = nil
|
45
|
+
max_y = nil
|
46
|
+
|
47
|
+
@nodes.each do |row|
|
48
|
+
row.each do |object|
|
49
|
+
x = object.x
|
50
|
+
y = object.y
|
51
|
+
|
52
|
+
# Update minimum x and y values
|
53
|
+
min_x = x if min_x.nil? || x < min_x
|
54
|
+
min_y = y if min_y.nil? || y < min_y
|
55
|
+
|
56
|
+
# Update maximum x and y values
|
57
|
+
max_x = x if max_x.nil? || x > max_x
|
58
|
+
max_y = y if max_y.nil? || y > max_y
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
{ min_x: min_x, min_y: min_y, max_x: max_x, max_y: max_y }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def edge_nodes
|
67
|
+
@edge_nodes ||=
|
68
|
+
@nodes.map do |row|
|
69
|
+
row.select do |obj|
|
70
|
+
obj.x == min_max_coordinates[:min_x] ||
|
71
|
+
obj.x == min_max_coordinates[:max_x] ||
|
72
|
+
obj.y == min_max_coordinates[:min_y] ||
|
73
|
+
obj.y == min_max_coordinates[:max_y]
|
74
|
+
end
|
75
|
+
end.flatten
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathfinding/grid'
|
4
|
+
require 'pathfinding/a_star_finder'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Generates roads across map tiles, randomly or given specific coordinates
|
8
|
+
#
|
9
|
+
class RoadGenerator
|
10
|
+
attr_reader :grid, :finder
|
11
|
+
|
12
|
+
def initialize(tiles)
|
13
|
+
@grid = Pathfinding::Grid.new(tiles)
|
14
|
+
@finder = Pathfinding::AStarFinder.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_num_of_random_roads(config)
|
18
|
+
return if config.roads <= 0
|
19
|
+
|
20
|
+
seed = config.road_seed
|
21
|
+
(1..config.roads).each do |n|
|
22
|
+
random_objects_at_edges = random_nodes_not_on_same_edge(seed + n) # add n otherwise each road is the same
|
23
|
+
generate_path(
|
24
|
+
random_objects_at_edges[0].x,
|
25
|
+
random_objects_at_edges[0].y,
|
26
|
+
random_objects_at_edges[1].x,
|
27
|
+
random_objects_at_edges[1].y
|
28
|
+
).each(&:make_road)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_roads_from_coordinate_list(road_paths)
|
33
|
+
road_paths.each_slice(4) do |road_coordinates|
|
34
|
+
generate_path(
|
35
|
+
road_coordinates[0],
|
36
|
+
road_coordinates[1],
|
37
|
+
road_coordinates[2],
|
38
|
+
road_coordinates[3]
|
39
|
+
).each(&:make_road)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def generate_path(start_x, start_y, end_x, end_y)
|
44
|
+
start_node = grid.node(start_x, start_y)
|
45
|
+
end_node = grid.node(end_x, end_y)
|
46
|
+
finder.find_path(start_node, end_node, grid)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def random_nodes_not_on_same_edge(seed)
|
52
|
+
random_generator = Random.new(seed)
|
53
|
+
length = @grid.edge_nodes.length
|
54
|
+
|
55
|
+
loop do
|
56
|
+
index1 = random_generator.rand(length)
|
57
|
+
index2 = random_generator.rand(length)
|
58
|
+
node_one, node_two = @grid.edge_nodes.values_at(index1, index2)
|
59
|
+
|
60
|
+
return [node_one, node_two] if node_one.x != node_two.x && node_one.y != node_two.y
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/tile.rb
CHANGED
@@ -2,17 +2,27 @@
|
|
2
2
|
|
3
3
|
require 'biome'
|
4
4
|
require 'flora'
|
5
|
+
require 'ansi_colours'
|
6
|
+
require 'pry-byebug'
|
5
7
|
|
6
8
|
class Tile
|
7
|
-
attr_reader :x, :y, :height, :moist, :temp, :map
|
9
|
+
attr_reader :x, :y, :height, :moist, :temp, :map, :type
|
8
10
|
|
9
|
-
|
11
|
+
TYPES = %i[
|
12
|
+
terrain
|
13
|
+
road
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0, type: :terrain)
|
10
17
|
@x = x
|
11
18
|
@y = y
|
12
19
|
@height = height
|
13
20
|
@moist = moist
|
14
21
|
@temp = temp
|
15
22
|
@map = map
|
23
|
+
raise ArgumentError, 'invalid tile type' unless TYPES.include?(type)
|
24
|
+
|
25
|
+
@type = type
|
16
26
|
end
|
17
27
|
|
18
28
|
def surrounding_tiles(distance = 1)
|
@@ -36,7 +46,7 @@ class Tile
|
|
36
46
|
end
|
37
47
|
|
38
48
|
def render_to_standard_output
|
39
|
-
print
|
49
|
+
print render_color_by_type + (!items.empty? ? item_with_highest_priority.render_symbol : ' ')
|
40
50
|
print AnsiColours::Background::ANSI_RESET
|
41
51
|
end
|
42
52
|
|
@@ -50,6 +60,10 @@ class Tile
|
|
50
60
|
items.max_by(&:render_priority)
|
51
61
|
end
|
52
62
|
|
63
|
+
def items_contain_flora?
|
64
|
+
items.any? { |i| i.is_a?(Flora) }
|
65
|
+
end
|
66
|
+
|
53
67
|
def to_h
|
54
68
|
{
|
55
69
|
x: x,
|
@@ -58,12 +72,56 @@ class Tile
|
|
58
72
|
moist: moist,
|
59
73
|
temp: temp,
|
60
74
|
biome: biome.to_h,
|
61
|
-
items: items.map(&:to_h)
|
75
|
+
items: items.map(&:to_h),
|
76
|
+
type: type
|
62
77
|
}
|
63
78
|
end
|
64
79
|
|
80
|
+
def make_road
|
81
|
+
@type = :road
|
82
|
+
end
|
83
|
+
|
84
|
+
def road?
|
85
|
+
@type == :road
|
86
|
+
end
|
87
|
+
|
88
|
+
def path_heuristic
|
89
|
+
height
|
90
|
+
end
|
91
|
+
|
92
|
+
def can_contain_road?
|
93
|
+
return true unless biome_is_water_and_is_excluded? || biome_is_high_mountain_and_is_excluded? || tile_contains_flora_and_is_excluded?
|
94
|
+
end
|
95
|
+
|
65
96
|
private
|
66
97
|
|
98
|
+
def biome_is_water_and_is_excluded?
|
99
|
+
biome.water? && map.config.road_config.road_exclude_water_path
|
100
|
+
end
|
101
|
+
|
102
|
+
def biome_is_high_mountain_and_is_excluded?
|
103
|
+
biome.high_mountain? && map.config.road_config.road_exclude_mountain_path
|
104
|
+
end
|
105
|
+
|
106
|
+
def tile_contains_flora_and_is_excluded?
|
107
|
+
items_contain_flora? && map.config.road_config.road_exclude_flora_path
|
108
|
+
end
|
109
|
+
|
110
|
+
def render_color_by_type
|
111
|
+
case type
|
112
|
+
when :terrain then biome.colour
|
113
|
+
when :road
|
114
|
+
case height
|
115
|
+
when 0.66..1
|
116
|
+
AnsiColours::Background::HIGH_ROAD_BLACK
|
117
|
+
when 0.33..0.66
|
118
|
+
AnsiColours::Background::ROAD_BLACK
|
119
|
+
when 0..0.33
|
120
|
+
AnsiColours::Background::LOW_ROAD_BLACK
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
67
125
|
def items_generated_with_flora_if_applicable
|
68
126
|
if map.config.generate_flora && biome.flora_available
|
69
127
|
range_max_value = map.tiles[(y - biome.flora_range)...(y + biome.flora_range)]&.map do |r|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-perlin-2D-map-generator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tyler Matthews (matthewstyler)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: perlin
|
@@ -58,14 +58,28 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 2.0
|
61
|
+
version: 2.1.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 2.0
|
68
|
+
version: 2.1.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.10.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.10.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,14 +100,14 @@ dependencies:
|
|
86
100
|
requirements:
|
87
101
|
- - "~>"
|
88
102
|
- !ruby/object:Gem::Version
|
89
|
-
version: 1.
|
103
|
+
version: 1.55.1
|
90
104
|
type: :development
|
91
105
|
prerelease: false
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
93
107
|
requirements:
|
94
108
|
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
|
-
version: 1.
|
110
|
+
version: 1.55.1
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: simplecov
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,10 +122,10 @@ dependencies:
|
|
108
122
|
- - "~>"
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: 0.22.0
|
111
|
-
description: A gem that procedurally generates a seeded and customizable 2D map
|
112
|
-
perlin noise. Map can be rendered in console using ansi colors
|
113
|
-
array of hashes describing each tile and binome.
|
114
|
-
--help option for full usage details.
|
125
|
+
description: A gem that procedurally generates a seeded and customizable 2D map with
|
126
|
+
optional roads using perlin noise. Map can be rendered in console using ansi colors
|
127
|
+
or returned as 2D array of hashes describing each tile and binome. Completely customizable,
|
128
|
+
use the --help option for full usage details.
|
115
129
|
email: matthews.tyl@gmail.com
|
116
130
|
executables:
|
117
131
|
- ruby-perlin-2D-map-generator
|
@@ -128,6 +142,9 @@ files:
|
|
128
142
|
- lib/map.rb
|
129
143
|
- lib/map_config.rb
|
130
144
|
- lib/map_tile_generator.rb
|
145
|
+
- lib/pathfinding/a_star_finder.rb
|
146
|
+
- lib/pathfinding/grid.rb
|
147
|
+
- lib/road_generator.rb
|
131
148
|
- lib/tile.rb
|
132
149
|
- lib/tile_item.rb
|
133
150
|
- lib/tile_perlin_generator.rb
|
@@ -155,6 +172,6 @@ requirements: []
|
|
155
172
|
rubygems_version: 3.2.3
|
156
173
|
signing_key:
|
157
174
|
specification_version: 4
|
158
|
-
summary: Procedurally generate seeded and customizable 2D maps
|
159
|
-
colours or described in a 2D array of hashes
|
175
|
+
summary: Procedurally generate seeded and customizable 2D maps with optional roads,
|
176
|
+
rendered with ansi colours or described in a 2D array of hashes
|
160
177
|
test_files: []
|