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

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: 4a4341c2583eba83e95d6ea4a9c1ee05a857bc1f5c77c6d3aeeba41f6995a656
4
- data.tar.gz: 599b47f78c83dc6acd99dcde0e9aadbeb8d445fc7d9b6ef7ba8b1acbb3c15d7b
3
+ metadata.gz: 8bef2e8554e39c0e315b940f4a98a8927348ae576a7f12168c86c74f969c241b
4
+ data.tar.gz: b3debfe8269979c1f1f0be2a8def6a86de9fcd2a8ad6f548295113068ec04566
5
5
  SHA512:
6
- metadata.gz: bbabe795544e432d7cf4e5dac2fd6f2b14dd17239d3eb2a59013ce3655bcee1873ef4edb5e6048be372c474af04234e5f7ef33806b517de1b160ea5d59b3d62f
7
- data.tar.gz: 246cc08e1413e89c6929eacf4201a7895e64595067367f02ed8e758a239e18b098186e16c1c8a85900e1519fa84eb5ace115d15e094894b62825664b1059e57d
6
+ metadata.gz: c91f9810eda61b360e26b31944820b2587ff9e6a10776cdc84347475470573b381853e63b629781fbc0f18eb9fc33299fad31e2a436ef7f7a66d67db8dbb2ea2
7
+ data.tar.gz: 8c12388e0e7965165557bc5680f030d6d2da9a42e35cc066fbff2390de0e6076c2318e7d62000d17026fa91206adb1ff631eff0ff6af5d670ef81e3d206e45a0
data/README.md CHANGED
@@ -3,16 +3,18 @@
3
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
4
  ![CI Status](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/actions/workflows/main.yml/badge.svg)
5
5
  ![CodeQL](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/workflows/CodeQL/badge.svg)
