feat: add coal generation

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-03 21:21:37 +08:00
parent 1354d9c08a
commit afc8d8fa1c
Signed by: szTom
GPG Key ID: 072D999D60C6473C
11 changed files with 330 additions and 26 deletions

View File

@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.27)
set(ISTD_TILEMAP_SRC set(ISTD_TILEMAP_SRC
src/pass/base_tile_type.cpp src/pass/base_tile_type.cpp
src/pass/biome.cpp src/pass/biome.cpp
src/pass/coal.cpp
src/pass/deepwater.cpp src/pass/deepwater.cpp
src/pass/mineral_cluster.cpp src/pass/mineral_cluster.cpp
src/pass/mountain_hole_fill.cpp src/pass/mountain_hole_fill.cpp

View File

@ -81,21 +81,7 @@ Configuration for terrain generation.
```cpp ```cpp
struct GenerationConfig { struct GenerationConfig {
Seed seed; Seed seed;
// For more options, see generation.h
// Noise parameters
double temperature_scale = 0.05;
double humidity_scale = 0.05;
double base_scale = 0.08;
int temperature_octaves = 3;
int humidity_octaves = 3;
int base_octaves = 3;
// Oil generation parameters
double oil_density = 0.8; // Average oil fields per chunk
std::uint32_t oil_cluster_min_size = 2; // Minimum tiles per cluster
std::uint32_t oil_cluster_max_size = 6; // Maximum tiles per cluster
double oil_biome_preference = 2.0; // Multiplier for preferred biomes
}; };
``` ```

View File

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.27)
# Examples for the tilemap library # Examples for the tilemap library
# Each example is built as a separate executable # Each example is built as a separate executable
# Biome system demonstration # Tilemap system demonstration
add_executable(tilemap_demo tilemap_demo.cpp) add_executable(tilemap_demo tilemap_demo.cpp)
target_link_libraries(tilemap_demo PRIVATE istd_tilemap) target_link_libraries(tilemap_demo PRIVATE istd_tilemap)
target_include_directories(tilemap_demo PRIVATE ../include) target_include_directories(tilemap_demo PRIVATE ../include)

View File

@ -241,6 +241,7 @@ constexpr Color OIL(0, 0, 0); // Black
constexpr Color HEMATITE(255, 0, 0); // Red constexpr Color HEMATITE(255, 0, 0); // Red
constexpr Color TITANOMAGNETITE(128, 0, 128); // Purple constexpr Color TITANOMAGNETITE(128, 0, 128); // Purple
constexpr Color GIBBSITE(255, 255, 0); // Yellow constexpr Color GIBBSITE(255, 255, 0); // Yellow
constexpr Color COAL(64, 64, 64); // Dark gray
} // namespace BmpColors } // namespace BmpColors
#endif // BMP_H #endif // BMP_H

View File

