ruby-perlin-2D-map-generator 0.0.4 → 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: 1bd8731cdd9870723cfcb6b977f7d111cc160591f727598ae05be919d6dd231f
4
- data.tar.gz: d13ec13f781e96e6251bf37ee54d998c065698063e59d99b8143735c48e7e28a
3
+ metadata.gz: 4a4341c2583eba83e95d6ea4a9c1ee05a857bc1f5c77c6d3aeeba41f6995a656
4
+ data.tar.gz: 599b47f78c83dc6acd99dcde0e9aadbeb8d445fc7d9b6ef7ba8b1acbb3c15d7b
5
5
  SHA512:
6
- metadata.gz: 766f4c73584e9e5152bf6842790bb3f330ca7d7fd9b46ab7427754f4c7869c0529e870827f181b0523f341d9134e8c5fdce18eecc2ef8a9ad2d22f17112cb040
7
- data.tar.gz: ed3f8b411ccced1c278e0038abd92ccb6f856f67b683f13ab5298b51f762e71683efecf69e87ab53b56ecacf24859a8541336330fb65dcb15da2a8ae1e3b2740
6
+ metadata.gz: bbabe795544e432d7cf4e5dac2fd6f2b14dd17239d3eb2a59013ce3655bcee1873ef4edb5e6048be372c474af04234e5f7ef33806b517de1b160ea5d59b3d62f
7
+ data.tar.gz: 246cc08e1413e89c6929eacf4201a7895e64595067367f02ed8e758a239e18b098186e16c1c8a85900e1519fa84eb5ace115d15e094894b62825664b1059e57d
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ![CodeQL](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/workflows/CodeQL/badge.svg)
6
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
7
 
8
- A gem that procedurally generates seeded and customizable 2D map using perlin noise.
8
+ A gem that procedurally generates seeded and customizable 2D map with optional roads using perlin noise.
9
9
 
10
10
  Include the gem in your project, or use the executable from the command line.
11
11
 
@@ -34,6 +34,7 @@ gem install ruby-perlin-2D-map-generator
34
34
  See Command line Usage for full customization, below are some examples. Alter the temperature, moisture or elevation seeds to alter these maps:
35
35
 
36
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`
37
38
  - Desert (increase temperature, decrease moisture): `ruby-perlin-2D-map-generator render --temp=100 --moisture=-100`
38
39
  - Mountainous with lakes (increase elevation, increase moisture) `ruby-perlin-2D-map-generator render --elevation=25 --moisture=25`
39
40
  - Islands (decreaes elevation, increase moisture): `ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25`
@@ -44,21 +45,39 @@ See Command line Usage for full customization, below are some examples. Alter th
44
45
  --width=int The width of the generated map (default 128)
45
46
  --height=int The height of the generated map (default 128)
46
47
 
48
+ --roads=int Add this many roads through the map,
49
+ starting and ending at edges
50
+ (default 0)
51
+
47
52
  --hs=int The seed for a terrains height perlin generation
48
53
  (default 10)
49
54
  --ms=int The seed for a terrains moist perlin generation
50
55
  (default 300)
51
56
  --ts=int The seed for a terrains temperature perlin generation
52
57
  (default 3000)
58
+ --rs=int The seed for generating roads
59
+ (default 100)
53
60
 
54
- --elevation=float Adjust each generated elevation by this percent (0 -
61
+ --elevation=float Adjust each generated elevation by this percent (-100 -
55
62
  100) (default 0.0)
56
- --moisture=float Adjust each generated moisture by this percent (0 -
63
+ --moisture=float Adjust each generated moisture by this percent (-100 -
57
64
  100) (default 0.0)
58
- --temp=float Adjust each generated temperature by this percent (0
65
+ --temp=float Adjust each generated temperature by this percent (-100
59
66
  - 100) (default 0.0)
60
67
  ```
61
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
+
62
81
  # Generate without rendering
63
82
 
64
83
  ```irb
@@ -112,7 +131,7 @@ $ ruby-perlin-2D-map-generator --help
112
131
  ```bash
113
132
  Usage: ruby-perlin-2D-map-generator [OPTIONS] (DESCRIBE | RENDER)
114
133
 
115
- Generate a seeded customizable procedurally generated 2D map.
134
+ Generate a seeded customizable procedurally generated 2D map with optional roads.
116
135
  Rendered in the console using ansi colours, or described as a 2D array of
117
136
  hashes with each tiles information.
118
137
 
@@ -128,40 +147,64 @@ Keywords:
128
147
  coordinate tile details
129
148
 
130
149
  Options:
