ruby-perlin-2D-map-generator 0.0.3 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e6cc04acc39df3077354eadba18f3dc043c2eca6806c14429b8468990aaa9f5
4
- data.tar.gz: f3ec0afeb64bea28b1c5593b1648ff1effad677fc595a00748e7fc79349138d4
3
+ metadata.gz: 4a4341c2583eba83e95d6ea4a9c1ee05a857bc1f5c77c6d3aeeba41f6995a656
4
+ data.tar.gz: 599b47f78c83dc6acd99dcde0e9aadbeb8d445fc7d9b6ef7ba8b1acbb3c15d7b
5
5
  SHA512:
6
- metadata.gz: 1a5b8063ab6ce6a9dd92774686e42a4b9ecaa30a9b1d0df14676b76e4a83d9213a5d14268d3ed5945d554cc4c8eb41a128657aca4243257d04f1a40bb54a5b5c
7
- data.tar.gz: 6d6e443145404ccd4d937723150b02953d5231e6a3d2b8f7287750bb79199cc5db275bdd44664d9025522a637b3f9104f852e3b652aefbe6ef395163e7b84f3e
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
- A gem that procedurally generates seeded and customizable 2D map using perlin noise.
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 (0 -
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 (0 -
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 (0
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
- ```bash
83
+ ```irb
59
84
  irb(main):001:0> map = Map.new
60
- ...
61
- irb(main):002:0> map.describe[1][0]
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, while describe prints each
86
- tiles bionome information in the map.
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 Adjust each generated elevation by this percent (0 -
90
- 100) (default 0.0)
91
- --fhx=float The frequency for height generation across the x-axis
92
- (default 2.5)
93
- --fhy=float The frequency for height generation across the y-axis
94
- (default 2.5)
95
- --fmx=float The frequency for moist generation across the x-axis
96
- (default 2.5)
97
- --fmy=float The frequency for moist generation across the y-axis
98
- (default 2.5)
99
- --ftx=float The frequency for temp generation across the x-axis
100
- (default 2.5)
101
- --fty=float The frequency for temp generation across the y-axis
102
- (default 2.5)
103
- --gf=bool Generate flora, significantly affects performance
104
- --height=int The height of the generated map (default 128)
105
- -h, --help Print usage
106
- --hs=int The seed for a terrains height perlin generation
107
- (default 10)
108
- --moisture=float Adjust each generated moisture by this percent (0 -
109
- 100) (default 0.0)
110
- --ms=int The seed for a terrains moist perlin generation
111
- (default 300)
112
- --oh=int Octaves for height generation (default 3)
113
- --om=int Octaves for moist generation (default 3)
114
- --ot=int Octaves for temp generation (default 3)
115
- --ph=float Persistance for height generation (default 1.0)
116
- --pm=float Persistance for moist generation (default 1.0)
117
- --pt=float Persistance for temp generation (default 1.0)
118
- --temp=float Adjust each generated temperature by this percent (0
119
- - 100) (default 0.0)
120
- --ts=int The seed for a terrains temperature perlin generation
121
- (default 3000)
122
- --width=int The width of the generated map (default 128)
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
- validate ->(v) { v.downcase == 'describe' || v.downcase == 'render' }
29
- desc 'command to run: render prints the map to standard output using ansi colors, ' \
30
- 'while describe prints each tiles bionome information in the map.'
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 (0 - 100)'
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 (0 - 100)'
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 (0 - 100)'
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
- perlin_height_config: perlin_height_config,
252
- perlin_moist_config: perlin_moist_config,
253
- perlin_temp_config: perlin_temp_config,
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
@@ -26,6 +26,9 @@ module AnsiColours
26
26
  TAIGA_HIGHLAND = "\e[48;5;65m"
27
27
  TAIGA_COAST = "\e[48;5;17m"
28
28
  ICE = "\e[48;5;159m"
29
+ LOW_ROAD_BLACK = "\e[48;5;241m"
30
+ ROAD_BLACK = "\e[48;5;239m"
31
+ HIGH_ROAD_BLACK = "\e[48;5;236m"
29
32
  ANSI_RESET = "\033[0m"
30
33
  end
31
34
  end
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 ||= MapTileGenerator.new(map: self).generate
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
- PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS)
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
- attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height
39
+ PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS)
40
+ AllPerlinConfigs = Struct.new(*ALL_PERLIN_CONFIGS)
41
+ RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS)
32
42
 
33
- def initialize(perlin_height_config: default_perlin_height_config, perlin_moist_config: default_perlin_moist_config, perlin_temp_config: default_perlin_temp_config, width: DEFAULT_TILE_COUNT,
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
- def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0)
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 biome.colour + (!items.empty? ? item_with_highest_priority.render_symbol : ' ')
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.3
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-07-08 00:00:00.000000000 Z
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.4
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.4
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.54.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.54.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 using
112
- perlin noise. Map can be rendered in console using ansi colors or returned as 2D
113
- array of hashes describing each tile and binome. Completelycustomizable, use the
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, rendered with ansi
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: []