@ -19,6 +19,8 @@ BmpColors::Color get_tile_color(const istd::Tile &tile) {
return BmpColors::TITANOMAGNETITE; return BmpColors::TITANOMAGNETITE;
case istd::SurfaceTileType::Gibbsite: case istd::SurfaceTileType::Gibbsite:
return BmpColors::GIBBSITE; return BmpColors::GIBBSITE;
case istd::SurfaceTileType::Coal:
return BmpColors::COAL;
case istd::SurfaceTileType::Empty: case istd::SurfaceTileType::Empty:
default: default:
break; // Fall through to base tile color break; // Fall through to base tile color
@ -101,6 +103,7 @@ void print_statistics(const istd::TileMap &tilemap) {
int hematite_count = 0; // Count hematite surface tiles int hematite_count = 0; // Count hematite surface tiles
int titanomagnetite_count = 0; // Count titanomagnetite surface tiles int titanomagnetite_count = 0; // Count titanomagnetite surface tiles
int gibbsite_count = 0; // Count gibbsite surface tiles int gibbsite_count = 0; // Count gibbsite surface tiles
int coal_count = 0; // Count coal surface tiles
int mountain_edge_count = 0; // Count mountain edge tiles int mountain_edge_count = 0; // Count mountain edge tiles
const int chunks_per_side = tilemap.get_size(); const int chunks_per_side = tilemap.get_size();
const int tiles_per_chunk = istd::Chunk::size; const int tiles_per_chunk = istd::Chunk::size;
@ -128,6 +131,9 @@ void print_statistics(const istd::TileMap &tilemap) {
case istd::SurfaceTileType::Gibbsite: case istd::SurfaceTileType::Gibbsite:
gibbsite_count++; gibbsite_count++;
break; break;
case istd::SurfaceTileType::Coal:
coal_count++;
break;
default: default:
break; break;
} }
@ -195,6 +201,7 @@ void print_statistics(const istd::TileMap &tilemap) {
print_mineral_stats("Hematite", hematite_count); print_mineral_stats("Hematite", hematite_count);
print_mineral_stats("Titanomagnetite", titanomagnetite_count); print_mineral_stats("Titanomagnetite", titanomagnetite_count);
print_mineral_stats("Gibbsite", gibbsite_count); print_mineral_stats("Gibbsite", gibbsite_count);
print_mineral_stats("Coal", coal_count);
// Mountain edge statistics for mineral context // Mountain edge statistics for mineral context
int mountain_count = tile_counts[static_cast<int>( int mountain_count = tile_counts[static_cast<int>(

View File

@ -47,16 +47,24 @@ struct GenerationConfig {
// of 255) // of 255)
// Mineral cluster generation parameters // Mineral cluster generation parameters
std::uint16_t hematite_density = 450; // ~1.8 per chunk (out of 255) std::uint16_t hematite_density = 450; // ~1.8 per chunk (n / 255)
std::uint16_t titanomagnetite_density = 300; // ~1.2 per chunk (out of 255) std::uint16_t titanomagnetite_density = 300; // ~1.2 per chunk (n / 255)
std::uint16_t gibbsite_density = 235; // ~0.9 per chunk (out of 255) std::uint16_t gibbsite_density = 235; // ~0.9 per chunk (n / 255)
std::uint8_t mineral_cluster_min_size = 2; // Minimum tiles per mineral std::uint8_t mineral_cluster_min_size = 2; // Minimum tiles per mineral
// cluster // cluster
std::uint8_t mineral_cluster_max_size = 5; // Maximum tiles per mineral std::uint8_t mineral_cluster_max_size = 5; // Maximum tiles per mineral
// cluster // cluster
std::uint8_t mineral_base_probe = 192; // Base probability for mineral std::uint8_t mineral_base_prob = 192; // Base probability for mineral
// placement // placement
// Coal generation parameters
std::uint8_t coal_seeds_per_chunk = 3; // Number of initial coal seeds per
// chunk
std::uint8_t coal_evolution_steps = 6; // Number of cellular automata
// evolution steps
std::uint8_t coal_growth_base_prob = 21; // Base probability for coal
// growth per neighbor (n / 255)
}; };
// Terrain generator class that manages the generation process // Terrain generator class that manages the generation process
@ -126,6 +134,12 @@ private:
* @param tilemap The tilemap to process * @param tilemap The tilemap to process
*/ */
void mineral_cluster_pass(TileMap &tilemap); void mineral_cluster_pass(TileMap &tilemap);
/**
* @brief Generate coal deposits on suitable terrain
* @param tilemap The tilemap to process
*/
void coal_pass(TileMap &tilemap);
}; };
/** /**
* @brief Generate a tilemap using the new biome-based system * @brief Generate a tilemap using the new biome-based system

View File

@ -0,0 +1,86 @@
#ifndef TILEMAP_PASS_COAL_H
#define TILEMAP_PASS_COAL_H
#include "tilemap/generation.h"
#include "tilemap/noise.h"
namespace istd {
/**
* @brief Generates coal deposits on suitable terrain using cellular automata
*/
class CoalGenerationPass {
private:
const GenerationConfig &config_;
Xoroshiro128PP rng_;
DiscreteRandomNoise noise_;
public:
/**
* @brief Construct a coal generation pass
* @param config Generation configuration parameters
* @param rng Random number generator for coal placement
* @param noise_rng Random number generator for noise-based operations
*/
CoalGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng,
Xoroshiro128PP noise_rng
);
/**
* @brief Generate coal deposits using cellular automata
* @param tilemap The tilemap to process
*/
void operator()(TileMap &tilemap);
private:
/**
* @brief Generate initial coal seed points for a chunk
* @param tilemap The tilemap to analyze
* @param chunk_x Chunk X coordinate
* @param chunk_y Chunk Y coordinate
* @param seeds Output vector to fill with seed positions
*/
void chunk_coal_seeds(
const TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y,
std::vector<TilePos> &seeds
);
/**
* @brief Evolve coal deposits using cellular automata
* @param tilemap The tilemap to modify
* @param initial_seeds Initial coal seed positions
*/
void evolve_coal_deposits(
TileMap &tilemap, const std::vector<TilePos> &initial_seeds
);
/**
* @brief Check if a tile is suitable for coal placement
* @param tilemap The tilemap to check
* @param pos Position to check
* @return True if coal can be placed at this position
*/
bool is_suitable_for_coal(const TileMap &tilemap, TilePos pos) const;
/**
* @brief Get the coal growth probability for a specific biome
* @param biome The biome type
* @return Growth probability multiplier (x / 255)
*/
std::uint8_t get_biome_coal_growth_probability(BiomeType biome) const;
/**
* @brief Count coal neighbors around a position
* @param tilemap The tilemap to check
* @param pos Position to check around
* @return Number of coal neighbors
*/
std::uint8_t count_coal_neighbors(
const TileMap &tilemap, TilePos pos
) const;
};
} // namespace istd
#endif // TILEMAP_PASS_COAL_H

View File

@ -20,6 +20,7 @@ enum class SurfaceTileType : std::uint8_t {
Hematite, Hematite,
Titanomagnetite, Titanomagnetite,
Gibbsite, Gibbsite,
Coal,
// Player built structures // Player built structures
// (not used in generation, but can be placed by player) // (not used in generation, but can be placed by player)

View File

@ -13,6 +13,7 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
deepwater_pass(tilemap); deepwater_pass(tilemap);
oil_pass(tilemap); oil_pass(tilemap);
mineral_cluster_pass(tilemap); mineral_cluster_pass(tilemap);
coal_pass(tilemap);
} }
void map_generate(TileMap &tilemap, const GenerationConfig &config) { void map_generate(TileMap &tilemap, const GenerationConfig &config) {

207
tilemap/src/pass/coal.cpp Normal file
View File

@ -0,0 +1,207 @@
#include "tilemap/pass/coal.h"
#include "tilemap/biome.h"
#include "tilemap/chunk.h"
#include "tilemap/generation.h"
#include "tilemap/noise.h"
#include "tilemap/xoroshiro.h"
#include <algorithm>
#include <queue>
#include <utility>
namespace istd {
CoalGenerationPass::CoalGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng, Xoroshiro128PP noise_rng
)
: config_(config), rng_(rng), noise_(noise_rng) {}
void CoalGenerationPass::operator()(TileMap &tilemap) {
std::uint8_t map_size = tilemap.get_size();
std::vector<TilePos> all_seeds;
// Generate coal seeds for each chunk
for (std::uint8_t chunk_x = 0; chunk_x < map_size; ++chunk_x) {
for (std::uint8_t chunk_y = 0; chunk_y < map_size; ++chunk_y) {
chunk_coal_seeds(tilemap, chunk_x, chunk_y, all_seeds);
}
}
// Evolve coal deposits using cellular automata
evolve_coal_deposits(tilemap, all_seeds);
}
void CoalGenerationPass::chunk_coal_seeds(
const TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y,
std::vector<TilePos> &seeds
) {
// Use a max heap to keep top N positions by noise value
using PosNoisePair = std::pair<std::uint64_t, TilePos>;
std::priority_queue<PosNoisePair> heap;
for (std::uint8_t local_x = 0; local_x < Chunk::size; ++local_x) {
for (std::uint8_t local_y = 0; local_y < Chunk::size; ++local_y) {
TilePos candidate{chunk_x, chunk_y, local_x, local_y};
if (!is_suitable_for_coal(tilemap, candidate)) {
continue;
}
auto [global_x, global_y] = candidate.to_global();
auto noise_val = noise_.noise(global_x, global_y, 0x90);
heap.emplace(noise_val, candidate);
if (heap.size() > config_.coal_seeds_per_chunk) {
heap.pop();
}
}
}
while (!heap.empty()) {
seeds.push_back(heap.top().second);
heap.pop();
}
}
void CoalGenerationPass::evolve_coal_deposits(
TileMap &tilemap, const std::vector<TilePos> &initial_seeds
) {
// Place initial seeds
for (const auto &seed : initial_seeds) {
Tile &tile = tilemap.get_tile(seed);
if (tile.surface == SurfaceTileType::Empty) {
tile.surface = SurfaceTileType::Coal;
}
}
// Evolve using cellular automata
for (std::uint8_t step = 1; step <= config_.coal_evolution_steps; ++step) {
std::vector<TilePos> new_coal_positions;
std::uint8_t map_size = tilemap.get_size();
// Iterate through all tiles
for (std::uint8_t chunk_x = 0; chunk_x < map_size; ++chunk_x) {
for (std::uint8_t chunk_y = 0; chunk_y < map_size; ++chunk_y) {
const Chunk &chunk = tilemap.get_chunk(chunk_x, chunk_y);
for (std::uint8_t local_x = 0; local_x < Chunk::size;
++local_x) {
for (std::uint8_t local_y = 0; local_y < Chunk::size;
++local_y) {
TilePos pos{chunk_x, chunk_y, local_x, local_y};
const Tile &tile = tilemap.get_tile(pos);
// Skip if not suitable for coal
if (!is_suitable_for_coal(tilemap, pos)) {
continue;
}
// Skip if already has coal
if (tile.surface == SurfaceTileType::Coal) {
continue;
}
// Count coal neighbors
std::uint8_t coal_neighbors = count_coal_neighbors(
tilemap, pos
);
if (coal_neighbors > 0) {
// Get biome for this position
BiomeType biome = chunk.get_biome(local_x, local_y);
auto biome_probability
= get_biome_coal_growth_probability(biome);
// Base probability increases with more coal
// neighbors
std::uint32_t base_probability = coal_neighbors
* config_.coal_growth_base_prob;
std::uint8_t final_probability = std::clamp(
base_probability * biome_probability / 255, 0u,
255u
);
// Use noise to decide whether to grow coal here
auto [global_x, global_y] = pos.to_global();
std::uint8_t sample = 0xFF
& noise_.noise(global_x, global_y, step);
if (sample < final_probability) {
new_coal_positions.push_back(pos);
}
}
}
}
}
}
// Place new coal
for (const auto &pos : new_coal_positions) {
Tile &tile = tilemap.get_tile(pos);
if (tile.surface == SurfaceTileType::Empty) {
tile.surface = SurfaceTileType::Coal;
}
}
}
}
bool CoalGenerationPass::is_suitable_for_coal(
const TileMap &tilemap, TilePos pos
) const {
const Tile &tile = tilemap.get_tile(pos);
return (tile.base == BaseTileType::Sand || tile.base == BaseTileType::Land)
&& tile.surface == SurfaceTileType::Empty;
}
// Returns the coal growth probability for a given biome.
// Higher values mean coal is more likely to spread in that biome.
std::uint8_t CoalGenerationPass::get_biome_coal_growth_probability(
BiomeType biome
) const {
switch (biome) {
case BiomeType::Forest:
return 255;
case BiomeType::LukeOcean:
return 204;
case BiomeType::Savanna:
return 153;
case BiomeType::Plains:
return 128;
case BiomeType::SnowyPlains:
case BiomeType::Ocean:
return 102;
case BiomeType::SnowyPeeks:
return 77;
case BiomeType::Desert:
return 51;
case BiomeType::FrozenOcean:
return 26;
default:
std::unreachable();
}
}
std::uint8_t CoalGenerationPass::count_coal_neighbors(
const TileMap &tilemap, TilePos pos
) const {
std::uint8_t count = 0;
auto neighbors = tilemap.get_neighbors(pos);
for (const auto neighbor_pos : neighbors) {
const Tile &neighbor_tile = tilemap.get_tile(neighbor_pos);
if (neighbor_tile.surface == SurfaceTileType::Coal) {
++count;
}
}
return count;
}
void TerrainGenerator::coal_pass(TileMap &tilemap) {
auto rng = master_rng_;
master_rng_ = master_rng_.jump_96();
auto noise_rng = master_rng_;
master_rng_ = master_rng_.jump_96();
CoalGenerationPass pass(config_, rng, noise_rng);
pass(tilemap);
}
} // namespace istd

View File

@ -87,7 +87,7 @@ std::vector<TilePos> MineralClusterGenerationPass::generate_mineral_centers(
mineral_type mineral_type
) // Use mineral type as seed variation ) // Use mineral type as seed variation
); );
if (sample < config_.mineral_base_probe) { if (sample < config_.mineral_base_prob) {
centers.push_back(candidate); centers.push_back(candidate);
} }
} }