mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: expandProvinces
This commit is contained in:
parent
fe2f8428ad
commit
a1e7727730
5 changed files with 152 additions and 65 deletions
|
|
@ -6,6 +6,42 @@ import {minmax} from "utils/numberUtils";
|
||||||
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
||||||
import type {TStateData} from "./createStateData";
|
import type {TStateData} from "./createStateData";
|
||||||
|
|
||||||
|
const costs = {
|
||||||
|
SAME_CULTURE: -9,
|
||||||
|
DIFFERENT_CULTURES: 100,
|
||||||
|
|
||||||
|
MAX_SUITABILITY: 20,
|
||||||
|
UNINHABITED_LAND: 5000,
|
||||||
|
NATIVE_BIOME_FIXED: 10,
|
||||||
|
|
||||||
|
GENERIC_WATER_CROSSING: 1000,
|
||||||
|
NOMADS_WATER_CROSSING: 10000,
|
||||||
|
NAVAL_WATER_CROSSING: 300,
|
||||||
|
LAKE_STATES_LAKE_CROSSING: 10,
|
||||||
|
GENERIC_MOUNTAINS_CROSSING: 2200,
|
||||||
|
GENERIC_HILLS_CROSSING: 300,
|
||||||
|
HIGHLAND_STATE_LOWLANDS: 1100,
|
||||||
|
HIGHLAND_STATE_HIGHTLAND: 0,
|
||||||
|
|
||||||
|
RIVER_STATE_RIVER_CROSSING: 0,
|
||||||
|
RIVER_STATE_NO_RIVER: 100,
|
||||||
|
RIVER_CROSSING_MIN: 20,
|
||||||
|
RIVER_CROSSING_MAX: 100,
|
||||||
|
|
||||||
|
GENERIC_LAND_COAST: 20,
|
||||||
|
MARITIME_LAND_COAST: 0,
|
||||||
|
NOMADS_LAND_COAST: 60,
|
||||||
|
GENERIC_LANDLOCKED: 0,
|
||||||
|
NAVAL_LANDLOCKED: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
const multipliers = {
|
||||||
|
HUNTERS_NON_NATIVE_BIOME: 2,
|
||||||
|
NOMADS_FOREST_BIOMES: 3,
|
||||||
|
GENERIC_NON_NATIVE_BIOME: 1,
|
||||||
|
GENERIC_DEEP_WATER: 2
|
||||||
|
};
|
||||||
|
|
||||||
// growth algorithm to assign cells to states
|
// growth algorithm to assign cells to states
|
||||||
export function expandStates(
|
export function expandStates(
|
||||||
capitalCells: Map<number, boolean>,
|
capitalCells: Map<number, boolean>,
|
||||||
|
|
@ -30,39 +66,6 @@ export function expandStates(
|
||||||
queue.push({cellId, stateId}, 0);
|
queue.push({cellId, stateId}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// expansion costs (less is better)
|
|
||||||
const SAME_CULTURE_BONUS = -9;
|
|
||||||
const DIFFERENT_CULTURES_FEE = 100;
|
|
||||||
|
|
||||||
const MAX_SUITABILITY_COST = 20;
|
|
||||||
const UNINHABITED_LAND_FEE = 5000;
|
|
||||||
|
|
||||||
const NATIVE_BIOME_FIXED_COST = 10;
|
|
||||||
const HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER = 2;
|
|
||||||
const NOMADS_FOREST_BIOMES_FEE_MULTIPLIER = 3;
|
|
||||||
const GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER = 1;
|
|
||||||
|
|
||||||
const GENERIC_DEEP_WATER_FEE_MULTIPLIER = 2;
|
|
||||||
const GENERIC_WATER_CROSSING_FEE = 1000;
|
|
||||||
const NOMADS_WATER_CROSSING_FEE = 10000;
|
|
||||||
const NAVAL_WATER_CROSSING_FEE = 300;
|
|
||||||
const LAKE_STATES_LAKE_CROSSING_FEE = 10;
|
|
||||||
const GENERIC_MOUNTAINS_CROSSING_FEE = 2200;
|
|
||||||
const GENERIC_HILLS_CROSSING_FEE = 300;
|
|
||||||
const HIGHLAND_STATE_LOWLANDS_FEE = 1100;
|
|
||||||
const HIGHLAND_STATE_HIGHTLAND_COST = 0;
|
|
||||||
|
|
||||||
const RIVER_STATE_RIVER_CROSSING_COST = 0;
|
|
||||||
const RIVER_STATE_NO_RIVER_COST = 100;
|
|
||||||
const RIVER_CROSSING_MIN_COST = 20;
|
|
||||||
const RIVER_CROSSING_MAX_COST = 100;
|
|
||||||
|
|
||||||
const GENERIC_LAND_COAST_FEE = 20;
|
|
||||||
const MARITIME_LAND_COAST_FEE = 0;
|
|
||||||
const NOMADS_LAND_COAST_FEE = 60;
|
|
||||||
const GENERIC_LANDLOCKED_FEE = 0;
|
|
||||||
const NAVAL_LANDLOCKED_FEE = 30;
|
|
||||||
|
|
||||||
const statesMap = new Map<number, TStateData>(statesData.map(stateData => [stateData.i, stateData]));
|
const statesMap = new Map<number, TStateData>(statesData.map(stateData => [stateData.i, stateData]));
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
|
|
@ -100,7 +103,7 @@ export function expandStates(
|
||||||
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
||||||
|
|
||||||
function getCultureCost(cellId: number, stateCulture: number) {
|
function getCultureCost(cellId: number, stateCulture: number) {
|
||||||
return cells.culture[cellId] === stateCulture ? SAME_CULTURE_BONUS : DIFFERENT_CULTURES_FEE;
|
return cells.culture[cellId] === stateCulture ? costs.SAME_CULTURE : costs.DIFFERENT_CULTURES;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPopulationCost(cellId: number) {
|
function getPopulationCost(cellId: number) {
|
||||||
|
|
@ -108,19 +111,19 @@ export function expandStates(
|
||||||
if (isWater) return 0;
|
if (isWater) return 0;
|
||||||
|
|
||||||
const suitability = cells.s[cellId];
|
const suitability = cells.s[cellId];
|
||||||
if (suitability) return Math.max(MAX_SUITABILITY_COST - suitability, 0);
|
if (suitability) return Math.max(costs.MAX_SUITABILITY - suitability, 0);
|
||||||
|
|
||||||
return UNINHABITED_LAND_FEE;
|
return costs.UNINHABITED_LAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBiomeCost(cellId: number, capitalBiome: number, type: TCultureType) {
|
function getBiomeCost(cellId: number, capitalBiome: number, type: TCultureType) {
|
||||||
const biome = cells.biome[cellId];
|
const biome = cells.biome[cellId];
|
||||||
if (biome === capitalBiome) return NATIVE_BIOME_FIXED_COST;
|
if (biome === capitalBiome) return costs.NATIVE_BIOME_FIXED;
|
||||||
|
|
||||||
const defaultCost = biomesData.cost[biome];
|
const defaultCost = biomesData.cost[biome];
|
||||||
if (type === "Hunting") return defaultCost * HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER;
|
if (type === "Hunting") return defaultCost * multipliers.HUNTERS_NON_NATIVE_BIOME;
|
||||||
if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * NOMADS_FOREST_BIOMES_FEE_MULTIPLIER;
|
if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * multipliers.NOMADS_FOREST_BIOMES;
|
||||||
return defaultCost * GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER;
|
return defaultCost * multipliers.GENERIC_NON_NATIVE_BIOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeightCost(cellId: number, type: TCultureType) {
|
function getHeightCost(cellId: number, type: TCultureType) {
|
||||||
|
|
@ -131,12 +134,12 @@ export function expandStates(
|
||||||
const feature = features[cells.f[cellId]];
|
const feature = features[cells.f[cellId]];
|
||||||
if (feature === 0) throw new Error(`No feature for cell ${cellId}`);
|
if (feature === 0) throw new Error(`No feature for cell ${cellId}`);
|
||||||
const isDeepWater = cells.t[cellId] > DISTANCE_FIELD.WATER_COAST;
|
const isDeepWater = cells.t[cellId] > DISTANCE_FIELD.WATER_COAST;
|
||||||
const multiplier = isDeepWater ? GENERIC_DEEP_WATER_FEE_MULTIPLIER : 1;
|
const multiplier = isDeepWater ? multipliers.GENERIC_DEEP_WATER : 1;
|
||||||
|
|
||||||
if (type === "Lake" && feature.type === "lake") return LAKE_STATES_LAKE_CROSSING_FEE * multiplier;
|
if (type === "Lake" && feature.type === "lake") return costs.LAKE_STATES_LAKE_CROSSING * multiplier;
|
||||||
if (type === "Naval") return NAVAL_WATER_CROSSING_FEE * multiplier;
|
if (type === "Naval") return costs.NAVAL_WATER_CROSSING * multiplier;
|
||||||
if (type === "Nomadic") return NOMADS_WATER_CROSSING_FEE * multiplier;
|
if (type === "Nomadic") return costs.NOMADS_WATER_CROSSING * multiplier;
|
||||||
return GENERIC_WATER_CROSSING_FEE * multiplier;
|
return costs.GENERIC_WATER_CROSSING * multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLowlands = height <= ELEVATION.FOOTHILLS;
|
const isLowlands = height <= ELEVATION.FOOTHILLS;
|
||||||
|
|
@ -144,22 +147,22 @@ export function expandStates(
|
||||||
const isMountains = height >= ELEVATION.MOUNTAINS;
|
const isMountains = height >= ELEVATION.MOUNTAINS;
|
||||||
|
|
||||||
if (type === "Highland") {
|
if (type === "Highland") {
|
||||||
if (isLowlands) return HIGHLAND_STATE_LOWLANDS_FEE;
|
if (isLowlands) return costs.HIGHLAND_STATE_LOWLANDS;
|
||||||
return HIGHLAND_STATE_HIGHTLAND_COST;
|
return costs.HIGHLAND_STATE_HIGHTLAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMountains) return GENERIC_MOUNTAINS_CROSSING_FEE;
|
if (isMountains) return costs.GENERIC_MOUNTAINS_CROSSING;
|
||||||
if (isHills) return GENERIC_HILLS_CROSSING_FEE;
|
if (isHills) return costs.GENERIC_HILLS_CROSSING;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRiverCost(cellId: number, type: TCultureType) {
|
function getRiverCost(cellId: number, type: TCultureType) {
|
||||||
const isRiver = cells.r[cellId] !== 0;
|
const isRiver = cells.r[cellId] !== 0;
|
||||||
if (type === "River") return isRiver ? RIVER_STATE_RIVER_CROSSING_COST : RIVER_STATE_NO_RIVER_COST;
|
if (type === "River") return isRiver ? costs.RIVER_STATE_RIVER_CROSSING : costs.RIVER_STATE_NO_RIVER;
|
||||||
if (!isRiver) return 0;
|
if (!isRiver) return 0;
|
||||||
|
|
||||||
const flux = cells.fl[cellId];
|
const flux = cells.fl[cellId];
|
||||||
return minmax(flux / 10, RIVER_CROSSING_MIN_COST, RIVER_CROSSING_MAX_COST);
|
return minmax(flux / 10, costs.RIVER_CROSSING_MIN, costs.RIVER_CROSSING_MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeCost(cellId: number, type: TCultureType) {
|
function getTypeCost(cellId: number, type: TCultureType) {
|
||||||
|
|
@ -168,15 +171,15 @@ export function expandStates(
|
||||||
|
|
||||||
const isLandCoast = t === DISTANCE_FIELD.LAND_COAST;
|
const isLandCoast = t === DISTANCE_FIELD.LAND_COAST;
|
||||||
if (isLandCoast) {
|
if (isLandCoast) {
|
||||||
if (isMaritime) return MARITIME_LAND_COAST_FEE;
|
if (isMaritime) return costs.MARITIME_LAND_COAST;
|
||||||
if (type === "Nomadic") return NOMADS_LAND_COAST_FEE;
|
if (type === "Nomadic") return costs.NOMADS_LAND_COAST;
|
||||||
return GENERIC_LAND_COAST_FEE;
|
return costs.GENERIC_LAND_COAST;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED;
|
const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED;
|
||||||
if (isLandlocked) {
|
if (isLandlocked) {
|
||||||
if (type === "Naval") return NAVAL_LANDLOCKED_FEE;
|
if (type === "Naval") return costs.NAVAL_LANDLOCKED;
|
||||||
return GENERIC_LANDLOCKED_FEE;
|
return costs.GENERIC_LANDLOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -193,7 +196,7 @@ function normalizeStates(
|
||||||
|
|
||||||
const normalizedStateIds = Uint16Array.from(stateIds);
|
const normalizedStateIds = Uint16Array.from(stateIds);
|
||||||
|
|
||||||
for (let cellId = 0; cellId > heights.length; cellId++) {
|
for (let cellId = 0; cellId < heights.length; cellId++) {
|
||||||
if (heights[cellId] < MIN_LAND_HEIGHT) continue;
|
if (heights[cellId] < MIN_LAND_HEIGHT) continue;
|
||||||
|
|
||||||
const neibs = neibCells[cellId].filter(neib => heights[neib] >= MIN_LAND_HEIGHT);
|
const neibs = neibCells[cellId].filter(neib => heights[neib] >= MIN_LAND_HEIGHT);
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,14 @@ export function createPack(grid: IGrid): IPack {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, {i: cells.i});
|
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, {
|
||||||
|
i: cells.i,
|
||||||
|
c: cells.c,
|
||||||
|
h: heights,
|
||||||
|
t: distanceField,
|
||||||
|
state: stateIds,
|
||||||
|
burg: burgIds
|
||||||
|
});
|
||||||
|
|
||||||
// BurgsAndStates.generateProvinces();
|
// BurgsAndStates.generateProvinces();
|
||||||
// BurgsAndStates.defineBurgFeatures();
|
// BurgsAndStates.defineBurgFeatures();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,86 @@
|
||||||
|
import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
|
import FlatQueue from "flatqueue";
|
||||||
|
|
||||||
import {gauss} from "utils/probabilityUtils";
|
import {gauss} from "utils/probabilityUtils";
|
||||||
|
|
||||||
export function expandProvinces(percentage: number, cells: Pick<IPack["cells"], "i">) {
|
const {WATER_COAST} = DISTANCE_FIELD;
|
||||||
|
const {MOUNTAINS, HILLS, LOWLANDS} = ELEVATION;
|
||||||
|
|
||||||
|
export function expandProvinces(
|
||||||
|
percentage: number,
|
||||||
|
provinces: IProvince[],
|
||||||
|
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "state" | "burg">
|
||||||
|
) {
|
||||||
const provinceIds = new Uint16Array(cells.i.length);
|
const provinceIds = new Uint16Array(cells.i.length);
|
||||||
|
|
||||||
const maxGrowth = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
|
const queue = new FlatQueue<{cellId: number; provinceId: number; stateId: number}>();
|
||||||
|
const cost: number[] = [];
|
||||||
|
|
||||||
return provinceIds;
|
const maxExpansionCost = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
|
||||||
|
|
||||||
|
for (const {i: provinceId, center: cellId, state: stateId} of provinces) {
|
||||||
|
provinceIds[cellId] = provinceId;
|
||||||
|
cost[cellId] = 1;
|
||||||
|
queue.push({cellId, provinceId, stateId}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const priority = queue.peekValue()!;
|
||||||
|
const {cellId, provinceId, stateId} = queue.pop()!;
|
||||||
|
|
||||||
|
cells.c[cellId].forEach(neibCellId => {
|
||||||
|
const isLand = cells.h[neibCellId] >= MIN_LAND_HEIGHT;
|
||||||
|
if (isLand && cells.state[neibCellId] !== stateId) return; // can expand only within state
|
||||||
|
|
||||||
|
const evevationCost = getElevationCost(cells.h[neibCellId], cells.t[neibCellId]);
|
||||||
|
const totalCost = priority + evevationCost;
|
||||||
|
if (totalCost > maxExpansionCost) return;
|
||||||
|
|
||||||
|
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
|
||||||
|
if (isLand) provinceIds[neibCellId] = provinceId; // assign province to cell
|
||||||
|
cost[neibCellId] = totalCost;
|
||||||
|
|
||||||
|
queue.push({cellId: neibCellId, provinceId, stateId}, totalCost);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeProvinces(provinceIds, cells.c, cells.state, cells.burg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElevationCost(elevation: number, distance: number) {
|
||||||
|
if (elevation >= MOUNTAINS) return 100;
|
||||||
|
if (elevation >= HILLS) return 30;
|
||||||
|
if (elevation >= LOWLANDS) return 10;
|
||||||
|
if (elevation >= MIN_LAND_HEIGHT) return 5;
|
||||||
|
if (distance === WATER_COAST) return 100;
|
||||||
|
|
||||||
|
return 300; // deep water
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProvinces(
|
||||||
|
provinceIds: Uint16Array,
|
||||||
|
neibCells: number[][],
|
||||||
|
stateIds: Uint16Array,
|
||||||
|
burgIds: Uint16Array
|
||||||
|
) {
|
||||||
|
const normalizedIds = Uint16Array.from(provinceIds);
|
||||||
|
|
||||||
|
for (let cellId = 0; cellId < neibCells.length; cellId++) {
|
||||||
|
if (!stateIds[cellId]) continue; // skip water or neutral cells
|
||||||
|
if (burgIds[cellId]) continue; // do not overwrite burgs
|
||||||
|
|
||||||
|
const neibs = neibCells[cellId].filter(neib => stateIds[neib] >= stateIds[cellId]);
|
||||||
|
|
||||||
|
const adversaries = neibs.filter(neib => normalizedIds[neib] !== normalizedIds[cellId]);
|
||||||
|
if (adversaries.length < 2) continue;
|
||||||
|
|
||||||
|
const buddies = neibs.filter(neib => normalizedIds[neib] === normalizedIds[cellId]);
|
||||||
|
if (buddies.length > 2) continue;
|
||||||
|
|
||||||
|
// change cells's province
|
||||||
|
if (adversaries.length > buddies.length) normalizedIds[cellId] = normalizedIds[adversaries[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedIds;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export function generateProvinces(
|
||||||
states: TStates,
|
states: TStates,
|
||||||
burgs: TBurgs,
|
burgs: TBurgs,
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
cells: Pick<IPack["cells"], "i">
|
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "state" | "burg">
|
||||||
) {
|
) {
|
||||||
TIME && console.time("generateProvinces");
|
TIME && console.time("generateProvinces");
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function generateProvinces(
|
||||||
return {provinceIds: new Uint16Array(cells.i.length), provinces: [] as TProvinces[]};
|
return {provinceIds: new Uint16Array(cells.i.length), provinces: [] as TProvinces[]};
|
||||||
|
|
||||||
const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
|
const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
|
||||||
const provinceIds = expandProvinces(percentage, cells);
|
const provinceIds = expandProvinces(percentage, coreProvinces, cells);
|
||||||
|
|
||||||
const provinces = [...coreProvinces];
|
const provinces = [...coreProvinces];
|
||||||
|
|
||||||
|
|
|
||||||
4
src/types/pack/pack.d.ts
vendored
4
src/types/pack/pack.d.ts
vendored
|
|
@ -27,8 +27,8 @@ interface IPackCells {
|
||||||
state: Uint16Array;
|
state: Uint16Array;
|
||||||
culture: Uint16Array;
|
culture: Uint16Array;
|
||||||
religion: Uint16Array;
|
religion: Uint16Array;
|
||||||
province: UintArray;
|
province: Uint16Array;
|
||||||
burg: UintArray;
|
burg: Uint16Array;
|
||||||
haven: UintArray;
|
haven: UintArray;
|
||||||
harbor: UintArray;
|
harbor: UintArray;
|
||||||
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
|
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue