From 228960d519b494c58c84f294dc29334c50440629 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sat, 5 Aug 2023 23:30:23 -0400 Subject: [PATCH 01/18] add irbrc --- .gitignore | 1 + .irbrc | 6 ++++++ Rakefile | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .irbrc diff --git a/.gitignore b/.gitignore index f17da10..493eb5f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ *.a mkmf.log Gemfile.lock +.irb_history \ No newline at end of file diff --git a/.irbrc b/.irbrc new file mode 100644 index 0000000..d6b7ca3 --- /dev/null +++ b/.irbrc @@ -0,0 +1,6 @@ +def require_all_files(directory) + Dir["#{directory}/**/*.rb"].each { |file| require file } +end + +# Require all files in the project_root directory and its subdirectories +require_all_files(File.expand_path('../', __FILE__)) \ No newline at end of file diff --git a/Rakefile b/Rakefile index 9b3a1b8..cd26ad7 100644 --- a/Rakefile +++ b/Rakefile @@ -10,7 +10,7 @@ Minitest::TestTask.create(:test) do |t| end task :irb do - exec 'irb -I lib -r ./lib/**/*' + exec 'irb -I lib' end task :lint do From 417f6808c95c06b636c29e9500b92fa00bed0906 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sat, 5 Aug 2023 23:31:50 -0400 Subject: [PATCH 02/18] fixup irbrc --- .irbrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.irbrc b/.irbrc index d6b7ca3..071db6d 100644 --- a/.irbrc +++ b/.irbrc @@ -1,6 +1,8 @@ +# frozen_string_literal: true + def require_all_files(directory) Dir["#{directory}/**/*.rb"].each { |file| require file } end # Require all files in the project_root directory and its subdirectories -require_all_files(File.expand_path('../', __FILE__)) \ No newline at end of file +require_all_files(File.expand_path(__dir__)) From dc3d0324cddf164ca305dd2b3a15b6c43ee69f88 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sat, 5 Aug 2023 23:32:18 -0400 Subject: [PATCH 03/18] initial a* pathfinder --- lib/pathfinding/a_star_finder.rb | 50 +++++++++++++++ lib/pathfinding/grid.rb | 28 +++++++++ test/pathfinding/a_star_finder_test.rb | 87 ++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 lib/pathfinding/a_star_finder.rb create mode 100644 lib/pathfinding/grid.rb create mode 100644 test/pathfinding/a_star_finder_test.rb diff --git a/lib/pathfinding/a_star_finder.rb b/lib/pathfinding/a_star_finder.rb new file mode 100644 index 0000000..dde4184 --- /dev/null +++ b/lib/pathfinding/a_star_finder.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Pathfinding + class AStarFinder + def find_path(start_node, end_node, grid) + open_set = [start_node] + came_from = {} + g_score = { start_node => 0 } + f_score = { start_node => heuristic_cost_estimate(start_node, end_node) } + + until open_set.empty? + current_node = open_set.min_by { |node| f_score[node] } + + return reconstruct_path(came_from, current_node) if current_node == end_node + + open_set.delete(current_node) + + grid.neighbors(current_node).each do |neighbor| + tentative_g_score = g_score[current_node] + 1 + + next unless !g_score[neighbor] || tentative_g_score < g_score[neighbor] + + came_from[neighbor] = current_node + g_score[neighbor] = tentative_g_score + f_score[neighbor] = g_score[neighbor] + heuristic_cost_estimate(neighbor, end_node) + + open_set << neighbor unless open_set.include?(neighbor) + end + end + + # No path found + [] + end + + private + + def heuristic_cost_estimate(node, end_node) + (node.x - end_node.x).abs + (node.y - end_node.y).abs + end + + def reconstruct_path(came_from, current_node) + path = [current_node] + while came_from[current_node] + current_node = came_from[current_node] + path.unshift(current_node) + end + path + end + end +end diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb new file mode 100644 index 0000000..38c0a9c --- /dev/null +++ b/lib/pathfinding/grid.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Pathfinding + class Grid + attr_reader :nodes + + def initialize(nodes) + @nodes = nodes + end + + def node(x, y) + nodes[y][x] + end + + def neighbors(node) + neighbors = [] + x = node.x + y = node.y + + neighbors << node(x - 1, y) if x.positive? && !node(x - 1, y).nil? + neighbors << node(x + 1, y) if x < @nodes[0].size - 1 && !node(x + 1, y).nil? + neighbors << node(x, y - 1) if y.positive? && !node(x, y - 1).nil? + neighbors << node(x, y + 1) if y < @nodes.size - 1 && !node(x, y + 1).nil? + + neighbors + end + end +end diff --git a/test/pathfinding/a_star_finder_test.rb b/test/pathfinding/a_star_finder_test.rb new file mode 100644 index 0000000..f585a28 --- /dev/null +++ b/test/pathfinding/a_star_finder_test.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'pathfinding/a_star_finder' +require 'pathfinding/grid' + +class TestAStarFinder < Minitest::Test + class Node + attr_reader :x, :y + + def initialize(x, y) + @x = x + @y = y + end + end + + def setup + @astar_finder = Pathfinding::AStarFinder.new + end + + def test_returns_empty_path_if_start_and_end_nodes_are_the_same + nodes = [ + [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0)], + [Node.new(0, 1), Node.new(1, 1), Node.new(2, 1)], + [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2)] + ] + grid = Pathfinding::Grid.new(nodes) + start_node = grid.node(1, 1) + end_node = grid.node(1, 1) + + path = @astar_finder.find_path(start_node, end_node, grid) + + assert_equal [start_node], path + end + + def test_finds_path_from_top_left_to_bottom_right + nodes = [ + [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0)], + [Node.new(0, 1), Node.new(1, 1), Node.new(2, 1)], + [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2)] + ] + grid = Pathfinding::Grid.new(nodes) + start_node = grid.node(0, 0) + end_node = grid.node(2, 2) + + path = @astar_finder.find_path(start_node, end_node, grid) + + assert_equal [start_node, grid.node(1, 0), grid.node(2, 0), grid.node(2, 1), end_node], path + end + + def test_returns_empty_path_if_no_path_is_found + nodes = [ + [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0)], + [nil, nil, nil], + [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2)] + ] + grid = Pathfinding::Grid.new(nodes) + start_node = grid.node(0, 0) + end_node = grid.node(2, 2) + + path = @astar_finder.find_path(start_node, end_node, grid) + + assert_empty path, 'No valid path should be found due to the obstacle.' + end + + def test_finds_path_with_obstacles + nodes = [ + [Node.new(0, 0), Node.new(1, 0), nil, Node.new(3, 0)], + [Node.new(0, 1), nil, nil, Node.new(3, 1)], + [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2), Node.new(3, 2)], + [Node.new(0, 3), Node.new(1, 3), Node.new(2, 3), Node.new(3, 3)] + ] + grid = Pathfinding::Grid.new(nodes) + start_node = grid.node(0, 0) + end_node = grid.node(3, 3) + + path = @astar_finder.find_path(start_node, end_node, grid) + + expected_path = [ + start_node, grid.node(0, 1), grid.node(0, 2), + grid.node(1, 2), grid.node(2, 2), grid.node(3, 2), + grid.node(3, 3) + ] + + assert_equal expected_path, path, 'The A* pathfinder should find the correct path around obstacles.' + end +end From 6586a0c51d54b48e80218ca55b8a188bea7a5fe1 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sun, 6 Aug 2023 21:18:56 -0400 Subject: [PATCH 04/18] add pry --- ruby-perlin-2D-map-generator.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/ruby-perlin-2D-map-generator.gemspec b/ruby-perlin-2D-map-generator.gemspec index b709a3a..383377c 100644 --- a/ruby-perlin-2D-map-generator.gemspec +++ b/ruby-perlin-2D-map-generator.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'minitest', '~> 5.18' s.add_development_dependency 'mocha', '~> 2.1.0' + s.add_development_dependency 'pry-byebug', '~> 3.10.1' s.add_development_dependency 'rake', '~> 13.0.6' s.add_development_dependency 'rubocop', '~> 1.55.1' s.add_development_dependency 'simplecov', '~> 0.22.0' From 5d715da1bc298b0ab8eccc429e89ee8b3dbca49c Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sun, 6 Aug 2023 21:19:59 -0400 Subject: [PATCH 05/18] initial road rendering --- lib/ansi_colours.rb | 1 + lib/map.rb | 16 +++++++++++++++- lib/road_generator.rb | 19 +++++++++++++++++++ lib/tile.rb | 28 ++++++++++++++++++++++++---- test/tile_test.rb | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 lib/road_generator.rb diff --git a/lib/ansi_colours.rb b/lib/ansi_colours.rb index 4a0eaba..5d70db0 100644 --- a/lib/ansi_colours.rb +++ b/lib/ansi_colours.rb @@ -26,6 +26,7 @@ module Background TAIGA_HIGHLAND = "\e[48;5;65m" TAIGA_COAST = "\e[48;5;17m" ICE = "\e[48;5;159m" + ROAD_BLACK = "\e[48;5;239m" ANSI_RESET = "\033[0m" end end diff --git a/lib/map.rb b/lib/map.rb index e0b660c..827c06f 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -2,6 +2,8 @@ require 'map_tile_generator' require 'map_config' +require 'road_generator' +require 'pry-byebug' class Map attr_reader :config @@ -30,6 +32,18 @@ def [](x, y) # rubocop:enable Naming/MethodParameterName: def tiles - @tiles ||= MapTileGenerator.new(map: self).generate + @tiles ||= + begin + map_tiles = generate_tiles + # binding.pry + RoadGenerator.new(map_tiles).generate(0, 0, 50, 50).each(&:make_road) + map_tiles + end + end + + private + + def generate_tiles + MapTileGenerator.new(map: self).generate end end diff --git a/lib/road_generator.rb b/lib/road_generator.rb new file mode 100644 index 0000000..a9eabcb --- /dev/null +++ b/lib/road_generator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'pathfinding/grid' +require 'pathfinding/a_star_finder' + +class RoadGenerator + attr_reader :grid, :finder + + def initialize(tiles) + @grid = Pathfinding::Grid.new(tiles) + @finder = Pathfinding::AStarFinder.new + end + + def generate(start_x, start_y, end_x, end_y) + start_node = grid.node(start_x, start_y) + end_node = grid.node(end_x, end_y) + finder.find_path(start_node, end_node, grid) + end +end diff --git a/lib/tile.rb b/lib/tile.rb index d3bc398..2d3a738 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -4,15 +4,23 @@ require 'flora' class Tile - attr_reader :x, :y, :height, :moist, :temp, :map + attr_reader :x, :y, :height, :moist, :temp, :map, :type - def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0) + TYPES = [ + :biome, + :road + ].freeze + + def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0, type: :biome) @x = x @y = y @height = height @moist = moist @temp = temp @map = map + raise ArgumentError, 'invalid tile type' unless TYPES.include?(type) + + @type = type end def surrounding_tiles(distance = 1) @@ -36,7 +44,7 @@ def items end def render_to_standard_output - print biome.colour + (!items.empty? ? item_with_highest_priority.render_symbol : ' ') + print render_color_by_type + (!items.empty? ? item_with_highest_priority.render_symbol : ' ') print AnsiColours::Background::ANSI_RESET end @@ -58,12 +66,24 @@ def to_h moist: moist, temp: temp, biome: biome.to_h, - items: items.map(&:to_h) + items: items.map(&:to_h), + type: type } end + def make_road + @type = :road + end + private + def render_color_by_type + case type + when :biome then biome.colour + when :road then "\e[48;5;239m" + end + end + def items_generated_with_flora_if_applicable if map.config.generate_flora && biome.flora_available range_max_value = map.tiles[(y - biome.flora_range)...(y + biome.flora_range)]&.map do |r| diff --git a/test/tile_test.rb b/test/tile_test.rb index 50c6cbe..40b3690 100644 --- a/test/tile_test.rb +++ b/test/tile_test.rb @@ -15,6 +15,7 @@ def setup @height = 0.5 @moist = 0.5 @temp = 0.5 + @type = :biome @tile = Tile.new( map: @map, @@ -22,7 +23,8 @@ def setup y: @y, height: @height, moist: @moist, - temp: @temp + temp: @temp, + type: @type ) end @@ -33,6 +35,7 @@ def test_initialize_with_valid_parameters assert_equal @height, @tile.height assert_equal @moist, @tile.moist assert_equal @temp, @tile.temp + assert_equal @type, @tile.type end def test_surrounding_tiles @@ -135,9 +138,34 @@ def test_to_h moist: @moist, temp: @temp, biome: biome_hash, - items: [tile_item_hash] + items: [tile_item_hash], + type: :biome } assert_equal expected_hash, @tile.to_h end + + def test_invalid_tile_type_raises_error + result = assert_raises ArgumentError, 'invalid tile type' do + Tile.new( + map: @map, + x: @x, + y: @y, + type: :not_real + ) + end + + assert_equal 'invalid tile type', result.to_s + end + + def test_make_road + tile = Tile.new( + map: @map, + x: @x, + y: @y + ) + assert_equal :biome, tile.type + tile.make_road + assert_equal :road, tile.type + end end From 0e2ef321a15e882ad187df5bb12d277afc40f6b6 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sun, 6 Aug 2023 22:31:43 -0400 Subject: [PATCH 06/18] configure num of generated roads --- lib/CLI/command.rb | 13 ++++++++- lib/map.rb | 4 +-- lib/map_config.rb | 7 +++-- lib/pathfinding/a_star_finder.rb | 4 ++- lib/pathfinding/grid.rb | 38 ++++++++++++++++++++++++++ lib/road_generator.rb | 16 ++++++++++- lib/tile.rb | 13 ++++++--- test/pathfinding/a_star_finder_test.rb | 4 +++ test/tile_test.rb | 6 +++- 9 files changed, 92 insertions(+), 13 deletions(-) diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index 09902dc..f673ae4 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -236,6 +236,16 @@ class Command default MapConfig::DEFAULT_MOIST_ADJUSTMENT end + option :roads do + long '--roads int' + long '--roads=int' + + desc 'Add this many roads through the map, starting and ending at edges' + convert Integer + validate ->(val) { val >= 0 } + default MapConfig::DEFAULT_NUM_OF_ROADS + end + flag :help do short '-h' long '--help' @@ -262,7 +272,8 @@ def execute_command perlin_height_config: perlin_height_config, perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, - generate_flora: params[:generate_flora] + generate_flora: params[:generate_flora], + roads: params[:roads] )) case params[:command] when 'render' then map.render diff --git a/lib/map.rb b/lib/map.rb index 827c06f..661ab6e 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -3,7 +3,6 @@ require 'map_tile_generator' require 'map_config' require 'road_generator' -require 'pry-byebug' class Map attr_reader :config @@ -35,8 +34,7 @@ def tiles @tiles ||= begin map_tiles = generate_tiles - # binding.pry - RoadGenerator.new(map_tiles).generate(0, 0, 50, 50).each(&:make_road) + RoadGenerator.new(map_tiles).generate_num_of_roads(config.roads) map_tiles end end diff --git a/lib/map_config.rb b/lib/map_config.rb index 77742e6..1f52758 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -25,13 +25,15 @@ class MapConfig DEFAULT_TEMP_X_FREQUENCY = 2.5 DEFAULT_TEMP_ADJUSTMENT = 0.0 + DEFAULT_NUM_OF_ROADS = 0 + PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) - attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height + attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :roads 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, - height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA) + height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, roads: DEFAULT_NUM_OF_ROADS) raise ArgumentError unless perlin_height_config.is_a?(PerlinConfig) && perlin_moist_config.is_a?(PerlinConfig) @generate_flora = generate_flora @@ -40,6 +42,7 @@ def initialize(perlin_height_config: default_perlin_height_config, perlin_moist_ @perlin_temp_config = perlin_temp_config @width = width @height = height + @roads = roads end private diff --git a/lib/pathfinding/a_star_finder.rb b/lib/pathfinding/a_star_finder.rb index dde4184..50cf0a7 100644 --- a/lib/pathfinding/a_star_finder.rb +++ b/lib/pathfinding/a_star_finder.rb @@ -35,7 +35,9 @@ def find_path(start_node, end_node, grid) private def heuristic_cost_estimate(node, end_node) - (node.x - end_node.x).abs + (node.y - end_node.y).abs + (node.x - end_node.x).abs + (node.y - end_node.y).abs + + (node.path_heuristic - end_node.path_heuristic) # elevation for natural roads end def reconstruct_path(came_from, current_node) diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb index 38c0a9c..e353c46 100644 --- a/lib/pathfinding/grid.rb +++ b/lib/pathfinding/grid.rb @@ -24,5 +24,43 @@ def neighbors(node) neighbors end + + def min_max_coordinates + @min_max_coordinates ||= begin + min_x = nil + min_y = nil + max_x = nil + max_y = nil + + @nodes.each do |row| + row.each do |object| + x = object.x + y = object.y + + # Update minimum x and y values + min_x = x if min_x.nil? || x < min_x + min_y = y if min_y.nil? || y < min_y + + # Update maximum x and y values + max_x = x if max_x.nil? || x > max_x + max_y = y if max_y.nil? || y > max_y + end + end + + { min_x: min_x, min_y: min_y, max_x: max_x, max_y: max_y } + end + end + + def edge_nodes + @edge_nodes ||= + @nodes.map do |row| + row.select do |obj| + obj.x == min_max_coordinates[:min_x] || + obj.x == min_max_coordinates[:max_x] || + obj.y == min_max_coordinates[:min_y] || + obj.y == min_max_coordinates[:max_y] + end + end.flatten + end end end diff --git a/lib/road_generator.rb b/lib/road_generator.rb index a9eabcb..7f5aa8c 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -11,7 +11,21 @@ def initialize(tiles) @finder = Pathfinding::AStarFinder.new end - def generate(start_x, start_y, end_x, end_y) + def generate_num_of_roads(x) + return if x <= 0 + + (1..x).each do |_n| + random_objects_at_edges = @grid.edge_nodes.sample(2) + generate_path( + random_objects_at_edges[0].x, + random_objects_at_edges[0].y, + random_objects_at_edges[1].x, + random_objects_at_edges[1].y + ).each(&:make_road) + end + end + + def generate_path(start_x, start_y, end_x, end_y) start_node = grid.node(start_x, start_y) end_node = grid.node(end_x, end_y) finder.find_path(start_node, end_node, grid) diff --git a/lib/tile.rb b/lib/tile.rb index 2d3a738..11fa341 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -2,13 +2,14 @@ require 'biome' require 'flora' +require 'ansi_colours' class Tile attr_reader :x, :y, :height, :moist, :temp, :map, :type - TYPES = [ - :biome, - :road + TYPES = %i[ + biome + road ].freeze def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0, type: :biome) @@ -75,12 +76,16 @@ def make_road @type = :road end + def path_heuristic + height + end + private def render_color_by_type case type when :biome then biome.colour - when :road then "\e[48;5;239m" + when :road then AnsiColours::Background::ROAD_BLACK end end diff --git a/test/pathfinding/a_star_finder_test.rb b/test/pathfinding/a_star_finder_test.rb index f585a28..7753f0d 100644 --- a/test/pathfinding/a_star_finder_test.rb +++ b/test/pathfinding/a_star_finder_test.rb @@ -12,6 +12,10 @@ def initialize(x, y) @x = x @y = y end + + def path_heuristic + 0 + end end def setup diff --git a/test/tile_test.rb b/test/tile_test.rb index 40b3690..397385b 100644 --- a/test/tile_test.rb +++ b/test/tile_test.rb @@ -154,7 +154,7 @@ def test_invalid_tile_type_raises_error type: :not_real ) end - + assert_equal 'invalid tile type', result.to_s end @@ -168,4 +168,8 @@ def test_make_road tile.make_road assert_equal :road, tile.type end + + def test_tile_path_heuristic_is_elevatione + assert_equal @tile.height, @tile.path_heuristic + end end From 9d9eb7e591574a7623c927897f6c584bc3011995 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 13:14:32 -0400 Subject: [PATCH 07/18] generated roads not on the same edge --- lib/road_generator.rb | 11 ++++++++++- test/map_test.rb | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/road_generator.rb b/lib/road_generator.rb index 7f5aa8c..e290f8a 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -15,7 +15,7 @@ def generate_num_of_roads(x) return if x <= 0 (1..x).each do |_n| - random_objects_at_edges = @grid.edge_nodes.sample(2) + random_objects_at_edges = random_nodes_not_on_same_edge generate_path( random_objects_at_edges[0].x, random_objects_at_edges[0].y, @@ -30,4 +30,13 @@ def generate_path(start_x, start_y, end_x, end_y) end_node = grid.node(end_x, end_y) finder.find_path(start_node, end_node, grid) end + + private + + def random_nodes_not_on_same_edge + loop do + node_one, node_two = @grid.edge_nodes.sample(2) + return [node_one, node_two] if node_one.x != node_two.x && node_one.y != node_two.y + end + end end diff --git a/test/map_test.rb b/test/map_test.rb index 30f496a..80cc9a2 100644 --- a/test/map_test.rb +++ b/test/map_test.rb @@ -3,6 +3,7 @@ require 'test_helper' require 'mocha/minitest' require 'map' +require 'road_generator' class MapTest < Minitest::Test def test_initialize_with_default_config @@ -18,10 +19,16 @@ def test_initialize_with_default_config def test_initialize_with_custom_config map_config = mock('MapConfig') + map_config.expects(:roads).returns(1) generator_mock = mock('MapTileGenerator') + road_generator_mock = mock('RoadGenerator') + MapTileGenerator.expects(:new).with(anything).returns(generator_mock) generator_mock.expects(:generate).returns([['Test2']]) + RoadGenerator.expects(:new).with([['Test2']]).returns(road_generator_mock) + road_generator_mock.expects(:generate_num_of_roads).with(1) + map = Map.new(map_config: map_config) assert_equal map_config, map.config From 4ca4d27b0fb3ae40b66521998aa2eb5d3b9c7ffc Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 13:45:45 -0400 Subject: [PATCH 08/18] road generation seeded --- lib/CLI/command.rb | 11 ++++++++++- lib/map.rb | 2 +- lib/map_config.rb | 14 +++++++++++--- lib/pathfinding/grid.rb | 6 +++--- lib/road_generator.rb | 18 ++++++++++++------ test/map_test.rb | 5 +++-- 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index f673ae4..dcf7354 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -246,6 +246,15 @@ class Command default MapConfig::DEFAULT_NUM_OF_ROADS end + option :road_seed do + long '--rs int' + long '--rs=int' + + desc 'The seed for generating roads' + convert Integer + default MapConfig::DEFAULT_ROAD_SEED + end + flag :help do short '-h' long '--help' @@ -273,7 +282,7 @@ def execute_command perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, generate_flora: params[:generate_flora], - roads: params[:roads] + road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads).values) )) case params[:command] when 'render' then map.render diff --git a/lib/map.rb b/lib/map.rb index 661ab6e..3c1a5c1 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -34,7 +34,7 @@ def tiles @tiles ||= begin map_tiles = generate_tiles - RoadGenerator.new(map_tiles).generate_num_of_roads(config.roads) + RoadGenerator.new(map_tiles).generate_num_of_roads(config.road_config) map_tiles end end diff --git a/lib/map_config.rb b/lib/map_config.rb index 1f52758..725437a 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -25,15 +25,19 @@ class MapConfig DEFAULT_TEMP_X_FREQUENCY = 2.5 DEFAULT_TEMP_ADJUSTMENT = 0.0 + DEFAULT_ROAD_SEED = 100 DEFAULT_NUM_OF_ROADS = 0 PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads].freeze + PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) + RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) - attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :roads + attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :road_config 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, - height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, roads: DEFAULT_NUM_OF_ROADS) + height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, road_config: default_road_config) raise ArgumentError unless perlin_height_config.is_a?(PerlinConfig) && perlin_moist_config.is_a?(PerlinConfig) @generate_flora = generate_flora @@ -42,7 +46,7 @@ def initialize(perlin_height_config: default_perlin_height_config, perlin_moist_ @perlin_temp_config = perlin_temp_config @width = width @height = height - @roads = roads + @road_config = road_config end private @@ -61,4 +65,8 @@ def default_perlin_temp_config PerlinConfig.new(DEFAULT_TILE_COUNT, DEFAULT_TILE_COUNT, DEFAULT_TEMP_SEED, DEFAULT_TEMP_OCTAVES, DEFAULT_TEMP_X_FREQUENCY, DEFAULT_TEMP_Y_FREQUENCY, DEFAULT_TEMP_PERSISTANCE, DEFAULT_TEMP_ADJUSTMENT) end + + def default_road_config + RoadConfig.new(DEFAULT_ROAD_SEED, DEFAULT_NUM_OF_ROADS) + end end diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb index e353c46..e8fa8be 100644 --- a/lib/pathfinding/grid.rb +++ b/lib/pathfinding/grid.rb @@ -56,9 +56,9 @@ def edge_nodes @nodes.map do |row| row.select do |obj| obj.x == min_max_coordinates[:min_x] || - obj.x == min_max_coordinates[:max_x] || - obj.y == min_max_coordinates[:min_y] || - obj.y == min_max_coordinates[:max_y] + obj.x == min_max_coordinates[:max_x] || + obj.y == min_max_coordinates[:min_y] || + obj.y == min_max_coordinates[:max_y] end end.flatten end diff --git a/lib/road_generator.rb b/lib/road_generator.rb index e290f8a..61e984e 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -11,11 +11,11 @@ def initialize(tiles) @finder = Pathfinding::AStarFinder.new end - def generate_num_of_roads(x) - return if x <= 0 + def generate_num_of_roads(config) + return if config.roads <= 0 - (1..x).each do |_n| - random_objects_at_edges = random_nodes_not_on_same_edge + (1..config.roads).each do |_n| + random_objects_at_edges = random_nodes_not_on_same_edge(config.road_seed) generate_path( random_objects_at_edges[0].x, random_objects_at_edges[0].y, @@ -33,9 +33,15 @@ def generate_path(start_x, start_y, end_x, end_y) private - def random_nodes_not_on_same_edge + def random_nodes_not_on_same_edge(seed) + random_generator = Random.new(seed) + length = @grid.edge_nodes.length + loop do - node_one, node_two = @grid.edge_nodes.sample(2) + index1 = random_generator.rand(length) + index2 = random_generator.rand(length) + node_one, node_two = @grid.edge_nodes.values_at(index1, index2) + return [node_one, node_two] if node_one.x != node_two.x && node_one.y != node_two.y end end diff --git a/test/map_test.rb b/test/map_test.rb index 80cc9a2..b7899d8 100644 --- a/test/map_test.rb +++ b/test/map_test.rb @@ -19,7 +19,8 @@ def test_initialize_with_default_config def test_initialize_with_custom_config map_config = mock('MapConfig') - map_config.expects(:roads).returns(1) + road_config = MapConfig::RoadConfig.new(1, 2) + map_config.expects(:road_config).returns(road_config) generator_mock = mock('MapTileGenerator') road_generator_mock = mock('RoadGenerator') @@ -27,7 +28,7 @@ def test_initialize_with_custom_config generator_mock.expects(:generate).returns([['Test2']]) RoadGenerator.expects(:new).with([['Test2']]).returns(road_generator_mock) - road_generator_mock.expects(:generate_num_of_roads).with(1) + road_generator_mock.expects(:generate_num_of_roads).with(road_config) map = Map.new(map_config: map_config) From fd7d46a30d1825b998b47538e15bc5f72fb9db37 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 13:55:37 -0400 Subject: [PATCH 09/18] each road has a unique seed --- lib/road_generator.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/road_generator.rb b/lib/road_generator.rb index 61e984e..4aa61b7 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -2,6 +2,7 @@ require 'pathfinding/grid' require 'pathfinding/a_star_finder' +require 'pry-byebug' class RoadGenerator attr_reader :grid, :finder @@ -14,8 +15,9 @@ def initialize(tiles) def generate_num_of_roads(config) return if config.roads <= 0 - (1..config.roads).each do |_n| - random_objects_at_edges = random_nodes_not_on_same_edge(config.road_seed) + seed = config.road_seed + (1..config.roads).each do |n| + random_objects_at_edges = random_nodes_not_on_same_edge(seed + n) # add n otherwise each road is the same generate_path( random_objects_at_edges[0].x, random_objects_at_edges[0].y, From 5b8fec1ca83f2b48212867db97b97ebda672f5bd Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 19:42:01 -0400 Subject: [PATCH 10/18] road colour render adjusted for height --- lib/ansi_colours.rb | 2 ++ lib/tile.rb | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/ansi_colours.rb b/lib/ansi_colours.rb index 5d70db0..5f3c0ae 100644 --- a/lib/ansi_colours.rb +++ b/lib/ansi_colours.rb @@ -26,7 +26,9 @@ module Background TAIGA_HIGHLAND = "\e[48;5;65m" TAIGA_COAST = "\e[48;5;17m" ICE = "\e[48;5;159m" + LOW_ROAD_BLACK = "\e[48;5;241m" ROAD_BLACK = "\e[48;5;239m" + HIGH_ROAD_BLACK = "\e[48;5;236m" ANSI_RESET = "\033[0m" end end diff --git a/lib/tile.rb b/lib/tile.rb index 11fa341..3525094 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -85,7 +85,15 @@ def path_heuristic def render_color_by_type case type when :biome then biome.colour - when :road then AnsiColours::Background::ROAD_BLACK + when :road + case height + when 0.66..1 + AnsiColours::Background::HIGH_ROAD_BLACK + when 0.33..0.66 + AnsiColours::Background::ROAD_BLACK + when 0..0.33 + AnsiColours::Background::LOW_ROAD_BLACK + end end end From 5519422ec973527205620105377edcc04d64eb33 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 20:34:18 -0400 Subject: [PATCH 11/18] configurable road through water exclusion --- lib/CLI/command.rb | 11 +++++++++- lib/map_config.rb | 7 ++++--- lib/pathfinding/grid.rb | 8 +++---- lib/tile.rb | 8 +++++++ test/pathfinding/a_star_finder_test.rb | 29 +++++++++++++++++++++++++- test/tile_test.rb | 19 +++++++++++++++++ 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index dcf7354..a7d360d 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -255,6 +255,15 @@ class Command default MapConfig::DEFAULT_ROAD_SEED end + option :road_exclude_water_path do + long '--road_exclude_water_path bool' + long '--road_exclude_water_path=bool' + + desc 'Controls if roads will run through water' + convert :bool + default MapConfig::DEFAULT_ROAD_EXCLUDE_WATER_PATH + end + flag :help do short '-h' long '--help' @@ -282,7 +291,7 @@ def execute_command perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, generate_flora: params[:generate_flora], - road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads).values) + road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path).values) )) case params[:command] when 'render' then map.render diff --git a/lib/map_config.rb b/lib/map_config.rb index 725437a..5b49351 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -25,11 +25,12 @@ class MapConfig DEFAULT_TEMP_X_FREQUENCY = 2.5 DEFAULT_TEMP_ADJUSTMENT = 0.0 - DEFAULT_ROAD_SEED = 100 - DEFAULT_NUM_OF_ROADS = 0 + DEFAULT_ROAD_SEED = 100 + DEFAULT_ROAD_EXCLUDE_WATER_PATH = true + DEFAULT_NUM_OF_ROADS = 0 PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze - ROAD_CONFIG_OPTIONS = %i[road_seed roads].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb index e8fa8be..8152d43 100644 --- a/lib/pathfinding/grid.rb +++ b/lib/pathfinding/grid.rb @@ -17,10 +17,10 @@ def neighbors(node) x = node.x y = node.y - neighbors << node(x - 1, y) if x.positive? && !node(x - 1, y).nil? - neighbors << node(x + 1, y) if x < @nodes[0].size - 1 && !node(x + 1, y).nil? - neighbors << node(x, y - 1) if y.positive? && !node(x, y - 1).nil? - neighbors << node(x, y + 1) if y < @nodes.size - 1 && !node(x, y + 1).nil? + neighbors << node(x - 1, y) if x.positive? && !node(x - 1, y).nil? && node.can_contain_road? + neighbors << node(x + 1, y) if x < @nodes[0].size - 1 && !node(x + 1, y).nil? && node.can_contain_road? + neighbors << node(x, y - 1) if y.positive? && !node(x, y - 1).nil? && node.can_contain_road? + neighbors << node(x, y + 1) if y < @nodes.size - 1 && !node(x, y + 1).nil? && node.can_contain_road? neighbors end diff --git a/lib/tile.rb b/lib/tile.rb index 3525094..09c6dc7 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -80,8 +80,16 @@ def path_heuristic height end + def can_contain_road? + return true unless biome_is_water_and_is_excluded? + end + private + def biome_is_water_and_is_excluded? + biome.water? && map.config.road_config.road_exclude_water_path + end + def render_color_by_type case type when :biome then biome.colour diff --git a/test/pathfinding/a_star_finder_test.rb b/test/pathfinding/a_star_finder_test.rb index 7753f0d..79c4414 100644 --- a/test/pathfinding/a_star_finder_test.rb +++ b/test/pathfinding/a_star_finder_test.rb @@ -8,14 +8,19 @@ class TestAStarFinder < Minitest::Test class Node attr_reader :x, :y - def initialize(x, y) + def initialize(x, y, can_contain_road = true) @x = x @y = y + @can_contain_road = can_contain_road end def path_heuristic 0 end + + def can_contain_road? + @can_contain_road + end end def setup @@ -88,4 +93,26 @@ def test_finds_path_with_obstacles assert_equal expected_path, path, 'The A* pathfinder should find the correct path around obstacles.' end + + def test_finds_path_with_non_walkable_paths + nodes = [ + [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0, false), Node.new(3, 0)], + [Node.new(0, 1), Node.new(1, 1, false), Node.new(2, 1, false), Node.new(3, 1)], + [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2), Node.new(3, 2)], + [Node.new(0, 3), Node.new(1, 3), Node.new(2, 3), Node.new(3, 3)] + ] + grid = Pathfinding::Grid.new(nodes) + start_node = grid.node(0, 0) + end_node = grid.node(3, 3) + + path = @astar_finder.find_path(start_node, end_node, grid) + + expected_path = [ + start_node, grid.node(0, 1), grid.node(0, 2), + grid.node(1, 2), grid.node(2, 2), grid.node(3, 2), + grid.node(3, 3) + ] + + assert_equal expected_path, path, 'The A* pathfinder should find the correct path around obstacles.' + end end diff --git a/test/tile_test.rb b/test/tile_test.rb index 397385b..cad414c 100644 --- a/test/tile_test.rb +++ b/test/tile_test.rb @@ -172,4 +172,23 @@ def test_make_road def test_tile_path_heuristic_is_elevatione assert_equal @tile.height, @tile.path_heuristic end + + def test_tile_can_contain_road + tile = Tile.new( + map: @map, + x: @x, + y: @y, + height: @height, + moist: @moist, + temp: @temp, + type: @type + ) + + assert tile.can_contain_road? + + tile.biome.expects(:water?).returns(true) + @map.config.expects(:road_config).returns(OpenStruct.new(road_exclude_water_path: true)) + + refute tile.can_contain_road? + end end From 6e1ccd0ceeaa8409f0f357a8c5aaaf5c3e956cfa Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 20:46:26 -0400 Subject: [PATCH 12/18] configurable road through mountain exclusion --- lib/CLI/command.rb | 11 ++++++++++- lib/biome.rb | 10 ++++++++++ lib/map_config.rb | 9 +++++---- lib/tile.rb | 6 +++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index a7d360d..af4be01 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -264,6 +264,15 @@ class Command default MapConfig::DEFAULT_ROAD_EXCLUDE_WATER_PATH end + option :road_exclude_mountain_path do + long '--road_exclude_mountain_path bool' + long '--road_exclude_mountain_path=bool' + + desc 'Controls if roads will run through high mountains' + convert :bool + default MapConfig::DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH + end + flag :help do short '-h' long '--help' @@ -291,7 +300,7 @@ def execute_command perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, generate_flora: params[:generate_flora], - road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path).values) + road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path, :road_exclude_mountain_path).values) )) case params[:command] when 'render' then map.render diff --git a/lib/biome.rb b/lib/biome.rb index f48c82a..d316e86 100644 --- a/lib/biome.rb +++ b/lib/biome.rb @@ -32,6 +32,10 @@ def taiga? TAIGA_TERRAIN.include?(self) end + def high_mountain? + HIGH_MOUNTAIN.include?(self) + end + def flora_available !flora_range.nil? end @@ -132,6 +136,12 @@ def self.from(elevation, moist, temp) TAIGA_COAST ].freeze + HIGH_MOUNTAIN = [ + SNOW, + ROCKS, + MOUNTAIN + ].freeze + LAND_TERRAIN = (ALL_TERRAIN - WATER_TERRAIN).freeze class << self diff --git a/lib/map_config.rb b/lib/map_config.rb index 5b49351..0c239e9 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -25,12 +25,13 @@ class MapConfig DEFAULT_TEMP_X_FREQUENCY = 2.5 DEFAULT_TEMP_ADJUSTMENT = 0.0 - DEFAULT_ROAD_SEED = 100 - DEFAULT_ROAD_EXCLUDE_WATER_PATH = true - DEFAULT_NUM_OF_ROADS = 0 + DEFAULT_ROAD_SEED = 100 + DEFAULT_NUM_OF_ROADS = 0 + DEFAULT_ROAD_EXCLUDE_WATER_PATH = true + DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH = true PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze - ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) diff --git a/lib/tile.rb b/lib/tile.rb index 09c6dc7..8050410 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -81,7 +81,7 @@ def path_heuristic end def can_contain_road? - return true unless biome_is_water_and_is_excluded? + return true unless biome_is_water_and_is_excluded? || biome_is_high_mountain_and_is_excluded? end private @@ -90,6 +90,10 @@ def biome_is_water_and_is_excluded? biome.water? && map.config.road_config.road_exclude_water_path end + def biome_is_high_mountain_and_is_excluded? + biome.high_mountain? && map.config.road_config.road_exclude_mountain_path + end + def render_color_by_type case type when :biome then biome.colour From 76ce2434bb68d00166eb00b3a01d11e9718bf182 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 21:23:47 -0400 Subject: [PATCH 13/18] configurable road through flora exclusion --- lib/CLI/command.rb | 11 ++++++++++- lib/map.rb | 10 ++++------ lib/map_config.rb | 3 ++- lib/road_generator.rb | 1 - lib/tile.rb | 11 ++++++++++- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index af4be01..3fde2cc 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -273,6 +273,15 @@ class Command default MapConfig::DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH end + option :road_exclude_flora_path do + long '--road_exclude_flora_path bool' + long '--road_exclude_flora_path=bool' + + desc 'Controls if roads will run tiles containing flora' + convert :bool + default MapConfig::DEFAULT_ROAD_EXCLUDE_FLORA_PATH + end + flag :help do short '-h' long '--help' @@ -300,7 +309,7 @@ def execute_command perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, generate_flora: params[:generate_flora], - road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path, :road_exclude_mountain_path).values) + road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path, :road_exclude_mountain_path, :road_exclude_flora_path).values) )) case params[:command] when 'render' then map.render diff --git a/lib/map.rb b/lib/map.rb index 3c1a5c1..d9cdf06 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -31,12 +31,10 @@ def [](x, y) # rubocop:enable Naming/MethodParameterName: def tiles - @tiles ||= - begin - map_tiles = generate_tiles - RoadGenerator.new(map_tiles).generate_num_of_roads(config.road_config) - map_tiles - end + return @tiles if @tiles + @tiles = generate_tiles + RoadGenerator.new(@tiles).generate_num_of_roads(config.road_config) + @tiles end private diff --git a/lib/map_config.rb b/lib/map_config.rb index 0c239e9..7069b13 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -29,9 +29,10 @@ class MapConfig DEFAULT_NUM_OF_ROADS = 0 DEFAULT_ROAD_EXCLUDE_WATER_PATH = true DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH = true + DEFAULT_ROAD_EXCLUDE_FLORA_PATH = true PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze - ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) diff --git a/lib/road_generator.rb b/lib/road_generator.rb index 4aa61b7..9632ca8 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -2,7 +2,6 @@ require 'pathfinding/grid' require 'pathfinding/a_star_finder' -require 'pry-byebug' class RoadGenerator attr_reader :grid, :finder diff --git a/lib/tile.rb b/lib/tile.rb index 8050410..9b2adb3 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -3,6 +3,7 @@ require 'biome' require 'flora' require 'ansi_colours' +require 'pry-byebug' class Tile attr_reader :x, :y, :height, :moist, :temp, :map, :type @@ -59,6 +60,10 @@ def item_with_highest_priority items.max_by(&:render_priority) end + def items_contain_flora? + items.any? { |i| i.is_a?(Flora) } + end + def to_h { x: x, @@ -81,7 +86,7 @@ def path_heuristic end def can_contain_road? - return true unless biome_is_water_and_is_excluded? || biome_is_high_mountain_and_is_excluded? + return true unless biome_is_water_and_is_excluded? || biome_is_high_mountain_and_is_excluded? || tile_contains_flora_and_is_excluded? end private @@ -94,6 +99,10 @@ def biome_is_high_mountain_and_is_excluded? biome.high_mountain? && map.config.road_config.road_exclude_mountain_path end + def tile_contains_flora_and_is_excluded? + map.config.road_config.road_exclude_flora_path && items_contain_flora? + end + def render_color_by_type case type when :biome then biome.colour From 686705bc6ea275af96544882dd651970b9c16f5d Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 22:00:51 -0400 Subject: [PATCH 14/18] update pathfinding heuristic to favour existing roads --- lib/map.rb | 1 + lib/pathfinding/a_star_finder.rb | 7 ++++--- lib/tile.rb | 6 +++++- test/pathfinding/a_star_finder_test.rb | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/map.rb b/lib/map.rb index d9cdf06..c69dbd5 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -32,6 +32,7 @@ def [](x, y) def tiles return @tiles if @tiles + @tiles = generate_tiles RoadGenerator.new(@tiles).generate_num_of_roads(config.road_config) @tiles diff --git a/lib/pathfinding/a_star_finder.rb b/lib/pathfinding/a_star_finder.rb index 50cf0a7..5689f9f 100644 --- a/lib/pathfinding/a_star_finder.rb +++ b/lib/pathfinding/a_star_finder.rb @@ -35,9 +35,10 @@ def find_path(start_node, end_node, grid) private def heuristic_cost_estimate(node, end_node) - (node.x - end_node.x).abs - (node.y - end_node.y).abs - + (node.path_heuristic - end_node.path_heuristic) # elevation for natural roads + (node.x - end_node.x).abs + + (node.y - end_node.y).abs + + (node.path_heuristic - end_node.path_heuristic) + # elevation for natural roads + (node.road? ? 0 : 5) # share existing roads end def reconstruct_path(came_from, current_node) diff --git a/lib/tile.rb b/lib/tile.rb index 9b2adb3..f352325 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -81,6 +81,10 @@ def make_road @type = :road end + def road? + @type == :road + end + def path_heuristic height end @@ -100,7 +104,7 @@ def biome_is_high_mountain_and_is_excluded? end def tile_contains_flora_and_is_excluded? - map.config.road_config.road_exclude_flora_path && items_contain_flora? + items_contain_flora? && map.config.road_config.road_exclude_flora_path end def render_color_by_type diff --git a/test/pathfinding/a_star_finder_test.rb b/test/pathfinding/a_star_finder_test.rb index 79c4414..f39cfe1 100644 --- a/test/pathfinding/a_star_finder_test.rb +++ b/test/pathfinding/a_star_finder_test.rb @@ -21,6 +21,10 @@ def path_heuristic def can_contain_road? @can_contain_road end + + def road? + false + end end def setup From bf2e2068d5ce0df1d270c27b32f4cd67ada836ff Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Mon, 7 Aug 2023 23:14:22 -0400 Subject: [PATCH 15/18] readme update, tests, tweaks --- README.md | 112 ++++++++++++++++++--------- lib/CLI/command.rb | 5 +- lib/pathfinding/grid.rb | 14 +++- lib/tile.rb | 6 +- ruby-perlin-2D-map-generator.gemspec | 6 +- test/pathfinding/grid_test.rb | 52 +++++++++++++ test/tile_test.rb | 6 +- 7 files changed, 151 insertions(+), 50 deletions(-) create mode 100644 test/pathfinding/grid_test.rb diff --git a/README.md b/README.md index e8d4901..ada7252 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![CodeQL](https://github.com/matthewstyler/ruby-perlin-2D-map-generator/workflows/CodeQL/badge.svg) [![Downloads](https://img.shields.io/gem/dt/ruby-perlin-2D-map-generator.svg?style=flat)](https://rubygems.org/gems/ruby-perlin-2D-map-generator) -A gem that procedurally generates seeded and customizable 2D map using perlin noise. +A gem that procedurally generates seeded and customizable 2D map with optional roads using perlin noise. Include the gem in your project, or use the executable from the command line. @@ -34,6 +34,7 @@ gem install ruby-perlin-2D-map-generator See Command line Usage for full customization, below are some examples. Alter the temperature, moisture or elevation seeds to alter these maps: - Plains with random terrain evens: `ruby-perlin-2D-map-generator render` +- Plains with random terrain events and two roads: `ruby-perlin-2D-map-generator render --roads=2` - Desert (increase temperature, decrease moisture): `ruby-perlin-2D-map-generator render --temp=100 --moisture=-100` - Mountainous with lakes (increase elevation, increase moisture) `ruby-perlin-2D-map-generator render --elevation=25 --moisture=25` - Islands (decreaes elevation, increase moisture): `ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25` @@ -44,12 +45,18 @@ See Command line Usage for full customization, below are some examples. Alter th --width=int The width of the generated map (default 128) --height=int The height of the generated map (default 128) +--roads=int Add this many roads through the map, + starting and ending at edges + (default 0) + --hs=int The seed for a terrains height perlin generation (default 10) --ms=int The seed for a terrains moist perlin generation (default 300) --ts=int The seed for a terrains temperature perlin generation (default 3000) +--rs=int The seed for generating roads + (default 100) --elevation=float Adjust each generated elevation by this percent (0 - 100) (default 0.0) @@ -59,6 +66,16 @@ See Command line Usage for full customization, below are some examples. Alter th - 100) (default 0.0) ``` +## Roads and the heuristic +Roads can be generated by providing a positive integer to the `roads=` argument. Roads are randomly seeded to begin +and start at an axis (but not the same axis). + +A* pathfinding is used to generate the roads with a heuristic that uses manhattan distance, favours existing roads and similar elevations in adjacent tiles. + +Roads can be configured to include/exclude generating paths thorugh water, mountains and flora. + +Tiles containing roads are of type `road`, those without are of type `terrain`. + # Generate without rendering ```irb @@ -112,7 +129,7 @@ $ ruby-perlin-2D-map-generator --help ```bash Usage: ruby-perlin-2D-map-generator [OPTIONS] (DESCRIBE | RENDER) -Generate a seeded customizable procedurally generated 2D map. +Generate a seeded customizable procedurally generated 2D map with optional roads. Rendered in the console using ansi colours, or described as a 2D array of hashes with each tiles information. @@ -128,40 +145,60 @@ Keywords: coordinate tile details Options: - --elevation=float Adjust each generated elevation by this percent (0 - - 100) (default 0.0) - --fhx=float The frequency for height generation across the x-axis - (default 2.5) - --fhy=float The frequency for height generation across the y-axis - (default 2.5) - --fmx=float The frequency for moist generation across the x-axis - (default 2.5) - --fmy=float The frequency for moist generation across the y-axis - (default 2.5) - --ftx=float The frequency for temp generation across the x-axis - (default 2.5) - --fty=float The frequency for temp generation across the y-axis - (default 2.5) - --gf=bool Generate flora, significantly affects performance - --height=int The height of the generated map (default 128) - -h, --help Print usage - --hs=int The seed for a terrains height perlin generation - (default 10) - --moisture=float Adjust each generated moisture by this percent (0 - - 100) (default 0.0) - --ms=int The seed for a terrains moist perlin generation - (default 300) - --oh=int Octaves for height generation (default 3) - --om=int Octaves for moist generation (default 3) - --ot=int Octaves for temp generation (default 3) - --ph=float Persistance for height generation (default 1.0) - --pm=float Persistance for moist generation (default 1.0) - --pt=float Persistance for temp generation (default 1.0) - --temp=float Adjust each generated temperature by this percent (0 - - 100) (default 0.0) - --ts=int The seed for a terrains temperature perlin generation - (default 3000) - --width=int The width of the generated map (default 128) + --elevation=float Adjust each generated elevation by + this percent (0 - 100) (default 0.0) + --fhx=float The frequency for height generation + across the x-axis (default 2.5) + --fhy=float The frequency for height generation + across the y-axis (default 2.5) + --fmx=float The frequency for moist generation + across the x-axis (default 2.5) + --fmy=float The frequency for moist generation + across the y-axis (default 2.5) + --ftx=float The frequency for temp generation + across the x-axis (default 2.5) + --fty=float The frequency for temp generation + across the y-axis (default 2.5) + --gf=bool Generate flora, significantly affects + performance + --height=int The height of the generated map + (default 128) + -h, --help Print usage + --hs=int The seed for a terrains height perlin + generation (default 10) + --moisture=float Adjust each generated moisture by + this percent (0 - 100) (default 0.0) + --ms=int The seed for a terrains moist perlin + generation (default 300) + --oh=int Octaves for height generation + (default 3) + --om=int Octaves for moist generation (default + 3) + --ot=int Octaves for temp generation (default + 3) + --ph=float Persistance for height generation + (default 1.0) + --pm=float Persistance for moist generation + (default 1.0) + --pt=float Persistance for temp generation + (default 1.0) + --road_exclude_flora_path=bool Controls if roads will run tiles + containing flora + --road_exclude_mountain_path=bool Controls if roads will run through + high mountains + --road_exclude_water_path=bool Controls if roads will run through + water + --roads=int Add this many roads through the map, + starting and ending at edges (default + 0) + --rs=int The seed for generating roads + (default 100) + --temp=float Adjust each generated temperature by + this percent (0 - 100) (default 0.0) + --ts=int The seed for a terrains temperature + perlin generation (default 3000) + --width=int The width of the generated map + (default 128) Examples: Render with defaults @@ -169,6 +206,9 @@ Examples: Render with options $ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1 + + Render with roads + $ ruby-perlin-2D-map-generator render --roads=2 Describe tile [1, 1] $ ruby-perlin-2D-map-generator describe coordinates=1,1 diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index 3fde2cc..e01a8ca 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -12,7 +12,7 @@ class Command no_command - desc 'Generate a seeded customizable procedurally generated 2D map. Rendered in the console ' \ + desc 'Generate a seeded customizable procedurally generated 2D map with optional roads. Rendered in the console ' \ ' using ansi colours, or described as a 2D array of hashes with each tiles information.' example 'Render with defaults', @@ -20,6 +20,9 @@ class Command example 'Render with options', ' $ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1' + + example 'Render with roads', + ' $ ruby-perlin-2D-map-generator render --roads=2' example 'Describe tile [1, 1]', ' $ ruby-perlin-2D-map-generator describe coordinates=1,1' diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb index 8152d43..90da1f3 100644 --- a/lib/pathfinding/grid.rb +++ b/lib/pathfinding/grid.rb @@ -14,13 +14,19 @@ def node(x, y) def neighbors(node) neighbors = [] + return neighbors unless node.can_contain_road? + x = node.x y = node.y - neighbors << node(x - 1, y) if x.positive? && !node(x - 1, y).nil? && node.can_contain_road? - neighbors << node(x + 1, y) if x < @nodes[0].size - 1 && !node(x + 1, y).nil? && node.can_contain_road? - neighbors << node(x, y - 1) if y.positive? && !node(x, y - 1).nil? && node.can_contain_road? - neighbors << node(x, y + 1) if y < @nodes.size - 1 && !node(x, y + 1).nil? && node.can_contain_road? + node_lookup = node(x - 1, y) if x.positive? + neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road? + node_lookup = node(x + 1, y) if x < @nodes[0].size - 1 + neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road? + node_lookup = node(x, y - 1) if y.positive? + neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road? + node_lookup = node(x, y + 1) if y < @nodes.size - 1 + neighbors << node_lookup if !node_lookup.nil? && node_lookup.can_contain_road? neighbors end diff --git a/lib/tile.rb b/lib/tile.rb index f352325..09e6a9d 100644 --- a/lib/tile.rb +++ b/lib/tile.rb @@ -9,11 +9,11 @@ class Tile attr_reader :x, :y, :height, :moist, :temp, :map, :type TYPES = %i[ - biome + terrain road ].freeze - def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0, type: :biome) + def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0, type: :terrain) @x = x @y = y @height = height @@ -109,7 +109,7 @@ def tile_contains_flora_and_is_excluded? def render_color_by_type case type - when :biome then biome.colour + when :terrain then biome.colour when :road case height when 0.66..1 diff --git a/ruby-perlin-2D-map-generator.gemspec b/ruby-perlin-2D-map-generator.gemspec index 383377c..ec7fe1f 100644 --- a/ruby-perlin-2D-map-generator.gemspec +++ b/ruby-perlin-2D-map-generator.gemspec @@ -3,9 +3,9 @@ Gem::Specification.new do |s| s.name = 'ruby-perlin-2D-map-generator' s.version = '0.0.4' - s.summary = 'Procedurally generate seeded and customizable 2D maps, rendered with ansi colours or described in a 2D array of hashes' - s.description = 'A gem that procedurally generates a seeded and customizable 2D map using perlin noise. Map can be rendered in console ' \ - 'using ansi colors or returned as 2D array of hashes describing each tile and binome. Completely' \ + s.summary = 'Procedurally generate seeded and customizable 2D maps with optional rodes, rendered with ansi colours or described in a 2D array of hashes' + s.description = 'A gem that procedurally generates a seeded and customizable 2D map with optional roads using perlin noise. 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.' s.authors = ['Tyler Matthews (matthewstyler)'] s.email = 'matthews.tyl@gmail.com' diff --git a/test/pathfinding/grid_test.rb b/test/pathfinding/grid_test.rb new file mode 100644 index 0000000..0e0f601 --- /dev/null +++ b/test/pathfinding/grid_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' +require 'pathfinding/grid' + +class GridTest < Minitest::Test + def setup + # Sample grid data for testing + nodes = [ + [Node.new(0, 0, false), Node.new(1, 0, true), Node.new(2, 0, true)], + [Node.new(0, 1, true), Node.new(1, 1, true), Node.new(2, 1, true)], + [Node.new(0, 2, true), Node.new(1, 2, true), Node.new(2, 2, false)] + ] + @grid = Pathfinding::Grid.new(nodes) + end + + class Node + # Replace this with your Node class implementation or use a mock/fake Node class. + attr_reader :x, :y + + def initialize(x, y, road) + @x = x + @y = y + @road = road + end + + def can_contain_road? + @road + end + end + + def test_node_method_returns_correct_node + node = @grid.node(1, 2) + assert_instance_of Node, node + assert_equal 1, node.x + assert_equal 2, node.y + end + + def test_neighbors_method_returns_correct_neighbors + # In this sample grid, (1, 1) node has four neighbors with roads (top, bottom, left, right) + neighbors = @grid.neighbors(@grid.node(1, 1)) + assert_equal 4, neighbors.size + + # Assuming Node objects have a `can_contain_road?` method + neighbors.each do |neighbor| + assert_equal true, neighbor.can_contain_road? + end + end + + def test_min_max_coordinates_method_returns_correct_values + min_max = @grid.min_max_coordinates + assert_equal({ min_x: 0, min_y: 0, max_x: 2, max_y: 2 }, min_max) + end +end diff --git a/test/tile_test.rb b/test/tile_test.rb index cad414c..481f206 100644 --- a/test/tile_test.rb +++ b/test/tile_test.rb @@ -15,7 +15,7 @@ def setup @height = 0.5 @moist = 0.5 @temp = 0.5 - @type = :biome + @type = :terrain @tile = Tile.new( map: @map, @@ -139,7 +139,7 @@ def test_to_h temp: @temp, biome: biome_hash, items: [tile_item_hash], - type: :biome + type: :terrain } assert_equal expected_hash, @tile.to_h @@ -164,7 +164,7 @@ def test_make_road x: @x, y: @y ) - assert_equal :biome, tile.type + assert_equal :terrain, tile.type tile.make_road assert_equal :road, tile.type end From 1b4fcb673cc2a271b508f046b2d468e799cb8716 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Tue, 8 Aug 2023 13:39:33 -0400 Subject: [PATCH 16/18] create roads from provided coordinates --- README.md | 6 ++++++ lib/CLI/command.rb | 13 +++++++++++-- lib/map.rb | 4 +++- lib/map_config.rb | 5 +++-- lib/road_generator.rb | 13 ++++++++++++- test/map_test.rb | 7 ++++--- test/pathfinding/grid_test.rb | 2 ++ 7 files changed, 41 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ada7252..6bfc025 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Roads can be configured to include/exclude generating paths thorugh water, mount Tiles containing roads are of type `road`, those without are of type `terrain`. +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` + # Generate without rendering ```irb @@ -191,6 +193,10 @@ Options: --roads=int Add this many roads through the map, starting and ending at edges (default 0) + --roads_to_make ints Attempt to create a road from a start + and end point (4 integers), can be + supplied multiple paths + (default []) --rs=int The seed for generating roads (default 100) --temp=float Adjust each generated temperature by diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index e01a8ca..16767e1 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -20,7 +20,7 @@ class Command example 'Render with options', ' $ ruby-perlin-2D-map-generator render --elevation=-40 --moisture=25 --hs=1' - + example 'Render with roads', ' $ ruby-perlin-2D-map-generator render --roads=2' @@ -44,6 +44,15 @@ class Command desc 'Used with the describe command, only returns the given coordinate tile details' end + option :roads_to_make do + arity one + long "--roads_to_make ints" + convert :int_list + validate ->(v) { v >= 0 } + desc 'Attempt to create a road from a start and end point (4 integers), can be supplied multiple paths' + default MapConfig::DEFAULT_ROADS_TO_MAKE + end + option :height_seed do long '--hs int' # or @@ -312,7 +321,7 @@ def execute_command perlin_moist_config: perlin_moist_config, perlin_temp_config: perlin_temp_config, generate_flora: params[:generate_flora], - road_config: MapConfig::RoadConfig.new(*params.to_h.slice(:road_seed, :roads, :road_exclude_water_path, :road_exclude_mountain_path, :road_exclude_flora_path).values) + 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) )) case params[:command] when 'render' then map.render diff --git a/lib/map.rb b/lib/map.rb index c69dbd5..1027adb 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -34,7 +34,9 @@ def tiles return @tiles if @tiles @tiles = generate_tiles - RoadGenerator.new(@tiles).generate_num_of_roads(config.road_config) + road_generator = RoadGenerator.new(@tiles) + road_generator.generate_num_of_random_roads(config.road_config) + road_generator.generate_roads_from_coordinate_list(config.road_config.roads_to_make) @tiles end diff --git a/lib/map_config.rb b/lib/map_config.rb index 7069b13..0c2a9b5 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -30,9 +30,10 @@ class MapConfig DEFAULT_ROAD_EXCLUDE_WATER_PATH = true DEFAULT_ROAD_EXCLUDE_MOUNTAIN_PATH = true DEFAULT_ROAD_EXCLUDE_FLORA_PATH = true + DEFAULT_ROADS_TO_MAKE = [].freeze PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze - ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path roads_to_make].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) @@ -70,6 +71,6 @@ def default_perlin_temp_config end def default_road_config - RoadConfig.new(DEFAULT_ROAD_SEED, DEFAULT_NUM_OF_ROADS) + 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) end end diff --git a/lib/road_generator.rb b/lib/road_generator.rb index 9632ca8..9a45a96 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -11,7 +11,7 @@ def initialize(tiles) @finder = Pathfinding::AStarFinder.new end - def generate_num_of_roads(config) + def generate_num_of_random_roads(config) return if config.roads <= 0 seed = config.road_seed @@ -26,6 +26,17 @@ def generate_num_of_roads(config) end end + def generate_roads_from_coordinate_list(road_paths) + road_paths.each_slice(4) do |road_coordinates| + generate_path( + road_coordinates[0], + road_coordinates[1], + road_coordinates[2], + road_coordinates[3] + ).each(&:make_road) + end + end + def generate_path(start_x, start_y, end_x, end_y) start_node = grid.node(start_x, start_y) end_node = grid.node(end_x, end_y) diff --git a/test/map_test.rb b/test/map_test.rb index b7899d8..ac89e05 100644 --- a/test/map_test.rb +++ b/test/map_test.rb @@ -19,8 +19,8 @@ def test_initialize_with_default_config def test_initialize_with_custom_config map_config = mock('MapConfig') - road_config = MapConfig::RoadConfig.new(1, 2) - map_config.expects(:road_config).returns(road_config) + road_config = MapConfig::RoadConfig.new(1, 2, 3, 4, 5, [1, 2, 3, 4]) + map_config.expects(:road_config).twice.returns(road_config) generator_mock = mock('MapTileGenerator') road_generator_mock = mock('RoadGenerator') @@ -28,7 +28,8 @@ def test_initialize_with_custom_config generator_mock.expects(:generate).returns([['Test2']]) RoadGenerator.expects(:new).with([['Test2']]).returns(road_generator_mock) - road_generator_mock.expects(:generate_num_of_roads).with(road_config) + road_generator_mock.expects(:generate_num_of_random_roads).with(road_config) + road_generator_mock.expects(:generate_roads_from_coordinate_list).with([1, 2, 3, 4]) map = Map.new(map_config: map_config) diff --git a/test/pathfinding/grid_test.rb b/test/pathfinding/grid_test.rb index 0e0f601..fe308df 100644 --- a/test/pathfinding/grid_test.rb +++ b/test/pathfinding/grid_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' require 'pathfinding/grid' From 8b7f560e3cf69294eb333b7d7497d156e2724f87 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Tue, 8 Aug 2023 13:41:45 -0400 Subject: [PATCH 17/18] fixup gemspec --- ruby-perlin-2D-map-generator.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby-perlin-2D-map-generator.gemspec b/ruby-perlin-2D-map-generator.gemspec index ec7fe1f..cb4ef4f 100644 --- a/ruby-perlin-2D-map-generator.gemspec +++ b/ruby-perlin-2D-map-generator.gemspec @@ -3,7 +3,7 @@ Gem::Specification.new do |s| s.name = 'ruby-perlin-2D-map-generator' s.version = '0.0.4' - s.summary = 'Procedurally generate seeded and customizable 2D maps with optional rodes, rendered with ansi colours or described in a 2D array of hashes' + s.summary = 'Procedurally generate seeded and customizable 2D maps with optional roads, rendered with ansi colours or described in a 2D array of hashes' s.description = 'A gem that procedurally generates a seeded and customizable 2D map with optional roads using perlin noise. 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.' From 76ece5eb7921278735b9c794d988ecdf6abcdf11 Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Tue, 8 Aug 2023 14:37:31 -0400 Subject: [PATCH 18/18] linting, doc updates --- .rubocop_todo.yml | 6 +++++- README.md | 12 ++++++------ lib/CLI/command.rb | 12 +++++------- lib/map_config.rb | 25 ++++++++++++++++++------- lib/pathfinding/a_star_finder.rb | 4 ++++ lib/pathfinding/grid.rb | 6 ++++++ lib/road_generator.rb | 3 +++ test/map_config_test.rb | 12 +++--------- test/pathfinding/a_star_finder_test.rb | 8 +++++--- test/pathfinding/grid_test.rb | 2 ++ 10 files changed, 57 insertions(+), 33 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6f68542..f648e40 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -29,7 +29,9 @@ Metrics/AbcSize: Metrics/ClassLength: Exclude: - 'lib/CLI/command.rb' - Max: 197 + - 'lib/biome.rb' + - 'test/**/*' + Max: 150 # Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -45,6 +47,8 @@ Metrics/MethodLength: # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Max: 6 + Exclude: + - 'lib/tile.rb' # Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns. diff --git a/README.md b/README.md index 6bfc025..cf5421b 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,11 @@ See Command line Usage for full customization, below are some examples. Alter th --rs=int The seed for generating roads (default 100) ---elevation=float Adjust each generated elevation by this percent (0 - +--elevation=float Adjust each generated elevation by this percent (-100 - 100) (default 0.0) ---moisture=float Adjust each generated moisture by this percent (0 - +--moisture=float Adjust each generated moisture by this percent (-100 - 100) (default 0.0) ---temp=float Adjust each generated temperature by this percent (0 +--temp=float Adjust each generated temperature by this percent (-100 - 100) (default 0.0) ``` @@ -148,7 +148,7 @@ Keywords: Options: --elevation=float Adjust each generated elevation by - this percent (0 - 100) (default 0.0) + this percent (-100 - 100) (default 0.0) --fhx=float The frequency for height generation across the x-axis (default 2.5) --fhy=float The frequency for height generation @@ -169,7 +169,7 @@ Options: --hs=int The seed for a terrains height perlin generation (default 10) --moisture=float Adjust each generated moisture by - this percent (0 - 100) (default 0.0) + this percent (-100 - 100) (default 0.0) --ms=int The seed for a terrains moist perlin generation (default 300) --oh=int Octaves for height generation @@ -200,7 +200,7 @@ Options: --rs=int The seed for generating roads (default 100) --temp=float Adjust each generated temperature by - this percent (0 - 100) (default 0.0) + this percent (-100 - 100) (default 0.0) --ts=int The seed for a terrains temperature perlin generation (default 3000) --width=int The width of the generated map diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb index 16767e1..4060274 100644 --- a/lib/CLI/command.rb +++ b/lib/CLI/command.rb @@ -46,7 +46,7 @@ class Command option :roads_to_make do arity one - long "--roads_to_make ints" + long '--roads_to_make ints' convert :int_list validate ->(v) { v >= 0 } desc 'Attempt to create a road from a start and end point (4 integers), can be supplied multiple paths' @@ -222,7 +222,7 @@ class Command long '--temp float' long '--temp=float' - desc 'Adjust each generated temperature by this percent (0 - 100)' + desc 'Adjust each generated temperature by this percent (-100 - 100)' convert ->(val) { val.to_f / 100.0 } validate ->(val) { val >= -1.0 && val <= 1.0 } default MapConfig::DEFAULT_TEMP_ADJUSTMENT @@ -232,7 +232,7 @@ class Command long '--elevation float' long '--elevation=float' - desc 'Adjust each generated elevation by this percent (0 - 100)' + desc 'Adjust each generated elevation by this percent (-100 - 100)' convert ->(val) { val.to_f / 100.0 } validate ->(val) { val >= -1.0 && val <= 1.0 } default MapConfig::DEFAULT_HEIGHT_ADJUSTMENT @@ -242,7 +242,7 @@ class Command long '--moisture float' long '--moisture=float' - desc 'Adjust each generated moisture by this percent (0 - 100)' + desc 'Adjust each generated moisture by this percent (-100 - 100)' convert ->(val) { val.to_f / 100.0 } validate ->(val) { val >= -1.0 && val <= 1.0 } default MapConfig::DEFAULT_MOIST_ADJUSTMENT @@ -317,9 +317,7 @@ def execute_command map = Map.new(map_config: MapConfig.new( width: params[:width], height: params[:height], - perlin_height_config: perlin_height_config, - perlin_moist_config: perlin_moist_config, - perlin_temp_config: perlin_temp_config, + all_perlin_configs: MapConfig::AllPerlinConfigs.new(perlin_height_config, perlin_moist_config, perlin_temp_config), generate_flora: params[:generate_flora], 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) )) diff --git a/lib/map_config.rb b/lib/map_config.rb index 0c2a9b5..c168607 100644 --- a/lib/map_config.rb +++ b/lib/map_config.rb @@ -33,21 +33,22 @@ class MapConfig DEFAULT_ROADS_TO_MAKE = [].freeze PERLIN_CONFIG_OPTIONS = %i[width height noise_seed octaves x_frequency y_frequency persistance adjustment].freeze - ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path roads_to_make].freeze + ALL_PERLIN_CONFIGS = %i[perlin_height_config perlin_moist_config perlin_temp_config].freeze + ROAD_CONFIG_OPTIONS = %i[road_seed roads road_exclude_water_path road_exclude_mountain_path road_exclude_flora_path roads_to_make].freeze PerlinConfig = Struct.new(*PERLIN_CONFIG_OPTIONS) + AllPerlinConfigs = Struct.new(*ALL_PERLIN_CONFIGS) RoadConfig = Struct.new(*ROAD_CONFIG_OPTIONS) attr_reader :generate_flora, :perlin_height_config, :perlin_moist_config, :perlin_temp_config, :width, :height, :road_config - 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, + def initialize(all_perlin_configs: default_perlin_configs, width: DEFAULT_TILE_COUNT, height: DEFAULT_TILE_COUNT, generate_flora: DEFAULT_GENERATE_FLORA, road_config: default_road_config) - raise ArgumentError unless perlin_height_config.is_a?(PerlinConfig) && perlin_moist_config.is_a?(PerlinConfig) - + validate(all_perlin_configs) @generate_flora = generate_flora - @perlin_height_config = perlin_height_config - @perlin_moist_config = perlin_moist_config - @perlin_temp_config = perlin_temp_config + @perlin_height_config = all_perlin_configs.perlin_height_config + @perlin_moist_config = all_perlin_configs.perlin_moist_config + @perlin_temp_config = all_perlin_configs.perlin_temp_config @width = width @height = height @road_config = road_config @@ -55,6 +56,12 @@ def initialize(perlin_height_config: default_perlin_height_config, perlin_moist_ private + def validate(all_perlin_configs) + 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) + raise ArgumentError + end + end + def default_perlin_height_config PerlinConfig.new(DEFAULT_TILE_COUNT, DEFAULT_TILE_COUNT, DEFAULT_HEIGHT_SEED, DEFAULT_HEIGHT_OCTAVES, DEFAULT_HEIGHT_X_FREQUENCY, DEFAULT_HEIGHT_Y_FREQUENCY, DEFAULT_HEIGHT_PERSISTANCE, DEFAULT_HEIGHT_ADJUSTMENT) @@ -73,4 +80,8 @@ def default_perlin_temp_config def default_road_config 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) end + + def default_perlin_configs + AllPerlinConfigs.new(default_perlin_height_config, default_perlin_moist_config, default_perlin_temp_config) + end end diff --git a/lib/pathfinding/a_star_finder.rb b/lib/pathfinding/a_star_finder.rb index 5689f9f..e97ce96 100644 --- a/lib/pathfinding/a_star_finder.rb +++ b/lib/pathfinding/a_star_finder.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true module Pathfinding + # + # An A* Pathfinder to build roads/paths between two coordinates containing + # different path costs, the heuristic behaviour that can be altered via configuration + # class AStarFinder def find_path(start_node, end_node, grid) open_set = [start_node] diff --git a/lib/pathfinding/grid.rb b/lib/pathfinding/grid.rb index 90da1f3..e07cbf5 100644 --- a/lib/pathfinding/grid.rb +++ b/lib/pathfinding/grid.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true module Pathfinding + # + # Responsible for manipulating and encapsulating behaviour of tiles related + # to pathfinding + # class Grid attr_reader :nodes @@ -8,9 +12,11 @@ def initialize(nodes) @nodes = nodes end + # rubocop:disable Naming/MethodParameterName: def node(x, y) nodes[y][x] end + # rubocop:enable Naming/MethodParameterName: def neighbors(node) neighbors = [] diff --git a/lib/road_generator.rb b/lib/road_generator.rb index 9a45a96..df9ff69 100644 --- a/lib/road_generator.rb +++ b/lib/road_generator.rb @@ -3,6 +3,9 @@ require 'pathfinding/grid' require 'pathfinding/a_star_finder' +# +# Generates roads across map tiles, randomly or given specific coordinates +# class RoadGenerator attr_reader :grid, :finder diff --git a/test/map_config_test.rb b/test/map_config_test.rb index 92c4dcb..968c7ed 100644 --- a/test/map_config_test.rb +++ b/test/map_config_test.rb @@ -43,9 +43,7 @@ def setup @generate_flora = false @map_config = MapConfig.new( - perlin_height_config: @perlin_height_config, - perlin_moist_config: @perlin_moist_config, - perlin_temp_config: @perlin_temp_config, + all_perlin_configs: MapConfig::AllPerlinConfigs.new(@perlin_height_config, @perlin_moist_config, @perlin_temp_config), width: @width, height: @height, generate_flora: @generate_flora @@ -65,9 +63,7 @@ def test_initialize_with_invalid_perlin_height_config invalid_perlin_height_config = :invalid assert_raises(ArgumentError) do MapConfig.new( - perlin_height_config: invalid_perlin_height_config, - perlin_moist_config: @perlin_moist_config, - perlin_temp_config: @perlin_temp_config + all_perlin_configs: MapConfig::AllPerlinConfigs.new(invalid_perlin_height_config, @perlin_moist_config, @perlin_temp_config) ) end end @@ -76,9 +72,7 @@ def test_initialize_with_invalid_perlin_moist_config invalid_perlin_moist_config = :invalid assert_raises(ArgumentError) do MapConfig.new( - perlin_height_config: @perlin_height_config, - perlin_moist_config: invalid_perlin_moist_config, - perlin_temp_config: @perlin_temp_config + all_perlin_configs: MapConfig::AllPerlinConfigs.new(@perlin_height_config, invalid_perlin_moist_config, @perlin_temp_config) ) end end diff --git a/test/pathfinding/a_star_finder_test.rb b/test/pathfinding/a_star_finder_test.rb index f39cfe1..aa541dd 100644 --- a/test/pathfinding/a_star_finder_test.rb +++ b/test/pathfinding/a_star_finder_test.rb @@ -8,11 +8,13 @@ class TestAStarFinder < Minitest::Test class Node attr_reader :x, :y - def initialize(x, y, can_contain_road = true) + # rubocop:disable Naming/MethodParameterName: + def initialize(x, y, can_contain_road: true) @x = x @y = y @can_contain_road = can_contain_road end + # rubocop:enable Naming/MethodParameterName: def path_heuristic 0 @@ -100,8 +102,8 @@ def test_finds_path_with_obstacles def test_finds_path_with_non_walkable_paths nodes = [ - [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0, false), Node.new(3, 0)], - [Node.new(0, 1), Node.new(1, 1, false), Node.new(2, 1, false), Node.new(3, 1)], + [Node.new(0, 0), Node.new(1, 0), Node.new(2, 0, can_contain_road: false), Node.new(3, 0)], + [Node.new(0, 1), Node.new(1, 1, can_contain_road: false), Node.new(2, 1, can_contain_road: false), Node.new(3, 1)], [Node.new(0, 2), Node.new(1, 2), Node.new(2, 2), Node.new(3, 2)], [Node.new(0, 3), Node.new(1, 3), Node.new(2, 3), Node.new(3, 3)] ] diff --git a/test/pathfinding/grid_test.rb b/test/pathfinding/grid_test.rb index fe308df..635790d 100644 --- a/test/pathfinding/grid_test.rb +++ b/test/pathfinding/grid_test.rb @@ -18,11 +18,13 @@ class Node # Replace this with your Node class implementation or use a mock/fake Node class. attr_reader :x, :y + # rubocop:disable Naming/MethodParameterName: def initialize(x, y, road) @x = x @y = y @road = road end + # rubocop:enable Naming/MethodParameterName: def can_contain_road? @road