civ_map_generator 0.1.5

A civilization map generator
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! This module defines the [`Tile`] struct and its associated methods.
//! It provides functionality to interact with tiles on a map, including retrieving
//! their properties, neighbors, and coordinates in different formats.

use crate::{
    grid::{
        Cell, Grid,
        direction::Direction,
        hex_grid::{Hex, HexGrid},
        offset_coordinate::OffsetCoordinate,
    },
    map_parameters::MapParameters,
    ruleset::Ruleset,
    tile_component::{BaseTerrain, Feature, NaturalWonder, Resource, TerrainType},
    tile_map::{Layer, Region, TileMap},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
/// `Tile` represents a tile on the map, where the `usize` is the index of the current tile.
///
/// The index indicates the tile's position on the map, typically used to access or reference specific tiles.
pub struct Tile(usize);

impl Tile {
    /// The maximum distance a `Settler` can move in one turn, without considering technologies, eras, improvements, etc.
    ///
    /// TODO: This should be a parameter read from the ruleset directly.
    const SETTLER_MOVEMENT_RANGE: u32 = 2;

    #[inline]
    pub const fn new(index: usize) -> Self {
        Self(index)
    }

    /// Creates a `Tile` from an `OffsetCoordinate` according to the specified `HexGrid`.
    ///
    pub fn from_offset(offset_coordinate: OffsetCoordinate, grid: HexGrid) -> Self {
        let cell = grid
            .offset_to_cell(offset_coordinate)
            .expect("Offset coordinate is out of bounds for the grid size");
        Self::from_cell(cell)
    }

    /// Creates a `Tile` from a `Cell`.
    ///
    #[inline(always)]
    pub fn from_cell(cell: Cell) -> Self {
        Self(cell.index())
    }

    #[inline(always)]
    pub fn to_cell(&self) -> Cell {
        Cell::new(self.0)
    }

    /// Get the index of the tile.
    ///
    /// The index indicates the tile's position on the map, typically used to access or reference specific tiles.
    #[inline(always)]
    pub const fn index(&self) -> usize {
        self.0
    }

    /// Converts a tile to the corresponding offset coordinate based on grid parameters.
    ///
    /// # Arguments
    ///
    /// - `grid`: A `HexGrid` that contains the map size information.
    ///
    /// # Returns
    /// Returns an `OffsetCoordinate` that corresponds to the provided tile, calculated based on the grid parameters.
    /// This coordinate represents the position of the tile within the map grid.
    ///
    pub fn to_offset(&self, grid: HexGrid) -> OffsetCoordinate {
        grid.cell_to_offset(self.to_cell())
    }

    /// Converts the current tile to a hexagonal coordinate based on the map parameters.
    ///
    /// # Returns
    /// Returns a `Hex` coordinate that corresponds to the provided map position, calculated based on the map grid parameters.
    /// This coordinate represents the position in hexagonal space within the map grid.
    ///
    /// # Panics
    /// This method will panic if the tile is out of bounds for the given map size.
    pub fn to_hex(&self, grid: HexGrid) -> Hex {
        let offset_coordinate = self.to_offset(grid);
        Hex::from_offset(offset_coordinate, grid.layout.orientation, grid.offset)
    }

    /// Calculates the latitude of the tile on the tile map.
    ///
    /// The latitude is defined such that:
    /// - The equator corresponds to a latitude of `0.0`.
    /// - The poles correspond to a latitude of `1.0`.
    ///
    /// As the latitude value approaches `0.0`, the tile is closer to the equator,
    /// while a value approaching `1.0` indicates proximity to the poles.
    ///
    /// # Arguments
    ///
    /// - `grid`: A `HexGrid` that contains the map size information.
    ///
    /// # Returns
    ///
    /// A `f64` representing the latitude of the tile, with values ranging from `0.0` (equator) to `1.0` (poles).
    ///
    /// # Panics
    ///
    /// This method will panic if the tile is out of bounds for the given map size.
    pub fn latitude(&self, grid: HexGrid) -> f64 {
        // We don't need to check if the index is valid here, as it has already been checked in `to_offset_coordinate`
        let y = self.to_offset(grid).0.y;
        let half_height = grid.height() as f64 / 2.0;
        ((half_height - y as f64) / half_height).abs()
    }

    /// Returns the terrain type of the tile at the given index.
    #[inline]
    pub fn terrain_type(&self, tile_map: &TileMap) -> TerrainType {
        tile_map.terrain_type_list[self.0]
    }

    /// Returns the base terrain of the tile at the given index.
    #[inline]
    pub fn base_terrain(&self, tile_map: &TileMap) -> BaseTerrain {
        tile_map.base_terrain_list[self.0]
    }

    /// Returns the feature of the tile at the given index.
    #[inline]
    pub fn feature(&self, tile_map: &TileMap) -> Option<Feature> {
        tile_map.feature_list[self.0]
    }

    /// Returns the natural wonder of the tile at the given index.
    #[inline]
    pub fn natural_wonder(&self, tile_map: &TileMap) -> Option<NaturalWonder> {
        tile_map.natural_wonder_list[self.0]
    }

    /// Returns the resource of the tile at the given index.
    #[inline]
    pub fn resource(&self, tile_map: &TileMap) -> Option<(Resource, u32)> {
        tile_map.resource_list[self.0]
    }

    /// Returns the area ID of the tile at the given index.
    #[inline]
    pub fn area_id(&self, tile_map: &TileMap) -> usize {
        tile_map.area_id_list[self.0]
    }

    /// Returns the landmass ID of the tile at the given index.
    #[inline]
    pub fn landmass_id(&self, tile_map: &TileMap) -> usize {
        tile_map.landmass_id_list[self.0]
    }

    /// Sets the terrain type of the tile at the given index.
    #[inline]
    pub fn set_terrain_type(&self, tile_map: &mut TileMap, terrain_type: TerrainType) {
        tile_map.terrain_type_list[self.0] = terrain_type;
    }

    /// Sets the base terrain of the tile at the given index.
    #[inline]
    pub fn set_base_terrain(&self, tile_map: &mut TileMap, base_terrain: BaseTerrain) {
        tile_map.base_terrain_list[self.0] = base_terrain;
    }

    /// Sets the feature of the tile at the given index.
    #[inline]
    pub fn set_feature(&self, tile_map: &mut TileMap, feature: Feature) {
        tile_map.feature_list[self.0] = Some(feature);
    }

    /// Clears the feature of the tile at the given index.
    #[inline]
    pub fn clear_feature(&self, tile_map: &mut TileMap) {
        tile_map.feature_list[self.0] = None;
    }

    /// Sets the natural wonder of the tile at the given index.
    #[inline]
    pub fn set_natural_wonder(&self, tile_map: &mut TileMap, natural_wonder: NaturalWonder) {
        tile_map.natural_wonder_list[self.0] = Some(natural_wonder);
    }

    /// Clears the natural wonder of the tile at the given index.
    #[inline]
    pub fn clear_natural_wonder(&self, tile_map: &mut TileMap) {
        tile_map.natural_wonder_list[self.0] = None;
    }

    /// Sets the resource of the tile at the given index.
    #[inline]
    pub fn set_resource(&self, tile_map: &mut TileMap, resource: Resource, quantity: u32) {
        tile_map.resource_list[self.0] = Some((resource, quantity));
    }

    /// Clears the resource of the tile at the given index.
    #[inline]
    pub fn clear_resource(&self, tile_map: &mut TileMap) {
        tile_map.resource_list[self.0] = None;
    }

    /// Sets the area ID of the tile at the given index.
    #[inline]
    pub fn set_area_id(&self, tile_map: &mut TileMap, area_id: usize) {
        tile_map.area_id_list[self.0] = area_id;
    }

    /// Sets the landmass ID of the tile at the given index.
    #[inline]
    pub fn set_landmass_id(&self, tile_map: &mut TileMap, landmass_id: usize) {
        tile_map.landmass_id_list[self.0] = landmass_id;
    }

    /// Returns an iterator over the neighboring tiles of the current tile.
    #[must_use = "iterators are lazy and do nothing unless consumed"]
    pub fn neighbor_tiles(&self, grid: HexGrid) -> impl Iterator<Item = Self> + use<> {
        self.tiles_at_distance(1, grid)
    }

    /// Retrieves the neighboring tile from the current tile in the specified direction.
    ///
    /// # Arguments
    ///
    /// - `direction`: The direction to locate the neighboring tile.
    /// - `grid`: The grid parameters that include layout and offset information.
    ///
    /// # Returns
    ///
    /// An `Option<Tile>`. This is `Some` if the neighboring tile exists,
    /// or `None` if the neighboring tile is invalid.
    ///
    /// # Panics
    ///
    /// This method will panic if the current tile is out of bounds for the given map size.
    pub fn neighbor_tile(&self, direction: Direction, grid: HexGrid) -> Option<Self> {
        grid.neighbor(self.to_cell(), direction)
            .map(Self::from_cell)
    }

    /// Returns an iterator over the tiles at the given distance from the current tile.
    #[must_use = "iterators are lazy and do nothing unless consumed"]
    pub fn tiles_at_distance(
        &self,
        distance: u32,
        grid: HexGrid,
    ) -> impl Iterator<Item = Self> + use<> {
        grid.cells_at_distance(self.to_cell(), distance)
            .map(Self::from_cell)
    }

    /// Returns an iterator over the tiles within the given distance from the current tile, including the current tile.
    #[must_use = "iterators are lazy and do nothing unless consumed"]
    pub fn tiles_in_distance(&self, distance: u32, grid: HexGrid) -> impl Iterator<Item = Self> {
        grid.cells_within_distance(self.to_cell(), distance)
            .map(Self::from_cell)
    }

    /// Checks if there is a river on the current tile.
    ///
    /// # Arguments
    ///
    /// - `tile_map`: A reference to the [`TileMap`] containing river information.
    ///
    /// # Returns
    ///
    /// - `bool`: Returns true if there is a river on the current tile, false otherwise.
    pub fn has_river(&self, tile_map: &TileMap) -> bool {
        let grid = tile_map.world_grid.grid;
        grid.edge_direction_array()
            .iter()
            .any(|&direction| self.has_river_in_direction(direction, tile_map))
    }

    /// Checks if there is a river on the current tile in the specified direction.
    ///
    /// # Arguments
    ///
    /// - `direction`: The direction to check for the river.
    /// - `tile_map`: A reference to the [`TileMap`] containing river information.
    ///
    /// # Returns
    ///
    /// - `bool`: Returns true if there is a river in the specified direction, false otherwise.
    pub fn has_river_in_direction(&self, direction: Direction, tile_map: &TileMap) -> bool {
        let grid = tile_map.world_grid.grid;
        // Get the edge index for the specified direction.
        let edge_index = grid.layout.orientation.edge_index(direction);

        // Determine the tile and edge direction to check based on the edge index.
        let (check_tile, check_edge_direction) = if edge_index < 3 {
            // If the edge index is less than 3, use the current tile and the given direction.
            (*self, direction)
        } else {
            // Otherwise, check the neighboring tile and the opposite direction.
            match self.neighbor_tile(direction, grid) {
                Some(neighbor_tile) => (neighbor_tile, direction.opposite()),
                None => return false,
            }
        };

        tile_map.river_list.iter().flatten().any(|river_edge| {
            river_edge.tile == check_tile // 1. Check whether there is a river in the current tile.
                    && check_edge_direction == river_edge.edge_direction(grid) // 2. Check whether the river edge in the direction of the current tile.
        })
    }

    /// Checks if the tile is water.
    ///
    /// When tile's terrain type is [`TerrainType::Water`], it is considered water.
    /// Otherwise, it is not water.
    pub fn is_water(&self, tile_map: &TileMap) -> bool {
        self.terrain_type(tile_map) == TerrainType::Water
    }

    /// Checks if the tile is impassable.
    pub fn is_impassable(&self, tile_map: &TileMap, ruleset: &Ruleset) -> bool {
        ruleset.terrain_types[self.terrain_type(tile_map).as_str()].impassable
            || self
                .feature(tile_map)
                .is_some_and(|feature| ruleset.features[feature.as_str()].impassable)
            || self.natural_wonder(tile_map).is_some_and(|natural_wonder| {
                ruleset.natural_wonders[natural_wonder.as_str()].impassable
            })
    }

    /// Check if the tile is freshwater
    ///
    /// Freshwater is not water and is adjacent to lake, oasis or has a river
    pub fn is_freshwater(&self, tile_map: &TileMap) -> bool {
        let grid = tile_map.world_grid.grid;
        self.terrain_type(tile_map) != TerrainType::Water
            && (self.neighbor_tiles(grid).any(|tile| {
                tile.base_terrain(tile_map) == BaseTerrain::Lake
                    || tile.feature(tile_map) == Some(Feature::Oasis)
            }) || self.has_river(tile_map))
    }

    /// Check if the tile is coastal land.
    ///
    /// A tile is considered `coastal land` if it is not `Water` and has at least one neighboring tile that is `Coast`.
    ///
    /// # Notice
    ///
    /// If the tile is not `Water` and has at least one neighboring tile that is `Lake`, but it has no neighboring tile that is `Coast`, it is not `coastal land`.
    pub fn is_coastal_land(&self, tile_map: &TileMap) -> bool {
        let grid = tile_map.world_grid.grid;
        self.terrain_type(tile_map) != TerrainType::Water
            && self
                .neighbor_tiles(grid)
                .any(|tile| tile.base_terrain(tile_map) == BaseTerrain::Coast)
    }

    /// Checks if a tile can be a starting tile of civilization.
    ///
    /// A tile is considered a starting tile if it is either `Flatland` or `Hill`, and then it must meet one of the following conditions:
    /// 1. The tile is a coastal land.
    /// 2. If `civ_require_coastal_land_start` is `false`, An inland tile (whose distance to `Coast` is greater than 2) can be a starting tile as well.
    ///
    /// **Why Inland Tiles with Distance 2 from Coast are Excluded**
    ///
    /// Because in the original game, the `Settler` unit can move 2 tiles per turn (ignoring terrain movement cost).
    /// If such a tile were considered a starting tile, a `Settler` can move to the coastal land and build a city in just one turn, which is functionally equivalent to choosing a coastal land tile as the starting tile of civilization directly.
    ///
    /// # Notice
    ///
    /// The tile with nature wonder can not be a starting tile of civilization.
    /// But we don't check the nature wonder in this function, because we generate the nature wonder after generating the civilization starting tile.
    /// That's like in original CIV5.
    /// City state starting tile is the same as well.
    /// In CIV6, we should check the nature wonder in this function.
    pub fn can_be_civilization_starting_tile(
        &self,
        tile_map: &TileMap,
        map_parameters: &MapParameters,
    ) -> bool {
        matches!(
            self.terrain_type(tile_map),
            TerrainType::Flatland | TerrainType::Hill
        ) && (self.is_coastal_land(tile_map)
            || (!map_parameters.civ_require_coastal_land_start
                && self
                    .tiles_in_distance(Self::SETTLER_MOVEMENT_RANGE, tile_map.world_grid.grid)
                    .all(|tile| tile.base_terrain(tile_map) != BaseTerrain::Coast)))
    }

    /// Checks if a tile can be a starting tile of city state.
    ///
    /// A tile is considered a starting tile, it must meet all of the following conditions:
    /// 1. It is either `Flatland` or `Hill`.
    /// 2. It is not `Snow`.
    ///
    /// # Arguments
    ///
    /// - `tile_map`: A reference to `TileMap`, which contains the tile data.
    /// - `region`: An optional reference to `Region`, which represents the region where the city state is located.\
    ///   If `None`, the function considers the tile as a candidate regardless of its region.
    ///   That usually happens when we place a city state in a unhabitated area.
    pub fn can_be_city_state_starting_tile(
        &self,
        tile_map: &TileMap,
        region: Option<&Region>,
    ) -> bool {
        // If the tile is already occupied, we can't place a city state there.
        if tile_map.starting_tile_and_civilization.get(self).is_some()
            || tile_map.starting_tile_and_city_state.get(self).is_some()
            || tile_map.layer_data[Layer::CityState][self.index()] != 0
        {
            return false;
        }

        // If the tile has a natural wonder, we can't place a city state there.
        if self.natural_wonder(tile_map).is_some() {
            return false;
        }

        // If the tile is on a snowy terrain, we can't place a city state there.
        if self.base_terrain(tile_map) == BaseTerrain::Snow {
            return false;
        }

        // Check the terrain type must be flatland or hill.
        let terrain = self.terrain_type(tile_map);
        if !matches!(terrain, TerrainType::Flatland | TerrainType::Hill) {
            return false;
        }

        // Check region constraints.
        if let Some(region) = region {
            if Some(self.area_id(tile_map)) != region.area_id {
                return false;
            }
        }

        true
    }
}