6
+ <a href="https://codeclimate.com/github/matthewstyler/ruby-perlin-2D-map-generator/test_coverage"><img src="https://api.codeclimate.com/v1/badges/b99aae29d02b7a8a4cc6/test_coverage" /></a>
7
+ <a href="https://codeclimate.com/github/matthewstyler/ruby-perlin-2D-map-generator/maintainability"><img src="https://api.codeclimate.com/v1/badges/b99aae29d02b7a8a4cc6/maintainability" /></a>
6
8
  [![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
9
 
8
- A gem that procedurally generates seeded and customizable 2D map with optional roads using perlin noise.
10
+ A gem that procedurally generates seeded and customizable 2D map with optional roads and towns using perlin noise.
9
11
 
10
12
  Include the gem in your project, or use the executable from the command line.
11
13
 
12
14
  Map can be rendered in console using ansi colors or returned as 2D array of hashes describing each tile and binome. Completely customizable, use the --help option for full usage details.
13
15
 
14
16
 
15
- ![2D-maps](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/assets/4560901/4fa5883f-839a-40c9-86a1-d5f9e2c37b9a)
17
+ ![2D-maps](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/assets/4560901/6234ebc1-f3bd-48b5-9b78-4d286d2c8d6e)
16
18
 
17
19
 
18
20
  # Installation
@@ -48,7 +50,8 @@ See Command line Usage for full customization, below are some examples. Alter th
48
50
  --roads=int Add this many roads through the map,
49
51
  starting and ending at edges
50
52
  (default 0)
51
-
53
+ --towns=int Add this randomly sized towns
54
+ (default 0)
52
55
  --hs=int The seed for a terrains height perlin generation
53
56
  (default 10)
54
57
  --ms=int The seed for a terrains moist perlin generation
@@ -70,7 +73,7 @@ See Command line Usage for full customization, below are some examples. Alter th
70
73
  Roads can be generated by providing a positive integer to the `roads=` argument. Roads are randomly seeded to begin
71
74
  and start at an axis (but not the same axis).
72
75
 
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.
76
+ A* pathfinding with a priority queue is used to generate the roads. The heuristic uses manhattan distance, and favours existing roads and similar elevations in adjacent tiles.
74
77
 
75
78
  Roads can be configured to include/exclude generating paths thorugh water, mountains and flora.
76
79
 
@@ -78,6 +81,8 @@ Tiles containing roads are of type `road`, those without are of type `terrain`.
78
81
 
79
82
  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
83
 
84
+ ## Towns
85
+ With Poisson Disk Sampling, towns can be generated randomly or with a provided x,y coordinate used as a centroid with radius. The result will be tiles that contain `Building` `items`. Buildings in a town are connected by roads, and additionally towns are connected to other towns by roads.
81
86
  # Generate without rendering
82
87
 
83
88
  ```irb
@@ -201,6 +206,16 @@ Options:
201
206
  (default 100)
202
207
  --temp=float Adjust each generated temperature by
203
208
  this percent (-100 - 100) (default 0.0)
209
+ --town_seed=int The seed for generating towns
210
+ (default 500)
211
+ --towns=int Add this many randomly sized towns
212
+ throughout the map
213
+ (default 0)
214
+ --towns_to_make ints Attempt to create a town at given x,y
215
+ coordinate, with z points and v radius
216
+ (4 integers). Can be supplied multiple
217
+ towns.
218
+ (default [])
204
219
  --ts=int The seed for a terrains temperature
205
220
  perlin generation (default 3000)
206
221
  --width=int The width of the generated map
@@ -215,6 +230,9 @@ Examples:
215
230
 
216
231
  Render with roads
217
232
  $ ruby-perlin-2D-map-generator render --roads=2
233
+
234
+ Render with 5 roads, 1 provided road, 10 random towns and 1 provided town
235
+ $ ruby-perlin-2D-map-generator render --roads=5 --roads_to_make=0,0,50,50 --towns=10 --towns_to_make=5,5,10,3
218
236
 
219
237
  Describe tile [1, 1]
220
238
  $ ruby-perlin-2D-map-generator describe coordinates=1,1
data/lib/CLI/command.rb CHANGED
@@ -53,6 +53,15 @@ module CLI
53
53
  default MapConfig::DEFAULT_ROADS_TO_MAKE
54
54
  end
55
55
 
56
+ option :towns_to_make do
57
+ arity one
58
+ long '--towns_to_make ints'
59
+ convert :int_list
60
+ validate ->(v) { v >= 0 }
61
+ desc 'Attempt to create a town at given x,y coordinate, with z points and v radius (4 integers). Can be supplied multiple towns.'
62
+ default MapConfig::DEFAULT_TOWNS_TO_MAKE
63
+ end
64
+
56
65
  option :height_seed do
57
66
  long '--hs int'
58
67
  # or
@@ -294,6 +303,31 @@ module CLI
294
303
  default MapConfig::DEFAULT_ROAD_EXCLUDE_FLORA_PATH
295
304
  end
296
305
 
306
+ option :town_seed do
307
+ long '--town_seed int'
308
+ long '--town_seed=int'
309
+
310
+ desc 'The seed for generating towns'
311
+ convert Integer
312
+ default MapConfig::DEFAULT_TOWN_SEED
313
+ end
314
+
315
+ option :towns do
316
+ long '--towns int'
317
+ long '--towns=int'
318
+
319
+ desc 'Add this many randomly sized towns throughout the map'
320
+ convert Integer
321
+ validate ->(val) { val >= 0 }
322
+ default MapConfig::DEFAULT_NUM_OF_TOWNS
323
+ end
324
+
325
+ option :verbose do
326
+ arity one
327
+ long '--verbose'
328
+ desc 'Used with the render command, outputs loading information'
329
+ end
330
+
297
331
  flag :help do
298
332
  short '-h'
299
333
  long '--help'
@@ -319,7 +353,9 @@ module CLI
319
353
  height: params[:height],
320
354
  all_perlin_configs: MapConfig::AllPerlinConfigs.new(perlin_height_config, perlin_moist_config, perlin_temp_config),
321
355
  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)
356
+ 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),
357
+ town_config: MapConfig::TownConfig.new(*params.to_h.slice(:town_seed, :towns, :towns_to_make).values),
358
+ verbose: params[:verbose]
323
359
  ))
324
360
  case params[:command]
325
361
  when 'render' then map.render
data/lib/biome.rb CHANGED
@@ -151,21 +151,21 @@ class Biome
151
151
  case elevation
152
152
  when 0.95..1
153
153
  SNOW
154
- when 0.9..0.95
154
+ when 0.9...0.95
155
155
  ROCKS
156
- when 0.8..0.9
156
+ when 0.8...0.9
157
157
  if moist < 0.9
158
158
  MOUNTAIN
159
159
  else
160
160
  SHOAL
161
161
  end
162
- when 0.7..0.8
162
+ when 0.7...0.8
163
163
  if moist < 0.9
164
164
  MOUNTAIN_FOOT
165
165
  else
166
166
  SHOAL
167
167
  end
168
- when 0.6..0.7
168
+ when 0.6...0.7
169
169
  if moist < 0.8
170
170
  if desert_condition?(moist, temp)
171
171
  STEPPE_DESERT
@@ -179,7 +179,7 @@ class Biome
179
179
  else
180
180
  SHOAL
181
181
  end
182
- when 0.3..0.6
182
+ when 0.3...0.6
183
183
  if desert_condition?(moist, temp)
184
184
  DESERT
185
185
  elsif taiga_condition?(moist, temp)
@@ -187,7 +187,7 @@ class Biome
187
187
  else
188
188
  VALLEY
189
189
  end
190
- when 0.2..0.3
190
+ when 0.2...0.3
191
191
  if desert_condition?(moist, temp)
192
192
  DEEP_DESERT
193
193
  elsif taiga_condition?(moist, temp)
@@ -195,25 +195,25 @@ class Biome
195
195
  else
196
196
  DEEP_VALLEY
197
197
  end
198
- when 0.15..0.2
198
+ when 0.15...0.2
199
199
  if taiga_condition?(moist, temp)
200
200
  TAIGA_COAST
201
201
  else
202
202
  COASTLINE
203
203
  end
204
- when 0.05..0.15
204
+ when 0.05...0.15
205
205
  if taiga_condition?(moist, temp)
206
206
  ICE
207
207
  else
208
208
  SHOAL
209
209
  end
210
- when 0.025..0.05
210
+ when 0.025...0.05
211
211
  if taiga_condition?(moist, temp)
212
212
  ICE
213
213
  else
214
214
  OCEAN
215
215
  end
216
- when 0.0..0.025
216
+ when 0.0...0.025
217
217
  if taiga_condition?(moist, temp)
218
218
  ICE
219
219
  else
data/lib/building.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tile_item'
4
+
5
+ #
6
+ # Represents a building item on a tile
7
+ #
8
+ class Building < TileItem
9
+ TOWN_RENDER_PRIORITY = DEFAULT_RENDER_PRIORITY + 1
10
+
11
+ HOUSE = "\u{1F3E0}"
12
+
13
+ def initialize(render_symbol)
14
+ super self, render_symbol: render_symbol, render_priority: TOWN_RENDER_PRIORITY
15
+ end
16
+
17
+ def self.random_town_building(_seed)
18
+ Building.new(HOUSE)
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Generates flora for the given tiles
5
+ #
6
+ class FloraGenerator
7
+ attr_reader :tiles
8
+
9
+ def initialize(tiles)
10
+ @tiles = tiles
11
+ end
12
+
13
+ def generate(config)
14
+ return unless config.generate_flora
15
+
16
+ puts 'generating flora...' if config.verbose
17
+ tiles.each do |row|
18
+ row.each do |tile|
19
+ next unless tile.biome.flora_available
20
+
21
+ range_max_value = tiles[(tile.y - tile.biome.flora_range)...(tile.y + tile.biome.flora_range)]&.map do |r|
22
+ r[(tile.x - tile.biome.flora_range)...(tile.x + tile.biome.flora_range)]
23
+ end&.flatten&.map(&:height)&.max
24
+ tile.add_flora if range_max_value == tile.height
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/map.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'map_tile_generator'
4
4
  require 'map_config'
5
5
  require 'road_generator'
6
+ require 'town_generator'
7
+ require 'flora_generator'
6
8
 
7
9
  class Map
8
10
  attr_reader :config
@@ -33,16 +35,33 @@ class Map
33
35
  def tiles
34
36
  return @tiles if @tiles
35
37
 
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)
38
+ generate_tiles
39
+ generate_flora
40
+ generate_roads
41
+ generate_towns
42
+
40
43
  @tiles
41
44
  end
42
45
 
43
46
  private
44
47
 
45
48
  def generate_tiles
46
- MapTileGenerator.new(map: self).generate
49
+ @tiles = MapTileGenerator.new(map: self).generate
50
+ end
51
+
52
+ def generate_flora
53
+ FloraGenerator.new(@tiles).generate(config)
54
+ end
55
+
56
+ def generate_roads
57
+ road_generator = RoadGenerator.new(@tiles)
58
+ road_generator.generate_num_of_random_roads(config.road_config)
59
+ road_generator.generate_roads_from_coordinate_list(config.road_config.roads_to_make, config.verbose)
60
+ end
61
+
62
+ def generate_towns
63
+ town_generator = TownGenerator.new(@tiles, seed: config.town_config.town_seed)
64
+ town_generator.generate_random_towns(config.town_config)
65
+ town_generator.generate_towns_from_coordinate_list(config.town_config)
47
66
  end
48
67
  end
data/lib/map_config.rb CHANGED
@@ -32,18 +32,27 @@ class MapConfig
32
32
  DEFAULT_ROAD_EXCLUDE_FLORA_PATH = true
33
33
  DEFAULT_ROADS_TO_MAKE = [].freeze
34
34
 
35
+ DEFAULT_TOWN_SEED = 500
36
+ DEFAULT_NUM_OF_TOWNS = 0
37
+ DEFAULT_TOWNS_TO_MAKE = [].freeze
38
+
39
+ DEFAULT_VERBOSE = false
40
+
35
41
  PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze
36
42
  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
43
+ ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path roads_to_make verbose].freeze
44
+ TOWN_CONFIG_OPTIONS = %i[town_seed towns towns_to_make verbose].freeze
38
45
 
39
46
  PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS)
40
47
  AllPerlinConfigs = Struct.new(*ALL_PERLIN_CONFIGS)
41
48
  RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS)
49
+ TownConfig = Struct.new(*TOWN_CONFIG_OPTIONS)
42
50
 
43
- attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :road_config
51
+ attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :road_config, :town_config, :verbose
44
52
 
53
+ # rubocop:disable Metrics/ParameterLists:
45
54
  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)
55
+ height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, road_config: default_road_config, town_config: default_town_config, verbose: DEFAULT_VERBOSE)
47
56
  validate(all_perlin_configs)
48
57
  @generate_flora = generate_flora
49
58
  @perlin_height_config = all_perlin_configs.perlin_height_config
@@ -52,7 +61,12 @@ class MapConfig
52
61
  @width = width
53
62
  @height = height
54
63
  @road_config = road_config
64
+ @town_config = town_config
65
+ @verbose = verbose
66
+ town_config.verbose = verbose
67
+ road_config.verbose = verbose
55
68
  end
69
+ # rubocop:enable Metrics/ParameterLists:
56
70
 
57
71
  private
58
72
 
@@ -78,7 +92,12 @@ class MapConfig
78
92
  end
79
93
 
80
94
  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)
95
+ 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,
96
+ DEFAULT_VERBOSE)
97
+ end
98
+
99
+ def default_town_config
100
+ TownConfig.new(DEFAULT_TOWN_SEED, DEFAULT_NUM_OF_TOWNS, DEFAULT_TOWNS_TO_MAKE, DEFAULT_VERBOSE)
82
101
  end
83
102
 
84
103
  def default_perlin_configs
@@ -15,6 +15,7 @@ class MapTileGenerator
15
15
  end
16
16
 
17
17
  def generate
18
+ puts "generating #{map_config.width} x #{map_config.height} tiles..." if map_config.verbose
18
19
  positive_quadrant_cartesian_plane
19
20
  end
20
21
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+ require 'pathfinding/priority_queue'
5
+
3
6
  module Pathfinding
4
7
  #
5
8
  # An A* Pathfinder to build roads/paths between two coordinates containing
@@ -7,28 +10,35 @@ module Pathfinding
7
10
  #
8
11
  class AStarFinder
9
12
  def find_path(start_node, end_node, grid)
10
- open_set = [start_node]
11
13
  came_from = {}
12
14
  g_score = { start_node => 0 }
13
- f_score = { start_node => heuristic_cost_estimate(start_node, end_node) }
15
+ f_score = { start_node => manhattan_distance(start_node, end_node) }
16
+
17
+ open_set = Pathfinding::PriorityQueue.new
18
+ open_set.push(start_node, f_score[start_node])
14
19
 
20
+ closed_set = Set.new
15
21
  until open_set.empty?
16
- current_node = open_set.min_by { |node| f_score[node] }
22
+ current = open_set.pop
23
+
24
+ # Early exit if the current node is in the closed set
25
+ next if closed_set.include?(current)
17
26
 
18
- return reconstruct_path(came_from, current_node) if current_node == end_node
27
+ # Mark the current node as visited
28
+ closed_set.add(current)
19
29
 
20
- open_set.delete(current_node)
30
+ return reconstruct_path(came_from, current) if current == end_node
21
31
 
22
- grid.neighbors(current_node).each do |neighbor|
23
- tentative_g_score = g_score[current_node] + 1
32
+ grid.neighbors(current).each do |neighbor|
33
+ tentative_g_score = g_score[current] + 1
24
34
 
25
- next unless !g_score[neighbor] || tentative_g_score < g_score[neighbor]
35
+ next if closed_set.include?(neighbor) || (g_score[neighbor] && tentative_g_score >= g_score[neighbor])
26
36
 
27
- came_from[neighbor] = current_node
37
+ came_from[neighbor] = current
28
38
  g_score[neighbor] = tentative_g_score
29
39
  f_score[neighbor] = g_score[neighbor] + heuristic_cost_estimate(neighbor, end_node)
30
40
 
31
- open_set << neighbor unless open_set.include?(neighbor)
41
+ open_set.push(neighbor, f_score[neighbor])
32
42
  end
33
43
  end
34
44
 
@@ -39,10 +49,13 @@ module Pathfinding
39
49
  private
40
50
 
41
51
  def heuristic_cost_estimate(node, end_node)
42
- (node.x - end_node.x).abs +
43
- (node.y - end_node.y).abs +
52
+ manhattan_distance(node, end_node) +
44
53
  (node.path_heuristic - end_node.path_heuristic) + # elevation for natural roads
45
- (node.road? ? 0 : 5) # share existing roads
54
+ (node.road? ? 0 : 1000) # share existing roads
55
+ end
56
+
57
+ def manhattan_distance(node, end_node)
58
+ (node.x - end_node.x).abs + (node.y - end_node.y).abs
46
59
  end
47
60
 
48
61
  def reconstruct_path(came_from, current_node)
@@ -20,19 +20,19 @@ module Pathfinding
20
20
 
21
21
  def neighbors(node)
22
22
  neighbors = []
23
- return neighbors unless node.can_contain_road?
23
+ return neighbors unless node.can_haz_road?
24
24
 
25
25
  x = node.x
26
26
  y = node.y
27
27
 
28
28
  node_lookup = node(x - 1, y) if x.positive?
29
- neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
29
+ neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_haz_road?
30
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?
31
+ neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_haz_road?
32
32
  node_lookup = node(x, y - 1) if y.positive?
33
- neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road?
33
+ neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_haz_road?
34
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?
35
+ neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_haz_road?
36
36
 
37
37
  neighbors
38
38
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pathfinding
4
+ #
5
+ # A Priority Queue implementation for managing elements with associated priorities.
6
+ # Elements are stored and retrieved based on their priority values.
7
+ #
8
+ # This Priority Queue is implemented using a binary heap, which provides efficient
9
+ # insertion, extraction of the minimum element, and deletion operations.
10
+ #
11
+ class PriorityQueue
12
+ def initialize(&block)
13
+ @heap = []
14
+ @compare = block || proc { |a, b| @priority_hash[a] < @priority_hash[b] }
15
+ @priority_hash = {}
16
+ end
17
+
18
+ def push(item, priority)
19
+ @heap << item
20
+ @priority_hash[item] = priority
21
+ heapify_up(@heap.length - 1)
22
+ end
23
+
24
+ def pop
25
+ return nil if @heap.empty?
26
+
27
+ swap(0, @heap.length - 1)
28
+ popped = @heap.pop
29
+ heapify_down(0)
30
+ popped
31
+ end
32
+
33
+ def peek
34
+ @heap[0]
35
+ end
36
+
37
+ def empty?
38
+ @heap.empty?
39
+ end
40
+
41
+ private
42
+
43
+ def heapify_up(index)
44
+ parent_index = (index - 1) / 2
45
+
46
+ return unless index.positive? && @compare.call(@heap[index], @heap[parent_index])
47
+
48
+ swap(index, parent_index)
49
+ heapify_up(parent_index)
50
+ end
51
+
52
+ def heapify_down(index)
53
+ left_child_index = 2 * index + 1
54
+ right_child_index = 2 * index + 2
55
+ smallest = index
56
+
57
+ smallest = left_child_index if left_child_index < @heap.length && @compare.call(@heap[left_child_index], @heap[smallest])
58
+
59
+ smallest = right_child_index if right_child_index < @heap.length && @compare.call(@heap[right_child_index], @heap[smallest])
60
+
61
+ return unless smallest != index
62
+
63
+ swap(index, smallest)
64
+ heapify_down(smallest)
65
+ end
66
+
67
+ # rubocop:disable Naming/MethodParameterName:
68
+ def swap(i, j)
69
+ @heap[i], @heap[j] = @heap[j], @heap[i]
70
+ end
71
+ # rubocop:enable Naming/MethodParameterName:
72
+ end
73
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoissonDiskSampling
4
+ #
5
+ # Encapsulates area to be sampled by poisson disk sampling
6
+ #
7
+ class SampleArea
8
+ def initialize(grid:)
9
+ @grid = grid.dup
10
+ @points = Array.new(height) { Array.new(width) { false } }
11
+ end
12
+
13
+ def height
14
+ @grid.length
15
+ end
16
+
17
+ def width
18
+ @grid[0].length
19
+ end
20
+
21
+ # rubocop:disable Naming/MethodParameterName:
22
+ def set_sampled_point(x, y)
23
+ @points[y][x] = true
24
+ end
25
+
26
+ def [](x, y)
27
+ @grid[y][x]
28
+ end
29
+
30
+ def sampled_point?(x, y)
31
+ @points[y][x]
32
+ end
33
+
34
+ def point_within_bounds?(x, y)
35
+ y >= 0 && y < height && x >= 0 && x < width
36
+ end
37
+
38
+ def point_within_bounds_and_can_have_road?(x, y)
39
+ point_within_bounds?(x, y) && @grid[y][x].can_haz_road?
40
+ end
41
+ # rubocop:enable Naming/MethodParameterName:
42
+ end
43
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry-byebug'
4
+ module PoissonDiskSampling
5
+ #
6
+ # Generates X randomly distributed points within given 2D space
7
+ # using possion disk sampling
8
+ #
9
+ class Sampler
10
+ attr_reader :sample_area, :num_attempts, :seed
11
+
12
+ def initialize(sample_area:, num_attempts: 20, seed: rand)
13
+ @sample_area = sample_area
14
+ @num_attempts = num_attempts
15
+ @seed = seed
16
+ end
17
+
18
+ def generate_points(num_of_points, radius, intial_start_point = nil)
19
+ raise ArgumentError, "invalid start argument #{intial_start_point}" if !intial_start_point.nil? && !intial_start_point.is_a?(Array) && intial_start_point.length != 1
20
+
21
+ retreive_points_until_active_list_empty_or_num_points_reached(intial_start_point || generate_and_assign_initial_point, num_of_points, radius)
22
+ end
23
+
24
+ private
25
+
26
+ def retreive_points_until_active_list_empty_or_num_points_reached(active_list, num_of_points, radius)
27
+ points = []
28
+ retrieve_points(active_list, points, num_of_points, radius) until active_list.empty? || points.length == num_of_points
29
+ points
30
+ end
31
+
32
+ def retrieve_points(active_list, points, num_of_points, radius)
33
+ return if active_list.empty?
34
+
35
+ current_point, active_index = retreive_current_point(active_list)
36
+ found = false
37
+
38
+ num_attempts.times do
39
+ new_point = generate_random_point_around(current_point, radius)
40
+ next if new_point.nil?
41
+ next unless new_point.can_haz_town? && neighbours_empty?(new_point, radius)
42
+
43
+ sample_area.set_sampled_point(new_point.x, new_point.y)
44
+ active_list << new_point
45
+ points << new_point
46
+ return points if points.length == num_of_points
47
+
48
+ found = true
49
+ break
50
+ end
51
+
52
+ active_list.delete_at(active_index) unless found
53
+ end
54
+
55
+ def random_value_and_increment_seed(max)
56
+ val = Random.new(seed).rand(max)
57
+ @seed += 1
58
+ val
59
+ end
60
+
61
+ def retreive_current_point(active_list)
62
+ active_index = random_value_and_increment_seed(active_list.length)
63
+ current_point = active_list[active_index]
64
+ [current_point, active_index]
65
+ end
66
+
67
+ def neighbours_empty?(new_point, radius)
68
+ cell_empty = true
69
+ (-radius..radius).each do |dy|
70
+ (-radius..radius).each do |dx|
71
+ x = new_point.x + dx
72
+ y = new_point.y + dy
73
+ next unless sample_area.point_within_bounds?(x, y) && sample_area[x, y]
74
+
75
+ if sample_area.sampled_point?(x, y) && distance(new_point, sample_area[x, y]) < radius
76
+ cell_empty = false
77
+ break
78
+ end
79
+ end
80
+ break unless cell_empty
81
+ end
82
+ end
83
+
84
+ def generate_and_assign_initial_point
85
+ num_attempts.times do
86
+ initial_point_coords = [random_value_and_increment_seed(sample_area.width), random_value_and_increment_seed(sample_area.height)]
87
+
88
+ if sample_area[initial_point_coords[0], initial_point_coords[1]].can_haz_town?
89
+ sample_area.set_sampled_point(initial_point_coords[0], initial_point_coords[1])
90
+ return [sample_area[initial_point_coords[0], initial_point_coords[1]]]
91
+ end
92
+ end
93
+ []
94
+ end
95
+
96
+ def generate_random_point_around(point, radius)
97
+ distance = radius * (random_value_and_increment_seed(1.0) + 1)
98
+ angle = 2 * Math::PI * random_value_and_increment_seed(1.0)
99
+ generated = [point.y + distance * Math.cos(angle), point.x + distance * Math.sin(angle)].map(&:round)
100
+
101
+ sample_area.point_within_bounds?(generated[1], generated[0]) ? sample_area[generated[1], generated[0]] : nil
102
+ end
103
+
104
+ def distance(point1, point2)
105
+ Math.sqrt((point1.y - point2.y)**2 + (point1.x - point2.x)**2)
106
+ end
107
+ end
108
+ end
@@ -17,8 +17,11 @@ class RoadGenerator
17
17
  def generate_num_of_random_roads(config)
18
18
  return if config.roads <= 0
19
19
 
20
+ puts "generating #{config.roads} random roads..." if config.verbose
21
+
20
22
  seed = config.road_seed
21
23
  (1..config.roads).each do |n|
24
+ puts "generating road #{n}..." if config.verbose
22
25
  random_objects_at_edges = random_nodes_not_on_same_edge(seed + n) # add n otherwise each road is the same
23
26
  generate_path(
24
27
  random_objects_at_edges[0].x,
@@ -29,7 +32,11 @@ class RoadGenerator
29
32
  end
30
33
  end
31
34
 
32
- def generate_roads_from_coordinate_list(road_paths)
35
+ def generate_roads_from_coordinate_list(road_paths, verbose)
36
+ return unless (road_paths.length % 4).zero?
37
+
38
+ puts "generating #{road_paths.length / 4} coordinate roads..." if verbose
39
+
33
40
  road_paths.each_slice(4) do |road_coordinates|
34
41
  generate_path(
35
42
  road_coordinates[0],
data/lib/tile.rb CHANGED
@@ -4,6 +4,7 @@ require 'biome'
4
4
  require 'flora'
5
5
  require 'ansi_colours'
6
6
  require 'pry-byebug'
7
+ require 'building'
7
8
 
8
9
  class Tile
9
10
  attr_reader :x, :y, :height, :moist, :temp, :map, :type
@@ -42,7 +43,7 @@ class Tile
42
43
  end
43
44
 
44
45
  def items
45
- @items ||= items_generated_with_flora_if_applicable
46
+ @items ||= []
46
47
  end
47
48
 
48
49
  def render_to_standard_output
@@ -61,7 +62,15 @@ class Tile
61
62
  end
62
63
 
63
64
  def items_contain_flora?
64
- items.any? { |i| i.is_a?(Flora) }
65
+ items_contain?(Flora)
66
+ end
67
+
68
+ def items_contain_building?
69
+ items_contain?(Building)
70
+ end
71
+
72
+ def items_contain?(item_class)
73
+ items.any? { |i| i.is_a?(item_class) }
65
74
  end
66
75
 
67
76
  def to_h
@@ -77,6 +86,10 @@ class Tile
77
86
  }
78
87
  end
79
88
 
89
+ def add_town_item(seed)
90
+ add_item(Building.random_town_building(seed))
91
+ end
92
+
80
93
  def make_road
81
94
  @type = :road
82
95
  end
@@ -89,8 +102,16 @@ class Tile
89
102
  height
90
103
  end
91
104
 
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?
105
+ def can_haz_town?
106
+ !road? && !biome.water? && !biome.high_mountain? && !items_contain_flora?
107
+ end
108
+
109
+ def can_haz_road?
110
+ true unless biome_is_water_and_is_excluded? || biome_is_high_mountain_and_is_excluded? || tile_contains_flora_and_is_excluded? || items_contain_building?
111
+ end
112
+
113
+ def add_flora
114
+ add_item(biome.flora)
94
115
  end
95
116
 
96
117
  private
@@ -121,19 +142,4 @@ class Tile
121
142
  end
122
143
  end
123
144
  end
124
-
125
- def items_generated_with_flora_if_applicable
126
- if map.config.generate_flora && biome.flora_available
127
- range_max_value = map.tiles[(y - biome.flora_range)...(y + biome.flora_range)]&.map do |r|
128
- r[(x - biome.flora_range)...(x + biome.flora_range)]
129
- end&.flatten&.map(&:height)&.max
130
- if range_max_value == height
131
- [biome.flora]
132
- else
133
- []
134
- end
135
- else
136
- []
137
- end
138
- end
139
145
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'poisson_disk_sampling/sampler'
4
+ require 'poisson_disk_sampling/sample_area'
5
+ require 'road_generator'
6
+ require 'map_config'
7
+
8
+ #
9
+ # Generates building tile items using Poisson Disk Sampling for the given tiles
10
+ # Roads are generated between the buildings and between towns using A* pathfinding
11
+ #
12
+ class TownGenerator
13
+ attr_reader :sample_area, :road_generator
14
+
15
+ def initialize(tiles, seed: MapConfig::DEFAULT_TOWN_SEED)
16
+ @sample_area = PoissonDiskSampling::SampleArea.new(grid: tiles)
17
+ @road_generator = RoadGenerator.new(tiles)
18
+ @seed = seed
19
+ @all_town_points = []
20
+ end
21
+
22
+ def generate_random_towns(config)
23
+ return if config.towns <= 0
24
+
25
+ puts "generating #{config.towns} random towns..." if config.verbose
26
+
27
+ @all_town_points.concat(iterate_through_towns(config.towns) do |n|
28
+ generate_random_town(n, config.verbose)
29
+ end)
30
+
31
+ generate_roads_between_towns(config.verbose)
32
+ end
33
+
34
+ def generate_towns_from_coordinate_list(config)
35
+ return unless (config.towns_to_make.length % 4).zero?
36
+
37
+ puts "generating #{config.towns_to_make.length / 4} coordinate towns..." if config.verbose
38
+
39
+ @all_town_points.concat(iterate_through_towns((config.towns_to_make.length / 4)) do |n|
40
+ town_values = config.towns_to_make[(n - 1) * 4..].take(4)
41
+ generate_town(n, town_values[2], town_values[3], [sample_area[town_values[0], town_values[1]]], config.verbose)
42
+ end)
43
+
44
+ generate_roads_between_towns(config.verbose)
45
+ end
46
+
47
+ private
48
+
49
+ def iterate_through_towns(num_of_towns)
50
+ (1..num_of_towns).map do |n|
51
+ town_points = yield n
52
+ @seed += 1000
53
+ town_points
54
+ end
55
+ end
56
+
57
+ def generate_random_town(town_num, verbose)
58
+ random_town_gen = Random.new(@seed)
59
+ generate_town(town_num, random_town_gen.rand(10..40), random_town_gen.rand(2..4), nil, verbose)
60
+ end
61
+
62
+ def generate_town(town_num, num_of_points, radius, initial_coords, verbose)
63
+ puts "generating town #{town_num}..." if verbose
64
+
65
+ points = generate_points_for_town(num_of_points, radius, initial_coords)
66
+ generate_town_roads(points, town_num, verbose)
67
+ points
68
+ end
69
+
70
+ def generate_points_for_town(num_of_points, radius, intial_coordinates)
71
+ points =
72
+ PoissonDiskSampling::Sampler.new(
73
+ sample_area: sample_area,
74
+ seed: @seed
75
+ ).generate_points(num_of_points, radius, intial_coordinates)
76
+ points.each do |point|
77
+ @seed += 1
78
+ point.add_town_item(@seed)
79
+ end
80
+ points
81
+ end
82
+
83
+ def generate_town_roads(points, town_num, verbose)
84
+ # TODO: slow, bad (complete graph) will update to use minimum tree spanning algorithm instead
85
+ puts "generating town #{town_num} roads..." if verbose
86
+
87
+ connected_pairs = Set.new
88
+ points.each_with_index do |point_one, idx_one|
89
+ points[idx_one + 1..].each do |point_two|
90
+ next if connected_pairs.include?([point_one, point_two]) || connected_pairs.include?([point_two, point_one])
91
+
92
+ road_to_building_one = place_in_front_or_behind(point_one)
93
+ road_to_building_two = place_in_front_or_behind(point_two)
94
+
95
+ connected_pairs.add([point_one, point_two])
96
+ connected_pairs.add([point_two, point_one])
97
+
98
+ next if road_to_building_one.nil? || road_to_building_two.nil?
99
+
100
+ road_generator.generate_roads_from_coordinate_list(road_to_building_one.concat(road_to_building_two), false)
101
+ end
102
+ end
103
+ end
104
+
105
+ def place_in_front_or_behind(point)
106
+ return [point.x, point.y - 1] if sample_area.point_within_bounds_and_can_have_road?(point.x, point.y - 1)
107
+ return [point.x, point.y + 1] if sample_area.point_within_bounds_and_can_have_road?(point.x, point.y + 1)
108
+ return [point.x - 1, point.y] if sample_area.point_within_bounds_and_can_have_road?(point.x - 1, point.y)
109
+ return [point.x + 1, point.y] if sample_area.point_within_bounds_and_can_have_road?(point.x + 1, point.y)
110
+
111
+ nil
112
+ end
113
+
114
+ def generate_roads_between_towns(verbose)
115
+ return if @all_town_points.length < 2
116
+
117
+ puts 'generating roads between towns...' if verbose
118
+
119
+ connected_pairs = Set.new
120
+ town_centroids = {}
121
+
122
+ @all_town_points.each_with_index do |town_one, idx_one|
123
+ find_town_centroid(town_one)
124
+
125
+ @all_town_points[idx_one + 1..].each do |town_two|
126
+ next if connected_pairs.include?([town_one, town_two]) || connected_pairs.include?([town_two, town_one])
127
+
128
+ town_one_center_x, town_one_center_y = (town_centroids[town_one] ||= find_town_centroid(town_one))
129
+ town_two_center_x, town_two_center_y = (town_centroids[town_two] ||= find_town_centroid(town_two))
130
+
131
+ road_generator.generate_roads_from_coordinate_list([town_one_center_x, town_one_center_y, town_two_center_x, town_two_center_y], false)
132
+
133
+ connected_pairs.add([town_one, town_two])
134
+ connected_pairs.add([town_two, town_one])
135
+ end
136
+ end
137
+ end
138
+
139
+ def find_town_centroid(points)
140
+ total_x = 0
141
+ total_y = 0
142
+ num_coordinates = points.length
143
+
144
+ points.each do |point|
145
+ total_x += point.x
146
+ total_y += point.y
147
+ end
148
+
149
+ average_x = total_x / num_coordinates.to_f
150
+ average_y = total_y / num_coordinates.to_f
151
+
152
+ [average_x, average_y]
153
+ end
154
+ end
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.5
4
+ version: 0.0.6
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-08-08 00:00:00.000000000 Z
11
+ date: 2023-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: perlin
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 1.55.1
103
+ version: 1.56.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 1.55.1
110
+ version: 1.56.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: simplecov
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -138,23 +138,30 @@ files:
138
138
  - lib/CLI/command.rb
139
139
  - lib/ansi_colours.rb
140
140
  - lib/biome.rb
141
+ - lib/building.rb
141
142
  - lib/flora.rb
143
+ - lib/flora_generator.rb
142
144
  - lib/map.rb
143
145
  - lib/map_config.rb
144
146
  - lib/map_tile_generator.rb
145
147
  - lib/pathfinding/a_star_finder.rb
146
148
  - lib/pathfinding/grid.rb
149
+ - lib/pathfinding/priority_queue.rb
150
+ - lib/poisson_disk_sampling/sample_area.rb
151
+ - lib/poisson_disk_sampling/sampler.rb
147
152
  - lib/road_generator.rb
148
153
  - lib/tile.rb
149
154
  - lib/tile_item.rb
150
155
  - lib/tile_perlin_generator.rb
156
+ - lib/town_generator.rb
151
157
  homepage: https://github.com/matthewstyler/ruby-perlin-2D-map-generator
152
158
  licenses:
153
159
  - MIT
154
160
  metadata:
155
161
  source_code_uri: https://github.com/matthewstyler/ruby-perlin-2D-map-generator
156
162
  bug_tracker_uri: https://github.com/matthewstyler/ruby-perlin-2D-map-generator/issues
157
- post_install_message:
163
+ post_install_message: Thanks for installing! Star on github if you found this useful,
164
+ or raise issues and requests.
158
165
  rdoc_options: []
159
166
  require_paths:
160
167
  - lib
@@ -172,6 +179,6 @@ requirements: []
172
179
  rubygems_version: 3.2.3
173
180
  signing_key:
174
181
  specification_version: 4
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
182
+ summary: Procedurally generate seeded and customizable 2D maps with optional roads
183
+ and towns, rendered with ansi colours or described in a 2D array of hashes
177
184
  test_files: []