From 1f932bc793b3f6212cf2046bc4b164842a8ae3df Mon Sep 17 00:00:00 2001 From: matthewstyer Date: Sun, 2 Jul 2023 19:53:11 -0400 Subject: [PATCH] init --- .gitignore | 13 ++ .rubocop.yml | 1 + .rubocop_todo.yml | 90 +++++++++ .ruby-version | 1 + Gemfile | 5 + Gemfile.lock | 39 ++++ Rakefile | 10 + bin/ruby-perlin-2D-map-generator | 13 ++ lib/CLI/command.rb | 277 +++++++++++++++++++++++++++ lib/ansi_colours.rb | 31 +++ lib/biome.rb | 225 ++++++++++++++++++++++ lib/flora.rb | 13 ++ lib/map.rb | 27 +++ lib/map_config.rb | 61 ++++++ lib/map_tile_generator.rb | 69 +++++++ lib/tile.rb | 81 ++++++++ lib/tile_item.rb | 22 +++ lib/tile_perlin_generator.rb | 46 +++++ ruby-perlin-2D-map-generator.gemspec | 24 +++ test/CLI/command_test.rb | 72 +++++++ test/biome_test.rb | 189 ++++++++++++++++++ test/flora_test.rb | 24 +++ test/map_config_test.rb | 89 +++++++++ test/map_test.rb | 70 +++++++ test/map_tile_generator_test.rb | 99 ++++++++++ test/tile_item_test.rb | 31 +++ test/tile_perlin_generator_test.rb | 122 ++++++++++++ test/tile_test.rb | 143 ++++++++++++++ 28 files changed, 1887 insertions(+) create mode 100644 .gitignore create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100755 bin/ruby-perlin-2D-map-generator create mode 100644 lib/CLI/command.rb create mode 100644 lib/ansi_colours.rb create mode 100644 lib/biome.rb create mode 100644 lib/flora.rb create mode 100644 lib/map.rb create mode 100644 lib/map_config.rb create mode 100644 lib/map_tile_generator.rb create mode 100644 lib/tile.rb create mode 100644 lib/tile_item.rb create mode 100644 lib/tile_perlin_generator.rb create mode 100644 ruby-perlin-2D-map-generator.gemspec create mode 100644 test/CLI/command_test.rb create mode 100644 test/biome_test.rb create mode 100644 test/flora_test.rb create mode 100644 test/map_config_test.rb create mode 100644 test/map_test.rb create mode 100644 test/map_tile_generator_test.rb create mode 100644 test/tile_item_test.rb create mode 100644 test/tile_perlin_generator_test.rb create mode 100644 test/tile_test.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fd4d35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..cc32da4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..101f053 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,90 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2023-07-06 02:10:34 UTC using RuboCop version 1.54.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Configuration parameters: Severity, Include. +# Include: **/*.gemspec +Gemspec/RequiredRubyVersion: + Exclude: + - 'ruby-perlin-2D-map-generator.gemspec' + +# Offense count: 2 +Lint/FloatComparison: + Exclude: + - 'lib/tile_perlin_generator.rb' + - 'test/tile_perlin_generator_test.rb' + +# Offense count: 10 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 44 + +# Offense count: 4 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 209 + +# Offense count: 2 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 26 + +# Offense count: 16 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 74 + +# Offense count: 2 +# Configuration parameters: CountKeywordArgs, MaxOptionalParameters. +Metrics/ParameterLists: + Max: 6 + +# Offense count: 2 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 28 + +# Offense count: 6 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: + Exclude: + - 'lib/tile.rb' + - 'lib/tile_perlin_generator.rb' + - 'test/tile_perlin_generator_test.rb' + +# Offense count: 2 +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: + Exclude: + - 'test/tile_perlin_generator_test.rb' + +# Offense count: 9 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'lib/CLI/command.rb' + - 'lib/biome.rb' + - 'lib/flora.rb' + - 'lib/map.rb' + - 'lib/map_config.rb' + - 'lib/map_tile_generator.rb' + - 'lib/tile.rb' + - 'lib/tile_item.rb' + - 'lib/tile_perlin_generator.rb' + +# Offense count: 17 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 193 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..4a36342 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.0 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7f4f5e9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..91bd249 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,39 @@ +PATH + remote: . + specs: + ruby-perlin-2D-map-generator (0.0.1) + perlin + tty-option + +GEM + remote: https://rubygems.org/ + specs: + ast (1.1.0) + minitest (5.18.1) + mocha (2.0.4) + ruby2_keywords (>= 0.0.5) + parser (2.0.0.pre1) + ast (~> 1.1) + slop (~> 3.4, >= 3.4.5) + perlin (0.2.2) + rainbow (3.1.1) + rake (13.0.6) + rubocop (0.9.1) + parser (= 2.0.0.pre1) + rainbow (>= 1.1.4) + ruby2_keywords (0.0.5) + slop (3.6.0) + tty-option (0.3.0) + +PLATFORMS + x86_64-darwin-21 + +DEPENDENCIES + minitest + mocha + rake + rubocop + ruby-perlin-2D-map-generator! + +BUNDLED WITH + 2.2.3 diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e133fa4 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'minitest/test_task' + +Minitest::TestTask.create(:test) do |t| + t.libs << 'test' + t.libs << 'lib' + t.warning = false + t.test_globs = ['test/**/*_test.rb'] +end diff --git a/bin/ruby-perlin-2D-map-generator b/bin/ruby-perlin-2D-map-generator new file mode 100755 index 0000000..d05bf20 --- /dev/null +++ b/bin/ruby-perlin-2D-map-generator @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +lib_path = File.expand_path('../lib', __dir__) +$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path) +require 'CLI/command' + +Signal.trap('INT') do + warn("\n#{caller.join("\n")}: interrupted") + exit(1) +end + +CLI::Command.new.parse.run diff --git a/lib/CLI/command.rb b/lib/CLI/command.rb new file mode 100644 index 0000000..5311a39 --- /dev/null +++ b/lib/CLI/command.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +require 'tty/option' +require 'map' +require 'map_config' +module CLI + class Command + include TTY::Option + + usage do + program 'ruby-perlin-2D-map-generator' + + no_command + + desc 'Generate a replayable seeded procedurally generated 2 dimensional map. Rendered in the console ' \ + ' using ASCII colours, or described as a hash containting each tiles information.' + + example 'Render with defaults', + ' $ ruby-perlin-2D-map-generator render' + + example 'Render with options', + ' $ ruby-perlin-2D-map-generator render --i=false --t=10 --ms=10' + end + + argument :command do + name '(describe | render)' + arity one + validate ->(v) { v.downcase == 'describe' || v.downcase == 'render' } + desc 'command to run: render prints the map to standard output using ansi colors, ' \ + 'while describe prints each tiles bionome information in the map.' + end + + option :height_seed do + long '--hs int' + # or + long '--hs=int' + + desc 'The seed for a terrains height perlin generation' + convert Integer + default MapConfig::DEFAULT_HEIGHT_SEED + end + + option :moist_seed do + long '--ms int' + # or + long '--ms=int' + + desc 'The seed for a terrains moist perlin generation' + convert Integer + default MapConfig::DEFAULT_MOIST_SEED + end + + option :temp_seed do + long '--ts int' + # or + long '--ts=int' + + desc 'The seed for a terrains temperature perlin generation' + convert Integer + default MapConfig::DEFAULT_TEMP_SEED + end + + option :octaves_height do + long '--oh int' + long '--oh=int' + + desc 'Octaves for height generation' + convert Integer + default MapConfig::DEFAULT_HEIGHT_OCTAVES + end + + option :octaves_moist do + long '--om int' + long '--om=int' + + desc 'Octaves for moist generation' + convert Integer + default MapConfig::DEFAULT_MOIST_OCTAVES + end + + option :octaves_temp do + long '--ot int' + long '--ot=int' + + desc 'Octaves for temp generation' + convert Integer + default MapConfig::DEFAULT_TEMP_OCTAVES + end + + option :persistance_moist do + long '--pm float' + long '--pm=float' + + desc 'Persistance for moist generation' + convert Float + default MapConfig::DEFAULT_MOIST_PERSISTANCE + end + + option :persistance_height do + long '--ph float' + long '--ph=float' + + desc 'Persistance for height generation' + convert Float + default MapConfig::DEFAULT_HEIGHT_PERSISTANCE + end + + option :persistance_temp do + long '--pt float' + long '--pt=float' + + desc 'Persistance for temp generation' + convert Float + default MapConfig::DEFAULT_TEMP_PERSISTANCE + end + + option :width do + long '--width int' + long '--width=int' + + desc 'The width of the generated map' + convert Integer + default MapConfig::DEFAULT_TILE_COUNT + end + + option :height do + long '--height int' + long '--height=int' + + desc 'The height of the generated map' + convert Integer + default MapConfig::DEFAULT_TILE_COUNT + end + + option :perlin_height_horizontal_frequency do + long '--fhx float' + long '--fhx=float' + + desc 'The frequency for height generation across the x-axis' + convert Float + default MapConfig::DEFAULT_HEIGHT_X_FREQUENCY + end + + option :perlin_height_vertical_frequency do + long '--fhy float' + long '--fhy=float' + + desc 'The frequency for height generation across the y-axis' + convert Float + default MapConfig::DEFAULT_HEIGHT_Y_FREQUENCY + end + + option :perlin_temp_horizontal_frequency do + long '--ftx float' + long '--ftx=float' + + desc 'The frequency for temp generation across the x-axis' + convert Float + default MapConfig::DEFAULT_TEMP_X_FREQUENCY + end + + option :perlin_temp_vertical_frequency do + long '--fty float' + long '--fty=float' + + desc 'The frequency for temp generation across the y-axis' + convert Float + default MapConfig::DEFAULT_TEMP_Y_FREQUENCY + end + + option :perlin_moist_horizontal_frequency do + long '--fmx float' + long '--fmx=float' + + desc 'The frequency for moist generation across the x-axis' + convert Float + default MapConfig::DEFAULT_MOIST_X_FREQUENCY + end + + option :perlin_moist_vertical_frequency do + long '--fmy float' + long '--fmy=float' + + desc 'The frequency for moist generation across the y-axis' + convert Float + default MapConfig::DEFAULT_MOIST_Y_FREQUENCY + end + + option :generate_flora do + long '--gf bool' + long '--gf=bool' + + desc 'Generate flora, significantly affects performance' + convert :bool + default MapConfig::DEFAULT_GENERATE_FLORA + end + + option :temp do + long '--temp float' + long '--temp=float' + + desc 'Adjust each generated temperature by this percent (0 - 100)' + convert ->(val) { val.to_f / 100.0 } + validate ->(val) { val >= -1.0 && val <= 1.0 } + default MapConfig::DEFAULT_TEMP_ADJUSTMENT + end + + option :elevation do + long '--elevation float' + long '--elevation=float' + + desc 'Adjust each generated elevation by this percent (0 - 100)' + convert ->(val) { val.to_f / 100.0 } + validate ->(val) { val >= -1.0 && val <= 1.0 } + default MapConfig::DEFAULT_HEIGHT_ADJUSTMENT + end + + option :moisture do + long '--moisture float' + long '--moisture=float' + + desc 'Adjust each generated moisture by this percent (0 - 100)' + convert ->(val) { val.to_f / 100.0 } + validate ->(val) { val >= -1.0 && val <= 1.0 } + default MapConfig::DEFAULT_MOIST_ADJUSTMENT + end + + flag :help do + short '-h' + long '--help' + desc 'Print usage' + end + + def run + if params[:help] + print help + elsif params.errors.any? + puts params.errors.summary + else + execute_command + # pp params.to_h + end + end + + private + + 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, + generate_flora: params[:generate_flora] + )) + case params[:command] + when 'render' then map.render + when 'describe' then puts map.describe + end + end + + def perlin_moist_config + MapConfig::PerlinConfig.new(*params.to_h.slice(:width, :height, :moist_seed, :octaves_moist, + :perlin_moist_horizontal_frequency, :perlin_moist_vertical_frequency, :persistance_moist, :moisture).values) + end + + def perlin_height_config + MapConfig::PerlinConfig.new(*params.to_h.slice(:width, :height, :height_seed, :octaves_height, + :perlin_height_horizontal_frequency, :perlin_height_vertical_frequency, :persistance_height, :elevation).values) + end + + def perlin_temp_config + MapConfig::PerlinConfig.new(*params.to_h.slice(:width, :height, :temp_seed, :octaves_temp, + :perlin_temp_horizontal_frequency, :perlin_temp_vertical_frequency, :persistance_temp, :temp).values) + end + end +end diff --git a/lib/ansi_colours.rb b/lib/ansi_colours.rb new file mode 100644 index 0000000..4a0eaba --- /dev/null +++ b/lib/ansi_colours.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module AnsiColours + module Background + WHITE = "\e[48;5;231m" + BLACK = "\033[40m" + DEEP_BLUE = "\033[48;5;23m" + BLUE = "\033[44m" + LIGHT_BLUE = "\033[48;5;117m" + SHOAL_BLUE = "\e[48;5;87m" + GREEN = "\033[102m" + LIGHT_GREEN = "\e[48;5;47m" + DESERT_YELLOW = "\e[48;5;220m" + DESERT_LIGHT_YELLOW = "\e[48;5;223m" + DESERT_DARK_YELLOW = "\e[48;5;214m" + GREEN_SWAMP = "\e[48;5;58m" + DARK_GREEN = "\033[42m" + BROWN = "\033[48;5;130m" + GREY = "\e[48;5;7m" + GREY_MOUNTAIN = "\033[38;5;240m" + SANDY = "\033[48;5;229m" + RED_BROWN = "\033[48;5;173m" + GRASSLAND = "\033[48;2;195;205;103m" + TAIGA_PLAIN = "\e[48;5;67m" + TAIGA_VALLEY = "\e[48;5;66m" + TAIGA_HIGHLAND = "\e[48;5;65m" + TAIGA_COAST = "\e[48;5;17m" + ICE = "\e[48;5;159m" + ANSI_RESET = "\033[0m" + end +end diff --git a/lib/biome.rb b/lib/biome.rb new file mode 100644 index 0000000..cd84bf3 --- /dev/null +++ b/lib/biome.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +require 'ansi_colours' +require 'flora' + +class Biome + attr_accessor :name, :flora_range, :colour + + def initialize(name:, colour:, flora_range: nil) + @name = name + @flora_range = flora_range + @colour = colour + end + + def water? + WATER_TERRAIN.include?(self) + end + + def land? + LAND_TERRAIN.include?(self) + end + + def grassland? + GRASSLAND_TERRAIN.include?(self) + end + + def desert? + DESERT_TERRAIN.include?(self) + end + + def taiga? + TAIGA_TERRAIN.include?(self) + end + + def flora_available + !flora_range.nil? + end + + def flora + @flora ||= + if flora_available + if desert? + Flora.new(Flora::CACTUS) + elsif grassland? + Flora.new(Flora::DECIDUOUS_TREE) + elsif taiga? + Flora.new(Flora::EVERGREEN_TREE) + else + raise StandardError, "Unknown flora #{to_h}" + end + end + end + + def to_h + { + name: name, + flora_range: flora_range, + colour: colour + } + end + + def self.from(elevation, moist, temp) + terrain_selection(elevation, moist, temp) + end + + SNOW = Biome.new(name: 'snow', flora_range: nil, colour: AnsiColours::Background::WHITE) + ROCKS = Biome.new(name: 'rocks', flora_range: nil, colour: AnsiColours::Background::GREY) + MOUNTAIN = Biome.new(name: 'mountain', flora_range: nil, colour: AnsiColours::Background::BROWN) + MOUNTAIN_FOOT = Biome.new(name: 'mountain_foot', flora_range: 10, colour: AnsiColours::Background::RED_BROWN) + GRASSLAND = Biome.new(name: 'grassland', flora_range: 1, colour: AnsiColours::Background::DARK_GREEN) + VALLEY = Biome.new(name: 'valley', flora_range: 1, colour: AnsiColours::Background::GREEN) + DEEP_VALLEY = Biome.new(name: 'deep_valley', flora_range: 1, colour: AnsiColours::Background::LIGHT_GREEN) + DESERT = Biome.new(name: 'desert', flora_range: 1, colour: AnsiColours::Background::DESERT_YELLOW) + DEEP_DESERT = Biome.new(name: 'deep_desert', flora_range: 1, colour: AnsiColours::Background::DESERT_LIGHT_YELLOW) + STEPPE_DESERT = Biome.new(name: 'steppe_desert', flora_range: 1, colour: AnsiColours::Background::DESERT_DARK_YELLOW) + SWAMP = Biome.new(name: 'swamp', flora_range: nil, colour: AnsiColours::Background::GREEN_SWAMP) + COASTLINE = Biome.new(name: 'coastline', flora_range: 5, colour: AnsiColours::Background::SANDY) + SHOAL = Biome.new(name: 'shoal', flora_range: nil, colour: AnsiColours::Background::SHOAL_BLUE) + OCEAN = Biome.new(name: 'ocean', flora_range: nil, colour: AnsiColours::Background::LIGHT_BLUE) + DEEP_OCEAN = Biome.new(name: 'deep_ocean', flora_range: nil, colour: AnsiColours::Background::BLUE) + TAIGA_PLAIN = Biome.new(name: 'taiga_plain', flora_range: 1.5, colour: AnsiColours::Background::TAIGA_PLAIN) + TAIGA_VALLEY = Biome.new(name: 'taiga_valley', flora_range: 1.5, colour: AnsiColours::Background::TAIGA_VALLEY) + TAIGA_HIGHLAND = Biome.new(name: 'taiga_highland', flora_range: 1.5, colour: AnsiColours::Background::TAIGA_HIGHLAND) + TAIGA_COAST = Biome.new(name: 'taiga_coast', flora_range: nil, colour: AnsiColours::Background::TAIGA_COAST) + ICE = Biome.new(name: 'ice', flora_range: nil, colour: AnsiColours::Background::ICE) + + ALL_TERRAIN = [ + SNOW, + ROCKS, + MOUNTAIN, + MOUNTAIN_FOOT, + GRASSLAND, + VALLEY, + COASTLINE, + SHOAL, + OCEAN, + DEEP_OCEAN, + DESERT, + DEEP_DESERT, + STEPPE_DESERT, + SWAMP, + TAIGA_PLAIN, + TAIGA_HIGHLAND, + TAIGA_VALLEY, + TAIGA_COAST, + ICE + ].freeze + + WATER_TERRAIN = [ + SHOAL, + OCEAN, + DEEP_OCEAN + ].freeze + + DESERT_TERRAIN = [ + DESERT, + DEEP_DESERT, + STEPPE_DESERT + ].freeze + + GRASSLAND_TERRAIN = [ + GRASSLAND, + VALLEY, + DEEP_VALLEY, + MOUNTAIN_FOOT + ].freeze + + TAIGA_TERRAIN = [ + TAIGA_PLAIN, + TAIGA_HIGHLAND, + TAIGA_VALLEY, + TAIGA_COAST + ].freeze + + LAND_TERRAIN = (ALL_TERRAIN - WATER_TERRAIN).freeze + + class << self + private + + def terrain_selection(elevation, moist, temp) + case elevation + when 0.95..1 + SNOW + when 0.9..0.95 + ROCKS + when 0.8..0.9 + if moist < 0.9 + MOUNTAIN + else + SHOAL + end + when 0.7..0.8 + if moist < 0.9 + MOUNTAIN_FOOT + else + SHOAL + end + when 0.6..0.7 + if moist < 0.8 + if desert_condition?(moist, temp) + STEPPE_DESERT + elsif taiga_condition?(moist, temp) + TAIGA_HIGHLAND + elsif moist > 0.75 + SWAMP + else + GRASSLAND + end + else + SHOAL + end + when 0.3..0.6 + if desert_condition?(moist, temp) + DESERT + elsif taiga_condition?(moist, temp) + TAIGA_PLAIN + else + VALLEY + end + when 0.2..0.3 + if desert_condition?(moist, temp) + DEEP_DESERT + elsif taiga_condition?(moist, temp) + TAIGA_VALLEY + else + DEEP_VALLEY + end + when 0.15..0.2 + if taiga_condition?(moist, temp) + TAIGA_COAST + else + COASTLINE + end + when 0.05..0.15 + if taiga_condition?(moist, temp) + ICE + else + SHOAL + end + when 0.025..0.05 + if taiga_condition?(moist, temp) + ICE + else + OCEAN + end + when 0.0..0.025 + if taiga_condition?(moist, temp) + ICE + else + DEEP_OCEAN + end + else + raise ArgumentError, "unknown biome elevation: #{elevation} moisture: #{moist} temp: #{temp}" + end + end + + def desert_condition?(moist, temp) + moist < 0.1 && temp > 0.5 + end + + def taiga_condition?(moist, temp) + moist > 0.6 && temp < 0.2 + end + end +end diff --git a/lib/flora.rb b/lib/flora.rb new file mode 100644 index 0000000..be114f2 --- /dev/null +++ b/lib/flora.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'tile_item' + +class Flora < TileItem + CACTUS = "\u{1F335}" + EVERGREEN_TREE = "\u{1F332}" + DECIDUOUS_TREE = "\u{1F333}" + + def initialize(render_symbol) + super self, render_symbol: render_symbol + end +end diff --git a/lib/map.rb b/lib/map.rb new file mode 100644 index 0000000..3c5a6c6 --- /dev/null +++ b/lib/map.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'map_tile_generator' +require 'map_config' + +class Map + attr_reader :config + + def initialize(map_config: MapConfig.new) + @config = map_config + end + + def describe + tiles.map { |row| row.map(&:to_h) } + end + + def render + tiles.each do |row| + row.each(&:render_to_standard_output) + puts + end + end + + def tiles + @tiles ||= MapTileGenerator.new(map: self).generate + end +end diff --git a/lib/map_config.rb b/lib/map_config.rb new file mode 100644 index 0000000..77742e6 --- /dev/null +++ b/lib/map_config.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class MapConfig + DEFAULT_TILE_COUNT = 128 + DEFAULT_GENERATE_FLORA = true + + DEFAULT_HEIGHT_SEED = 10 + DEFAULT_HEIGHT_OCTAVES = 3 + DEFAULT_HEIGHT_X_FREQUENCY = 2.5 + DEFAULT_HEIGHT_Y_FREQUENCY = 2.5 + DEFAULT_HEIGHT_PERSISTANCE = 1.0 + DEFAULT_HEIGHT_ADJUSTMENT = 0.0 + + DEFAULT_MOIST_SEED = 300 + DEFAULT_MOIST_OCTAVES = 3 + DEFAULT_MOIST_X_FREQUENCY = 2.5 + DEFAULT_MOIST_Y_FREQUENCY = 2.5 + DEFAULT_MOIST_PERSISTANCE = 1.0 + DEFAULT_MOIST_ADJUSTMENT = 0.0 + + DEFAULT_TEMP_SEED = 3000 + DEFAULT_TEMP_OCTAVES = 3 + DEFAULT_TEMP_PERSISTANCE = 1.0 + DEFAULT_TEMP_Y_FREQUENCY = 2.5 + DEFAULT_TEMP_X_FREQUENCY = 2.5 + DEFAULT_TEMP_ADJUSTMENT = 0.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 + + 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) + raise ArgumentError unless perlin_height_config.is_a?(PerlinConfig) && perlin_moist_config.is_a?(PerlinConfig) + + @generate_flora = generate_flora + @perlin_height_config = perlin_height_config + @perlin_moist_config = perlin_moist_config + @perlin_temp_config = perlin_temp_config + @width = width + @height = height + end + + private + + 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) + end + + def default_perlin_moist_config + PerlinConfig.new(DEFAULT_TILE_COUNT, DEFAULT_TILE_COUNT, DEFAULT_MOIST_SEED, DEFAULT_MOIST_OCTAVES, + DEFAULT_MOIST_X_FREQUENCY, DEFAULT_MOIST_Y_FREQUENCY, DEFAULT_MOIST_PERSISTANCE, DEFAULT_MOIST_ADJUSTMENT) + end + + 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 +end diff --git a/lib/map_tile_generator.rb b/lib/map_tile_generator.rb new file mode 100644 index 0000000..eaf9dbc --- /dev/null +++ b/lib/map_tile_generator.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'tile' +require 'tile_perlin_generator' + +class MapTileGenerator + attr_reader :map_config, :map, :height_perlin_generator, :moist_perlin_generator, :temp_perlin_generator + + def initialize(map:, height_perlin_generator: nil, moist_perlin_generator: nil, temp_perlin_generator: nil) + @map = map + @map_config = map.config + @height_perlin_generator = height_perlin_generator || default_perlin_height_generator + @moist_perlin_generator = moist_perlin_generator || default_perlin_moist_generator + @temp_perlin_generator = temp_perlin_generator || default_perlin_temp_generator + end + + def generate + positive_quadrant_cartesian_plane + end + + private + + def positive_quadrant_cartesian_plane + y_axis_array do |y| + x_axis_array do |x| + Tile.new( + map: map, + x: x, + y: y, + height: heights[y][x], + moist: moists[y][x], + temp: temps[y][x] + ) + end + end + end + + def y_axis_array(&block) + Array.new(map_config.height, &block) + end + + def x_axis_array(&block) + Array.new(map_config.width, &block) + end + + def default_perlin_height_generator + TilePerlinGenerator.new(map_config.perlin_height_config) + end + + def default_perlin_moist_generator + TilePerlinGenerator.new(map_config.perlin_moist_config) + end + + def default_perlin_temp_generator + TilePerlinGenerator.new(map_config.perlin_temp_config) + end + + def heights + @heights ||= height_perlin_generator.generate + end + + def moists + @moists ||= moist_perlin_generator.generate + end + + def temps + @temps ||= temp_perlin_generator.generate + end +end diff --git a/lib/tile.rb b/lib/tile.rb new file mode 100644 index 0000000..d3bc398 --- /dev/null +++ b/lib/tile.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'biome' +require 'flora' + +class Tile + attr_reader :x, :y, :height, :moist, :temp, :map + + def initialize(map:, x:, y:, height: 0, moist: 0, temp: 0) + @x = x + @y = y + @height = height + @moist = moist + @temp = temp + @map = map + end + + def surrounding_tiles(distance = 1) + @surround_cache ||= {} + @surround_cache[distance] ||= begin + left_limit = [0, x - distance].max + top_limit = [0, y - distance].max + + map.tiles[left_limit..(x + distance)].map do |r| + r[top_limit..(y + distance)] + end.flatten + end + end + + def biome + @biome ||= Biome.from(height, moist, temp) + end + + def items + @items ||= items_generated_with_flora_if_applicable + end + + def render_to_standard_output + print biome.colour + (!items.empty? ? item_with_highest_priority.render_symbol : ' ') + print AnsiColours::Background::ANSI_RESET + end + + def add_item(tile_item) + raise ArgumentError, 'item should be a tile' unless tile_item.is_a?(TileItem) + + items.push(tile_item) + end + + def item_with_highest_priority + items.max_by(&:render_priority) + end + + def to_h + { + x: x, + y: y, + height: height, + moist: moist, + temp: temp, + biome: biome.to_h, + items: items.map(&:to_h) + } + end + + private + + 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| + r[(x - biome.flora_range)...(x + biome.flora_range)] + end&.flatten&.map(&:height)&.max + if range_max_value == height + [biome.flora] + else + [] + end + else + [] + end + end +end diff --git a/lib/tile_item.rb b/lib/tile_item.rb new file mode 100644 index 0000000..5a1b70a --- /dev/null +++ b/lib/tile_item.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class TileItem + DEFAULT_RENDER_PRIORITY = 0 + attr_reader :obj, :id, :render_symbol, :colour, :render_priority + + def initialize(obj, render_symbol:, id: object_id, render_priority: DEFAULT_RENDER_PRIORITY) + @obj = obj + @id = id + @render_symbol = render_symbol + @render_priority = render_priority + end + + def to_h + { + id: id, + type: obj.class.name.downcase, + render_symbol: render_symbol, + render_priority: render_priority + } + end +end diff --git a/lib/tile_perlin_generator.rb b/lib/tile_perlin_generator.rb new file mode 100644 index 0000000..e0bfbb3 --- /dev/null +++ b/lib/tile_perlin_generator.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'perlin' + +class TilePerlinGenerator + attr_reader :perlin_config + + def initialize(perlin_config) + @perlin_config = perlin_config + end + + def generate + Array.new(perlin_config.height) do |y| + Array.new(perlin_config.width) do |x| + nx = perlin_config.x_frequency * (x.to_f / perlin_config.width - 0.5) + ny = perlin_config.y_frequency * (y.to_f / perlin_config.height - 0.5) + with_adjustment((Math.cos(noise(nx, ny))**6)) + end + end + end + + private + + def with_adjustment(result) + if perlin_config.adjustment != 0.0 + result += perlin_config.adjustment + result = [result, 1.0].min + [result, 0.0].max + else + result + end + end + + def noise(x, y) + (noise_generator[x, y] / 2) + 0.5 + end + + def noise_generator + @noise_generator ||= + Perlin::Generator.new( + perlin_config.noise_seed, + perlin_config.persistance, + perlin_config.octaves + ) + end +end diff --git a/ruby-perlin-2D-map-generator.gemspec b/ruby-perlin-2D-map-generator.gemspec new file mode 100644 index 0000000..953ae50 --- /dev/null +++ b/ruby-perlin-2D-map-generator.gemspec @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = 'ruby-perlin-2D-map-generator' + s.version = '0.0.1' + s.summary = 'Generate 2D maps, rendered with ansi colours or described in a hash' + s.description = 'A gem that generates a 2D map using perlin noise. Map can be rendered in console ' \ + 'using ansi colors or returned as a hash 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' + s.files = `git ls-files bin lib *.md LICENSE`.split("\n") + s.license = 'MIT' + s.executables = ['ruby-perlin-2D-map-generator'] + s.require_paths = ['lib'] + + s.add_dependency 'perlin' + s.add_dependency 'tty-option' + + s.add_development_dependency 'minitest' + s.add_development_dependency 'mocha' + s.add_development_dependency 'rake' + s.add_development_dependency 'rubocop' +end diff --git a/test/CLI/command_test.rb b/test/CLI/command_test.rb new file mode 100644 index 0000000..c047f60 --- /dev/null +++ b/test/CLI/command_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'mocha/minitest' +require 'stringio' +require 'CLI/command' +require 'ostruct' + +class CommandTest < Minitest::Test + def test_run_help_flag + command = CLI::Command.new + + output = capture_output { command.parse(['--help']).run } + assert_includes output, 'Usage: ' + end + + def test_run_command_with_errors + command = CLI::Command.new + + output = capture_output { command.parse(['--unknown']).run } + expected = "Errors:\n" \ + " 1) Invalid option '--unknown'\n" \ + " 2) Argument '(describe | render)' must be provided\n" + + assert_equal expected, output + end + + def test_run_render_command + tiles = [[mock('Tile1'), mock('Tile2')], [mock('Tile3'), mock('Tile4')]] + map = Map.new + map.expects(:tiles).returns(tiles) + + Map.expects(:new).with(anything).returns(map) + + output = capture_output do + tiles.each_with_index do |row, index| + row.each_with_index do |tile, tindex| + tile.expects(:render_to_standard_output).returns(print("#{index}#{tindex}")) + end + end + CLI::Command.new.parse(['render']).run + end + + assert_equal "00011011\n\n", output + end + + def test_run_describe_command + tiles = [[mock('Tile1'), mock('Tile2')]] + map = Map.new + map.expects(:tiles).returns(tiles) + + Map.expects(:new).with(anything).returns(map) + + tiles.each do |row| + row.each { |tile| tile.expects(:to_h).returns('tile_data') } + end + output = capture_output { CLI::Command.new.parse(['describe']).run } + + assert_equal "tile_data\ntile_data\n", output + end + + private + + def capture_output + output = StringIO.new + $stdout = output + yield + output.string + ensure + $stdout = STDOUT + end +end diff --git a/test/biome_test.rb b/test/biome_test.rb new file mode 100644 index 0000000..e03b169 --- /dev/null +++ b/test/biome_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'biome' + +class BiomeTest < Minitest::Test + def test_initialize + name = 'grassland' + colour = 'green' + flora_range = 1 + biome = Biome.new(name: name, colour: colour, flora_range: flora_range) + assert_equal name, biome.name + assert_equal colour, biome.colour + assert_equal flora_range, biome.flora_range + end + + def test_water? + water_biome = Biome::OCEAN + non_water_biome = Biome::GRASSLAND + assert water_biome.water? + refute non_water_biome.water? + end + + def test_land? + land_biome = Biome::GRASSLAND + water_biome = Biome::OCEAN + assert land_biome.land? + refute water_biome.land? + end + + def test_grassland? + grassland_biome = Biome::GRASSLAND + non_grassland_biome = Biome::OCEAN + assert grassland_biome.grassland? + refute non_grassland_biome.grassland? + end + + def test_desert? + desert_biome = Biome::DESERT + non_desert_biome = Biome::GRASSLAND + assert desert_biome.desert? + refute non_desert_biome.desert? + end + + def test_taiga? + taiga_biome = Biome::TAIGA_PLAIN + non_taiga_biome = Biome::GRASSLAND + assert taiga_biome.taiga? + refute non_taiga_biome.taiga? + end + + def test_flora_available + biome_with_flora = Biome.new(name: 'grassland', colour: 'green', flora_range: 1) + biome_without_flora = Biome.new(name: 'ocean', colour: 'blue') + assert biome_with_flora.flora_available + refute biome_without_flora.flora_available + end + + def test_flora + assert_instance_of Flora, Biome::GRASSLAND.flora + assert_equal Flora::DECIDUOUS_TREE, Biome::GRASSLAND.flora.render_symbol + + desert_biome = Biome::DESERT + taiga_biome = Biome::TAIGA_PLAIN + + assert_instance_of Flora, desert_biome.flora + assert_equal Flora::CACTUS, desert_biome.flora.render_symbol + + assert_instance_of Flora, taiga_biome.flora + assert_equal Flora::EVERGREEN_TREE, taiga_biome.flora.render_symbol + end + + def test_to_h + name = 'grassland' + colour = 'green' + flora_range = 1 + biome = Biome.new(name: name, colour: colour, flora_range: flora_range) + expected_hash = { + name: name, + flora_range: flora_range, + colour: colour + } + assert_equal expected_hash, biome.to_h + end + + def test_from + elevation = 0.7 + moist = 0.8 + temp = 0.3 + expected_biome = Biome::MOUNTAIN_FOOT + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_snow + elevation = 1.0 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::SNOW + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_rocks + elevation = 0.925 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::ROCKS + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_mountain + elevation = 0.825 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::MOUNTAIN + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_mountain_foot + elevation = 0.775 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::MOUNTAIN_FOOT + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_grassland + elevation = 0.625 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::GRASSLAND + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_valley + elevation = 0.475 + moist = 0.5 + temp = 0.5 + expected_biome = Biome::VALLEY + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_desert + elevation = 0.375 + moist = 0.05 + temp = 0.51 + expected_biome = Biome::DESERT + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_deep_desert + elevation = 0.275 + moist = 0.05 + temp = 0.51 + expected_biome = Biome::DEEP_DESERT + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_steppe_desert + elevation = 0.61 + moist = 0.05 + temp = 0.51 + expected_biome = Biome::STEPPE_DESERT + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_swamp + elevation = 0.625 + moist = 0.76 + temp = 0.5 + expected_biome = Biome::SWAMP + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_taiga_plain + elevation = 0.375 + moist = 0.8 + temp = 0.05 + expected_biome = Biome::TAIGA_PLAIN + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end + + def test_from_taiga_valley + elevation = 0.275 + moist = 0.8 + temp = 0.05 + expected_biome = Biome::TAIGA_VALLEY + assert_equal expected_biome, Biome.from(elevation, moist, temp) + end +end diff --git a/test/flora_test.rb b/test/flora_test.rb new file mode 100644 index 0000000..942c6d5 --- /dev/null +++ b/test/flora_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'flora' + +class FloraTest < Minitest::Test + def test_initialize + render_symbol = Flora::CACTUS + flora = Flora.new(render_symbol) + assert_equal render_symbol, flora.render_symbol + assert_equal 0, flora.render_priority + end + + def test_to_h + render_symbol = Flora::EVERGREEN_TREE + flora = Flora.new(render_symbol) + expected_hash = { + id: flora.id, + type: flora.obj.class.name.downcase, + render_symbol: render_symbol, + render_priority: flora.render_priority + } + assert_equal expected_hash, flora.to_h + end +end diff --git a/test/map_config_test.rb b/test/map_config_test.rb new file mode 100644 index 0000000..64efc74 --- /dev/null +++ b/test/map_config_test.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'map_config' + +class MapConfigTest < Minitest::Test + def setup + @perlin_height_config = MapConfig::PerlinConfig.new( + width: 100, + height: 100, + noise_seed: 123, + octaves: 4, + x_frequency: 2.0, + y_frequency: 2.0, + persistance: 0.5, + adjustment: 0.1 + ) + + @perlin_moist_config = MapConfig::PerlinConfig.new( + width: 100, + height: 100, + noise_seed: 456, + octaves: 3, + x_frequency: 1.5, + y_frequency: 1.5, + persistance: 0.8, + adjustment: -0.2 + ) + + @perlin_temp_config = MapConfig::PerlinConfig.new( + width: 100, + height: 100, + noise_seed: 789, + octaves: 5, + x_frequency: 2.5, + y_frequency: 2.5, + persistance: 0.7, + adjustment: 0.3 + ) + + @width = 200 + @height = 200 + @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, + width: @width, + height: @height, + generate_flora: @generate_flora + ) + end + + def test_initialize_with_valid_parameters + assert_equal @perlin_height_config, @map_config.perlin_height_config + assert_equal @perlin_moist_config, @map_config.perlin_moist_config + assert_equal @perlin_temp_config, @map_config.perlin_temp_config + assert_equal @width, @map_config.width + assert_equal @height, @map_config.height + assert_equal @generate_flora, @map_config.generate_flora + end + + 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 + ) + end + end + + 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 + ) + end + end + + def test_generate_flora + assert_equal @generate_flora, @map_config.generate_flora + end +end diff --git a/test/map_test.rb b/test/map_test.rb new file mode 100644 index 0000000..136961b --- /dev/null +++ b/test/map_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'mocha/minitest' +require 'map' + +class MapTest < Minitest::Test + def test_initialize_with_default_config + generator_mock = mock('MapTileGenerator') + MapTileGenerator.expects(:new).with(anything).at_least_once.returns(generator_mock) + generator_mock.expects(:generate).returns([['test']]) + + map = Map.new + + assert_equal [['test']], map.tiles + assert map.config + end + + def test_initialize_with_custom_config + map_config = mock('MapConfig') + generator_mock = mock('MapTileGenerator') + MapTileGenerator.expects(:new).with(anything).returns(generator_mock) + generator_mock.expects(:generate).returns([['Test2']]) + + map = Map.new(map_config: map_config) + + assert_equal map_config, map.config + assert_equal [['Test2']], map.tiles + end + + def test_describe + tiles = [[mock('Tile1'), mock('Tile2')]] + map = Map.new + map.expects(:tiles).returns(tiles) + + tiles.each do |row| + row.each { |tile| tile.expects(:to_h).returns('tile_data') } + end + + assert_equal '[["tile_data", "tile_data"]]', map.describe.to_s + end + + def test_render + tiles = [[mock('Tile1'), mock('Tile2')], [mock('Tile3'), mock('Tile4')]] + map = Map.new + map.expects(:tiles).returns(tiles) + + output = capture_output do + tiles.each_with_index do |row, index| + row.each_with_index do |tile, tindex| + tile.expects(:render_to_standard_output).returns(print("#{index}#{tindex}")) + end + end + map.render + end + + assert_equal "00011011\n\n", output + end + + private + + def capture_output + output = StringIO.new + $stdout = output + yield + output.string + ensure + $stdout = STDOUT + end +end diff --git a/test/map_tile_generator_test.rb b/test/map_tile_generator_test.rb new file mode 100644 index 0000000..fce118c --- /dev/null +++ b/test/map_tile_generator_test.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'mocha/minitest' +require 'map_tile_generator' +require 'ostruct' + +class MapTileGeneratorTest < Minitest::Test + def setup + @map = mock('Map') + @map_config = OpenStruct.new(width: 2, height: 2) + @map.stubs(:config).returns(@map_config) + + @height_perlin_generator = mock('TilePerlinGenerator') + @moist_perlin_generator = mock('TilePerlinGenerator') + @temp_perlin_generator = mock('TilePerlinGenerator') + + @generator = MapTileGenerator.new( + map: @map, + height_perlin_generator: @height_perlin_generator, + moist_perlin_generator: @moist_perlin_generator, + temp_perlin_generator: @temp_perlin_generator + ) + end + + def test_initialize_with_custom_perlin_generators + assert_equal @map, @generator.map + assert_equal @map_config, @generator.map_config + assert_equal @height_perlin_generator, @generator.height_perlin_generator + assert_equal @moist_perlin_generator, @generator.moist_perlin_generator + assert_equal @temp_perlin_generator, @generator.temp_perlin_generator + end + + def test_initialize_with_default_perlin_generators + @map_config.stubs(:perlin_height_config).returns({}) + @map_config.stubs(:perlin_moist_config).returns({}) + @map_config.stubs(:perlin_temp_config).returns({}) + @generator.stubs(:map_config).returns(@map_config) + TilePerlinGenerator.expects(:new).times(3) + + generator = MapTileGenerator.new(map: @map) + + assert_equal @map, generator.map + assert_equal @map_config, generator.map_config + end + + def test_generate + height_map = [[0.1, 0.2], [0.3, 0.4]] + moist_map = [[0.5, 0.6], [0.7, 0.8]] + temp_map = [[0.9, 1.0], [1.1, 1.2]] + expected_tiles = [ + [Tile.new(map: @map, x: 0, y: 0, height: 0.1, moist: 0.5, temp: 0.9), + Tile.new(map: @map, x: 1, y: 0, height: 0.2, moist: 0.6, temp: 1.0)], + [Tile.new(map: @map, x: 0, y: 1, height: 0.3, moist: 0.7, temp: 1.1), + Tile.new(map: @map, x: 1, y: 1, height: 0.4, moist: 0.8, temp: 1.2)] + ] + @height_perlin_generator.stubs(:generate).returns(height_map) + @moist_perlin_generator.stubs(:generate).returns(moist_map) + @temp_perlin_generator.stubs(:generate).returns(temp_map) + + tiles = @generator.generate + + expected_tiles.each_with_index do |expected_row, expected_row_index| + expected_row.each_with_index do |expected_tile, expected_tile_index| + assert_equal expected_tile.height, tiles[expected_row_index][expected_tile_index].height + assert_equal expected_tile.moist, tiles[expected_row_index][expected_tile_index].moist + assert_equal expected_tile.temp, tiles[expected_row_index][expected_tile_index].temp + assert_equal expected_tile.x, tiles[expected_row_index][expected_tile_index].x + assert_equal expected_tile.y, tiles[expected_row_index][expected_tile_index].y + assert_equal expected_tile.map, tiles[expected_row_index][expected_tile_index].map + end + end + end + + def test_positive_quadrant_cartesian_plane + @generator.stubs(:heights).returns([[0.1, 0.2], [0.3, 0.4]]) + @generator.stubs(:moists).returns([[0.5, 0.6], [0.7, 0.8]]) + @generator.stubs(:temps).returns([[0.9, 1.0], [1.1, 1.2]]) + expected_tiles = [ + [Tile.new(map: @map, x: 0, y: 0, height: 0.1, moist: 0.5, temp: 0.9), + Tile.new(map: @map, x: 1, y: 0, height: 0.2, moist: 0.6, temp: 1.0)], + [Tile.new(map: @map, x: 0, y: 1, height: 0.3, moist: 0.7, temp: 1.1), + Tile.new(map: @map, x: 1, y: 1, height: 0.4, moist: 0.8, temp: 1.2)] + ] + + tiles = @generator.send(:positive_quadrant_cartesian_plane) + + expected_tiles.each_with_index do |expected_row, expected_row_index| + expected_row.each_with_index do |expected_tile, expected_tile_index| + assert_equal expected_tile.height, tiles[expected_row_index][expected_tile_index].height + assert_equal expected_tile.moist, tiles[expected_row_index][expected_tile_index].moist + assert_equal expected_tile.temp, tiles[expected_row_index][expected_tile_index].temp + assert_equal expected_tile.x, tiles[expected_row_index][expected_tile_index].x + assert_equal expected_tile.y, tiles[expected_row_index][expected_tile_index].y + assert_equal expected_tile.map, tiles[expected_row_index][expected_tile_index].map + end + end + end +end diff --git a/test/tile_item_test.rb b/test/tile_item_test.rb new file mode 100644 index 0000000..6aea5b7 --- /dev/null +++ b/test/tile_item_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'tile_item' + +class TileItemTest < Minitest::Test + def setup + @obj = Object.new + @id = 123 + @render_symbol = 'X' + @render_priority = 2 + @tile_item = TileItem.new(@obj, id: @id, render_symbol: @render_symbol, render_priority: @render_priority) + end + + def test_initialize + assert_equal @obj, @tile_item.obj + assert_equal @id, @tile_item.id + assert_equal @render_symbol, @tile_item.render_symbol + assert_equal @render_priority, @tile_item.render_priority + end + + def test_to_h + expected_hash = { + id: @id, + type: @obj.class.name.downcase, + render_symbol: @render_symbol, + render_priority: @render_priority + } + assert_equal expected_hash, @tile_item.to_h + end +end diff --git a/test/tile_perlin_generator_test.rb b/test/tile_perlin_generator_test.rb new file mode 100644 index 0000000..07d6cdc --- /dev/null +++ b/test/tile_perlin_generator_test.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'mocha/minitest' +require 'tile_perlin_generator' +require 'ostruct' + +class TilePerlinGeneratorTest < Minitest::Test + def setup + @perlin_config = OpenStruct.new( + width: 100, + height: 100, + noise_seed: 123, + octaves: 4, + x_frequency: 2.0, + y_frequency: 2.0, + persistance: 0.5, + adjustment: 0.1 + ) + + @tile_perlin_generator = TilePerlinGenerator.new(@perlin_config) + end + + def test_initialize_with_valid_perlin_config + assert_equal @perlin_config, @tile_perlin_generator.perlin_config + end + + def test_generate + perlin_generator_mock = mock('Perlin::Generator') + noise_value = 0.3 + perlin_generator_mock.stubs(:[]).returns(noise_value) + Perlin::Generator.stubs(:new).returns(perlin_generator_mock) + + expected_generated_array = Array.new(@perlin_config[:height]) do |y| + Array.new(@perlin_config[:width]) do |x| + nx = @perlin_config[:x_frequency] * (x.to_f / @perlin_config[:width] - 0.5) + ny = @perlin_config[:y_frequency] * (y.to_f / @perlin_config[:height] - 0.5) + e = noise(nx, ny) + e = Math.cos(e)**6 + + with_adjustment(e) + end + end + + generated_array = @tile_perlin_generator.generate + + assert_equal expected_generated_array, generated_array + end + + def test_with_adjustment_without_adjustment + result = 0.8 + adjusted_result = @tile_perlin_generator.send(:with_adjustment, result) + + assert_equal 0.9, adjusted_result + end + + def test_with_adjustment_with_positive_adjustment + result = 0.7 + @perlin_config[:adjustment] = 0.2 + adjusted_result = @tile_perlin_generator.send(:with_adjustment, result) + + assert_equal result + @perlin_config[:adjustment], adjusted_result + end + + def test_with_adjustment_with_negative_adjustment + result = 0.7 + @perlin_config[:adjustment] = -0.2 + adjusted_result = @tile_perlin_generator.send(:with_adjustment, result) + + assert_equal result + @perlin_config[:adjustment], adjusted_result + end + + def test_with_adjustment_with_positive_adjustment_clamped_to_1 + result = 0.9 + @perlin_config[:adjustment] = 0.3 + adjusted_result = @tile_perlin_generator.send(:with_adjustment, result) + + assert_equal 1.0, adjusted_result + end + + def test_with_adjustment_with_negative_adjustment_clamped_to_0 + result = 0.1 + @perlin_config[:adjustment] = -0.3 + adjusted_result = @tile_perlin_generator.send(:with_adjustment, result) + assert_equal 0.0, adjusted_result + end + + def test_noise + x = 0.5 + y = 0.5 + noise_value = 0.4 + + result = @tile_perlin_generator.send(:noise, x, y) + + assert_in_delta (noise_value / 2) + 0.5, result, 0.01 + end + + private + + def with_adjustment(result) + if @perlin_config.adjustment != 0.0 + result += @perlin_config.adjustment + result = [result, 1.0].min + [result, 0.0].max + else + result + end + end + + def noise(x, y) + (noise_generator[x, y] / 2) + 0.5 + end + + def noise_generator + @noise_generator ||= + Perlin::Generator.new( + @perlin_config.noise_seed, + @perlin_config.persistance, + @perlin_config.octaves + ) + end +end diff --git a/test/tile_test.rb b/test/tile_test.rb new file mode 100644 index 0000000..5507f81 --- /dev/null +++ b/test/tile_test.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'mocha/minitest' +require 'ostruct' +require 'tile' + +class TileTest < Minitest::Test + def setup + @map = mock('Map') + @map.stubs(:config).returns(OpenStruct.new(generate_flora: true)) + @map.stubs(:tiles).returns([]) + @x = 1 + @y = 1 + @height = 0.5 + @moist = 0.5 + @temp = 0.5 + + @tile = Tile.new( + map: @map, + x: @x, + y: @y, + height: @height, + moist: @moist, + temp: @temp + ) + end + + def test_initialize_with_valid_parameters + assert_equal @map, @tile.map + assert_equal @x, @tile.x + assert_equal @y, @tile.y + assert_equal @height, @tile.height + assert_equal @moist, @tile.moist + assert_equal @temp, @tile.temp + end + + def test_surrounding_tiles + distance = 1 + mock_tile = mock('Tile') + @map.stubs(:tiles).returns([[mock_tile, mock_tile, mock_tile], [mock_tile, @tile, mock_tile], + [mock_tile, mock_tile, mock_tile]]) + + surrounding_tiles = @tile.surrounding_tiles(distance) + + assert_equal 9, surrounding_tiles.size + assert_equal mock_tile, surrounding_tiles[0] + assert_equal mock_tile, surrounding_tiles[1] + assert_equal mock_tile, surrounding_tiles[2] + assert_equal mock_tile, surrounding_tiles[3] + assert_equal @tile, surrounding_tiles[4] + assert_equal mock_tile, surrounding_tiles[5] + assert_equal mock_tile, surrounding_tiles[6] + assert_equal mock_tile, surrounding_tiles[7] + assert_equal mock_tile, surrounding_tiles[8] + end + + def test_biome + mock_biome = mock('Biome') + Biome.stubs(:from).returns(mock_biome) + + assert_equal mock_biome, @tile.biome + end + + def test_items_with_flora_generation_disabled + @tile.expects(:items_generated_with_flora_if_applicable).returns([]) + + items = @tile.items + + assert_empty items + end + + def test_items_with_flora_generation_enabled_and_flora_available + @tile.expects(:items_generated_with_flora_if_applicable).returns(['Flower']) + + items = @tile.items + + assert_equal ['Flower'], items + end + + def test_items_with_flora_generation_enabled_but_flora_unavailable + @tile.expects(:items_generated_with_flora_if_applicable).returns([]) + + biome = mock('Biome') + biome.stubs(:flora_available).returns(false) + @tile.stubs(:biome).returns(biome) + + items = @tile.items + + assert_empty items + end + + def test_add_item_with_valid_tile_item + tile_item = TileItem.new('test', render_symbol: 'test') + + @tile.add_item(tile_item) + + assert_equal [tile_item], @tile.items + end + + def test_add_item_with_invalid_tile_item + tile_item = :invalid + + assert_raises(ArgumentError) do + @tile.add_item(tile_item) + end + end + + def test_item_with_highest_priority + tile_item1 = mock('TileItem', render_priority: 1) + tile_item2 = mock('TileItem', render_priority: 2) + tile_item3 = mock('TileItem', render_priority: 3) + @tile.stubs(:items).returns([tile_item1, tile_item2, tile_item3]) + + highest_priority_item = @tile.item_with_highest_priority + + assert_equal tile_item3, highest_priority_item + end + + def test_to_h + mock_biome = mock('Biome') + biome_hash = { name: 'Forest', color: '#00FF00' } + mock_biome.stubs(:to_h).returns(biome_hash) + @tile.stubs(:biome).returns(mock_biome) + + tile_item = mock('TileItem') + tile_item_hash = { name: 'Flower', symbol: '*' } + tile_item.stubs(:to_h).returns(tile_item_hash) + @tile.stubs(:items).returns([tile_item]) + + expected_hash = { + x: @x, + y: @y, + height: @height, + moist: @moist, + temp: @temp, + biome: biome_hash, + items: [tile_item_hash] + } + + assert_equal expected_hash, @tile.to_h + end +end