131
- --elevation=float Adjust each generated elevation by this percent (0 -
132
- 100) (default 0.0)
133
- --fhx=float The frequency for height generation across the x-axis
134
- (default 2.5)
135
- --fhy=float The frequency for height generation across the y-axis
136
- (default 2.5)
137
- --fmx=float The frequency for moist generation across the x-axis
138
- (default 2.5)
139
- --fmy=float The frequency for moist generation across the y-axis
140
- (default 2.5)
141
- --ftx=float The frequency for temp generation across the x-axis
142
- (default 2.5)
143
- --fty=float The frequency for temp generation across the y-axis
144
- (default 2.5)
145
- --gf=bool Generate flora, significantly affects performance
146
- --height=int The height of the generated map (default 128)
147
- -h, --help Print usage
148
- --hs=int The seed for a terrains height perlin generation
149
- (default 10)
150
- --moisture=float Adjust each generated moisture by this percent (0 -
151
- 100) (default 0.0)
152
- --ms=int The seed for a terrains moist perlin generation
153
- (default 300)
154
- --oh=int Octaves for height generation (default 3)
155
- --om=int Octaves for moist generation (default 3)
156
- --ot=int Octaves for temp generation (default 3)
157
- --ph=float Persistance for height generation (default 1.0)
158
- --pm=float Persistance for moist generation (default 1.0)
159
- --pt=float Persistance for temp generation (default 1.0)
160
- --temp=float Adjust each generated temperature by this percent (0
161
- - 100) (default 0.0)
162
- --ts=int The seed for a terrains temperature perlin generation
163
- (default 3000)
164
- --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)
165
208
 
166
209
  Examples:
167
210
  Render with defaults
@@ -169,6 +212,9 @@ Examples:
169
212
 
170
213
  Render with options
171
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
172
218
 
173
219
  Describe tile [1, 1]
174
220
  $ ruby-perlin-2D-map-generator describe coordinates=1,1
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',
@@ -21,6 +21,9 @@ module CLI
21
21
  example 'Render with options',
22
22
  ' $ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1'
23
23
 
24
+ example 'Render with roads',
25
+ ' $ ruby-perlin-2D-map-generator render --roads=2'
26
+
24
27
  example 'Describe tile [1, 1]',
25
28
  ' $ ruby-perlin-2D-map-generator describe coordinates=1,1'
26
29
  end
@@ -41,6 +44,15 @@ module CLI
41
44
  desc 'Used with the describe command, only returns the given coordinate tile details'
42
45
  end
43
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
54
+ end
55
+
44
56
  option :height_seed do
45
57
  long '--hs int'
46
58
  # or
@@ -210,7 +222,7 @@ module CLI
210
222
  long '--temp float'
211
223
  long '--temp=float'
212
224
 
213
- desc 'Adjust each generated temperature by this percent (0 - 100)'
225
+ desc 'Adjust each generated temperature by this percent (-100 - 100)'
214
226
  convert ->(val) { val.to_f / 100.0 }
215
227
  validate ->(val) { val >= -1.0 && val <= 1.0 }
216
228
  default MapConfig::DEFAULT_TEMP_ADJUSTMENT
@@ -220,7 +232,7 @@ module CLI
220
232
  long '--elevation float'
221
233
  long '--elevation=float'
222
234
 
223
- desc 'Adjust each generated elevation by this percent (0 - 100)'
235
+ desc 'Adjust each generated elevation by this percent (-100 - 100)'
224
236
  convert ->(val) { val.to_f / 100.0 }
225
237
  validate ->(val) { val >= -1.0 && val <= 1.0 }
226
238
  default MapConfig::DEFAULT_HEIGHT_ADJUSTMENT
@@ -230,12 +242,58 @@ module CLI
230
242
  long '--moisture float'
231
243
  long '--moisture=float'
232
244
 
233
- desc 'Adjust each generated moisture by this percent (0 - 100)'
245
+ desc 'Adjust each generated moisture by this percent (-100 - 100)'
234
246
  convert ->(val) { val.to_f / 100.0 }
235
247
  validate ->(val) { val >= -1.0 && val <= 1.0 }
236
248
  default MapConfig::DEFAULT_MOIST_ADJUSTMENT
237
249
  end
238
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
+
239
297
  flag :help do
240
298
  short '-h'
241
299
  long '--help'
@@ -259,10 +317,9 @@ module CLI
259
317
  map = Map.new(map_config: MapConfig.new(
260
318
  width: params[:width],
261
319
  height: params[:height],
262
- perlin_height_config: perlin_height_config,
263
- perlin_moist_config: perlin_moist_config,
264
- perlin_temp_config: perlin_temp_config,
265
- 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)
266
323
  ))
267
324
  case params[:command]
268
325
  when 'render' then map.render
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
@@ -30,6 +31,18 @@ class Map
30
31
  # rubocop:enable Naming/MethodParameterName:
31
32
 
32
33
  def tiles
33
- @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
34
47
  end
35
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.4
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-09 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: []