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

Sign up to get free protection for your applications and to get access to all the features.
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: []