use crate::{
grid::{direction::Direction, hex_grid::*},
map_parameters::{MapParameters, ResourceSetting, WorldGrid},
nation::Nation,
tile::Tile,
tile_component::*,
};
use arrayvec::ArrayVec;
use enum_map::{Enum, EnumMap, enum_map};
use rand::{Rng, SeedableRng, rngs::StdRng};
use std::{
cmp::{max, min},
collections::{BTreeMap, HashMap},
};
mod impls;
pub(crate) use impls::*;
#[derive(PartialEq, Debug)]
pub struct TileMap {
pub random_number_generator: StdRng,
pub world_grid: WorldGrid,
pub river_list: Vec<River>,
pub terrain_type_list: Vec<TerrainType>,
pub base_terrain_list: Vec<BaseTerrain>,
pub feature_list: Vec<Option<Feature>>,
pub natural_wonder_list: Vec<Option<NaturalWonder>>,
pub resource_list: Vec<Option<(Resource, u32)>>,
pub area_id_list: Vec<usize>,
pub landmass_id_list: Vec<usize>,
pub area_list: Vec<Area>,
pub landmass_list: Vec<Landmass>,
pub starting_tile_and_civilization: BTreeMap<Tile, Nation>,
pub starting_tile_and_city_state: BTreeMap<Tile, Nation>,
region_list: ArrayVec<Region, { MapParameters::MAX_CIVILIZATION_NUM as usize }>,
pub layer_data: EnumMap<Layer, Vec<u32>>,
city_state_starting_tile_and_region_index: Vec<(Tile, Option<usize>)>,
luxury_resource_role: LuxuryResourceRole,
luxury_assign_to_region_count: HashMap<Resource, u32>,
}
impl TileMap {
pub fn new(map_parameters: &MapParameters) -> Self {
let random_number_generator = StdRng::seed_from_u64(map_parameters.seed);
let world_grid = map_parameters.world_grid;
let height = world_grid.grid.size.height;
let width = world_grid.grid.size.width;
let size = (height * width) as usize;
let layer_data = enum_map! {
_ => vec![0; size],
};
let region_list = ArrayVec::new();
Self {
random_number_generator,
world_grid,
river_list: Vec::new(),
terrain_type_list: vec![TerrainType::Water; size],
base_terrain_list: vec![BaseTerrain::Ocean; size],
feature_list: vec![None; size],
natural_wonder_list: vec![None; size],
resource_list: vec![None; size],
area_id_list: Vec::with_capacity(size),
landmass_id_list: Vec::with_capacity(size),
area_list: Vec::new(),
landmass_list: Vec::new(),
region_list,
layer_data,
starting_tile_and_civilization: BTreeMap::new(),
starting_tile_and_city_state: BTreeMap::new(),
city_state_starting_tile_and_region_index: Vec::new(),
luxury_resource_role: LuxuryResourceRole::default(),
luxury_assign_to_region_count: HashMap::new(),
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub fn all_tiles(&self) -> impl Iterator<Item = Tile> + use<> {
let size = &self.world_grid.size();
(0..((size.width * size.height) as usize)).map(Tile::new)
}
pub fn place_impact_and_ripples(&mut self, tile: Tile, layer: Layer, radius: u32) {
match layer {
Layer::Strategic | Layer::Luxury | Layer::Bonus | Layer::Fish => {
self.place_impact_and_ripples_for_resource(tile, layer, radius)
}
Layer::CityState => {
self.place_impact_and_ripples_for_resource(tile, Layer::CityState, 4);
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::Strategic, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::Bonus, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::Fish, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::Marble, 3);
}
Layer::NaturalWonder => {
self.place_impact_and_ripples_for_resource(
tile,
Layer::NaturalWonder,
self.world_grid.size().height / 5,
);
let natural_wonder = tile.natural_wonder(self);
if let Some(natural_wonder) = natural_wonder {
match natural_wonder {
NaturalWonder::MountFuji => {
self.place_impact_and_ripples_for_resource(tile, Layer::Strategic, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::Bonus, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::CityState, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::Marble, 1);
}
NaturalWonder::Krakatoa | NaturalWonder::GreatBarrierReef => {
self.place_impact_and_ripples_for_resource(tile, Layer::Strategic, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Bonus, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::CityState, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Marble, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Fish, 1);
}
_ => {
self.place_impact_and_ripples_for_resource(tile, Layer::Strategic, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Bonus, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::CityState, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Marble, 1);
}
}
}
}
Layer::Marble => {
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 1);
self.place_impact_and_ripples_for_resource(tile, Layer::Marble, 6);
}
Layer::Civilization => self.place_impact_and_ripples_for_civilization(tile),
}
}
fn place_impact_and_ripples_for_civilization(&mut self, tile: Tile) {
let grid = self.world_grid.grid;
let impact_value = 99;
let ripple_values = [97, 95, 92, 89, 69, 57, 24, 15];
self.place_impact_and_ripples_for_resource(tile, Layer::Luxury, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::Strategic, 0);
self.place_impact_and_ripples_for_resource(tile, Layer::Bonus, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::Fish, 3);
self.place_impact_and_ripples_for_resource(tile, Layer::NaturalWonder, 4);
self.layer_data[Layer::Civilization][tile.index()] = impact_value;
self.layer_data[Layer::CityState][tile.index()] = 1;
for (index, ripple_value) in ripple_values.into_iter().enumerate() {
let distance = index as u32 + 1;
tile.tiles_at_distance(distance, grid)
.for_each(|tile_at_distance| {
let mut current_value =
self.layer_data[Layer::Civilization][tile_at_distance.index()];
if current_value != 0 {
let stronger_value = max(current_value, ripple_value);
let overlap_value = min(97, (stronger_value as f64 * 1.2) as u32);
current_value = overlap_value;
} else {
current_value = ripple_value;
}
self.layer_data[Layer::Civilization][tile_at_distance.index()] = current_value;
if distance <= 6 {
self.layer_data[Layer::CityState][tile_at_distance.index()] = 1;
}
})
}
}
fn place_impact_and_ripples_for_resource(&mut self, tile: Tile, layer: Layer, radius: u32) {
debug_assert_ne!(
layer,
Layer::Civilization,
"`place_impact_and_ripples_for_resource` should not be used for `Layer::Civilization`, use `place_impact_and_ripples_for_civilization` instead."
);
let grid = self.world_grid.grid;
let impact_value = 99;
self.layer_data[layer][tile.index()] = impact_value;
if radius == 0 {
return;
}
if radius > 0 && radius < (grid.size.height / 2) {
for distance in 1..=radius {
let ripple_value = radius - distance + 1;
tile.tiles_at_distance(distance, grid)
.for_each(|tile_at_distance| {
let mut current_value = self.layer_data[layer][tile_at_distance.index()];
match layer {
Layer::Strategic | Layer::Luxury | Layer::Bonus | Layer::NaturalWonder | Layer::Fish => {
if current_value != 0 {
let stronger_value = max(current_value, ripple_value);
let overlap_value = min(50, stronger_value + 2);
current_value = overlap_value;
} else {
current_value = ripple_value;
}
}
Layer::CityState | Layer::Marble => {
current_value = 1;
}
Layer::Civilization => {
unreachable!("Civilization layer should not be used in place_resource_impact function.");
}
}
self.layer_data[layer][tile_at_distance.index()] = current_value;
})
}
}
}
pub fn attempt_to_place_hill_at_tile(&mut self, tile: Tile) -> bool {
if tile.resource(self).is_none()
&& tile.terrain_type(self) != TerrainType::Water
&& tile.feature(self) != Some(Feature::Forest)
&& !tile.has_river(self)
{
tile.set_terrain_type(self, TerrainType::Hill);
tile.clear_feature(self);
tile.clear_natural_wonder(self);
true
} else {
false
}
}
pub fn attempt_to_place_bonus_resource_at_tile(
&mut self,
tile: Tile,
allow_oasis: bool,
) -> (bool, bool) {
let terrain_type = tile.terrain_type(self);
let base_terrain = tile.base_terrain(self);
let feature = tile.feature(self);
if tile.resource(self).is_none()
&& base_terrain != BaseTerrain::Snow
&& feature != Some(Feature::Oasis)
{
match terrain_type {
TerrainType::Water => {
if base_terrain == BaseTerrain::Coast && feature.is_none() {
tile.set_resource(self, Resource::Fish, 1);
return (true, false);
}
}
TerrainType::Flatland => {
if feature.is_none() {
match base_terrain {
BaseTerrain::Grassland => {
tile.set_resource(self, Resource::Cattle, 1);
return (true, false);
}
BaseTerrain::Desert => {
if tile.is_freshwater(self) {
tile.set_resource(self, Resource::Wheat, 1);
return (true, false);
} else if allow_oasis {
tile.set_feature(self, Feature::Oasis);
return (true, true);
}
}
BaseTerrain::Plain => {
tile.set_resource(self, Resource::Wheat, 1);
return (true, false);
}
BaseTerrain::Tundra => {
tile.set_resource(self, Resource::Deer, 1);
return (true, false);
}
_ => {
unreachable!()
}
}
} else if feature == Some(Feature::Forest) {
tile.set_resource(self, Resource::Deer, 1);
return (true, false);
} else if feature == Some(Feature::Jungle) {
tile.set_resource(self, Resource::Bananas, 1);
return (true, false);
}
}
TerrainType::Mountain => (),
TerrainType::Hill => {
if feature.is_none() {
tile.set_resource(self, Resource::Sheep, 1);
return (true, false);
} else if feature == Some(Feature::Forest) {
tile.set_resource(self, Resource::Deer, 1);
return (true, false);
} else if feature == Some(Feature::Jungle) {
tile.set_resource(self, Resource::Bananas, 1);
return (true, false);
}
}
}
}
(false, false)
}
#[allow(clippy::too_many_arguments)]
pub fn place_specific_number_of_resources(
&mut self,
resource: Resource,
quantity: u32,
amount: u32,
ratio: f64,
layer: Option<Layer>,
min_radius: u32,
max_radius: u32,
tile_list: &[Tile],
) -> u32 {
debug_assert!(
max_radius >= min_radius,
"'max_radius' must be greater than or equal to 'min_radius'!"
);
if tile_list.is_empty() {
return amount;
}
let has_impact = matches!(
layer,
Some(Layer::Strategic | Layer::Luxury | Layer::Bonus | Layer::Fish)
);
let mut num_left_to_place = amount;
let num_candidate_tiles = (ratio * tile_list.len() as f64).ceil() as u32;
let num_resources = min(amount, num_candidate_tiles);
for _ in 1..=num_resources {
for &tile in tile_list.iter() {
if !has_impact || self.layer_data[layer.unwrap()][tile.index()] == 0 {
if tile.resource(self).is_none() {
tile.set_resource(self, resource, quantity);
num_left_to_place -= 1;
}
if has_impact {
let radius = self
.random_number_generator
.random_range(min_radius..=max_radius);
self.place_impact_and_ripples(tile, layer.unwrap(), radius)
}
break;
}
}
}
num_left_to_place
}
pub fn clear_ice_near_city_site(&mut self, city_site: Tile, radius: u32) {
let grid = self.world_grid.grid;
for ripple_radius in 1..=radius {
city_site
.tiles_at_distance(ripple_radius, grid)
.for_each(|tile_at_distance| {
let feature = tile_at_distance.feature(self);
if feature == Some(Feature::Ice) {
tile_at_distance.clear_feature(self);
}
})
}
}
}
pub fn get_major_strategic_resource_quantity_values(
resource_setting: ResourceSetting,
) -> (u32, u32, u32, u32, u32, u32) {
let (uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt) = match resource_setting {
ResourceSetting::Sparse => (2, 4, 5, 4, 5, 5),
ResourceSetting::Abundant => (4, 6, 9, 9, 10, 10),
_ => (4, 4, 7, 6, 7, 8), };
(uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt)
}
#[derive(Enum, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Layer {
Strategic,
Luxury,
Bonus,
Fish,
CityState,
NaturalWonder,
Marble,
Civilization,
}
pub type River = Vec<RiverEdge>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RiverEdge {
pub tile: Tile,
pub flow_direction: Direction,
}
impl RiverEdge {
pub fn new(tile: Tile, flow_direction: Direction) -> Self {
Self {
tile,
flow_direction,
}
}
pub fn start_and_end_corner_directions(&self, grid: HexGrid) -> [Direction; 2] {
use {Direction::*, HexOrientation::*};
match (grid.layout.orientation, self.flow_direction) {
(Pointy, North) => [SouthEast, NorthEast], (Pointy, NorthEast) => [South, SouthEast], (Pointy, SouthEast) => [SouthWest, South], (Pointy, South) => [NorthEast, SouthEast], (Pointy, SouthWest) => [SouthEast, South], (Pointy, NorthWest) => [South, SouthWest],
(Flat, NorthEast) => [SouthEast, East], (Flat, East) => [SouthWest, SouthEast], (Flat, SouthEast) => [NorthEast, East], (Flat, SouthWest) => [East, SouthEast], (Flat, West) => [SouthEast, SouthWest], (Flat, NorthWest) => [East, NorthEast],
(Pointy, East | West) | (Flat, North | South) => {
panic!("Invalid flow direction for this hex orientation")
}
}
}
pub fn edge_direction(&self, grid: HexGrid) -> Direction {
use {Direction::*, HexOrientation::*};
match (grid.layout.orientation, self.flow_direction) {
(Pointy, North | South) => East,
(Pointy, NorthEast | SouthWest) => SouthEast,
(Pointy, NorthWest | SouthEast) => SouthWest,
(Flat, NorthWest | SouthEast) => NorthEast,
(Flat, NorthEast | SouthWest) => SouthEast,
(Flat, East | West) => South,
_ => panic!("Invalid flow direction for hex orientation"),
}
}
}