refactor: generate wild provicnes continue

This commit is contained in:
Azgaar 2022-09-08 01:38:26 +03:00
parent 859d20546a
commit 2c35122bb8
5 changed files with 201 additions and 111 deletions

View file

@ -143,11 +143,13 @@ export function createPack(grid: IGrid): IPack {
} }
}); });
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, { const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, {
i: cells.i, i: cells.i,
c: cells.c, c: cells.c,
h: heights, h: heights,
t: distanceField, t: distanceField,
f: featureIds,
culture: cultureIds,
state: stateIds, state: stateIds,
burg: burgIds burg: burgIds
}); });

View file

@ -1,6 +1,6 @@
import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
import FlatQueue from "flatqueue"; import FlatQueue from "flatqueue";
import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
import {gauss} from "utils/probabilityUtils"; import {gauss} from "utils/probabilityUtils";
const {WATER_COAST} = DISTANCE_FIELD; const {WATER_COAST} = DISTANCE_FIELD;

View file

@ -4,6 +4,7 @@ import {brighter, getMixedColor} from "utils/colorUtils";
import {gauss, P, rw} from "utils/probabilityUtils"; import {gauss, P, rw} from "utils/probabilityUtils";
import {isBurg, isState} from "utils/typeUtils"; import {isBurg, isState} from "utils/typeUtils";
import {provinceForms} from "./config"; import {provinceForms} from "./config";
import {generateProvinceName, generateProvinceEmblem} from "./utils";
const {COA, Names} = window; const {COA, Names} = window;
@ -26,9 +27,9 @@ export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures:
for (let i = 0; i < provincesNumber; i++) { for (let i = 0; i < provincesNumber; i++) {
const {i: burg, cell: center, culture: cultureId, coa: burgEmblem, name: burgName, type} = stateBurgs[i]; const {i: burg, cell: center, culture: cultureId, coa: burgEmblem, name: burgName, type} = stateBurgs[i];
const nameByBurg = P(0.5); const nameByBurg = P(0.5);
const name = nameByBurg ? burgName : generateName(cultureId, cultures); const name = generateName(nameByBurg, burgName, cultureId, cultures);
3;
const formName = rw(formsPool); const formName = rw(formsPool);
formsPool[formName] += 10; // increase chance to get the same form again formsPool[formName] += 10; // increase chance to get the same form again
@ -43,7 +44,9 @@ export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures:
return provinces; return provinces;
} }
function generateName(cultureId: number, cultures: TCultures) { function generateName(nameByBurg: boolean, burgName: string, cultureId: number, cultures: TCultures) {
if (nameByBurg) return burgName;
const base = cultures[cultureId].base; const base = cultures[cultureId].base;
return Names.getState(Names.getBaseShort(base), base); return Names.getState(Names.getBaseShort(base), base);
} }

View file

@ -8,7 +8,8 @@ export function generateProvinces(
states: TStates, states: TStates,
burgs: TBurgs, burgs: TBurgs,
cultures: TCultures, cultures: TCultures,
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "state" | "burg"> features: TPackFeatures,
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">
) { ) {
TIME && console.time("generateProvinces"); TIME && console.time("generateProvinces");
@ -18,7 +19,17 @@ export function generateProvinces(
const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage); const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
const provinceIds = expandProvinces(percentage, coreProvinces, cells); const provinceIds = expandProvinces(percentage, coreProvinces, cells);
const wildProvinces = generateWildProvinces(states, burgs, cultures, coreProvinces, provinceIds, cells); // mutates provinceIds
const wildProvinces = generateWildProvinces({
states,
burgs,
cultures,
features,
coreProvinces,
provinceIds,
percentage,
cells
}); // mutates provinceIds
const provinces = [...coreProvinces, ...wildProvinces]; const provinces = [...coreProvinces, ...wildProvinces];

View file

@ -1,126 +1,200 @@
import {group} from "d3-array"; import {group} from "d3-array";
import {rand} from "utils/probabilityUtils"; import FlatQueue from "flatqueue";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {unique} from "utils/arrayUtils";
import {brighter, getMixedColor} from "utils/colorUtils";
import {gauss, P, ra, rw} from "utils/probabilityUtils";
import {isBurg, isState} from "utils/typeUtils";
import {provinceForms} from "./config";
const {COA, Names} = window;
// add "wild" provinces if some cells don't have a province assigned // add "wild" provinces if some cells don't have a province assigned
export function generateWildProvinces( export function generateWildProvinces({
states: TStates, states,
burgs: TBurgs, burgs,
cultures: TCultures, cultures,
coreProvinces: IProvince[], features,
provinceIds: Uint16Array, coreProvinces,
cells: Pick<IPack["cells"], "i" | "state" | "burg"> provinceIds,
) { percentage,
const stateProvincesMap = group(coreProvinces, (province: IProvince) => province.state); cells
}: {
states: TStates;
burgs: TBurgs;
cultures: TCultures;
features: TPackFeatures;
coreProvinces: IProvince[];
provinceIds: Uint16Array;
percentage: number;
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">;
}) {
const noProvinceCells = Array.from(cells.i.filter(i => cells.state[i] && !provinceIds[i])); const noProvinceCells = Array.from(cells.i.filter(i => cells.state[i] && !provinceIds[i]));
const wildProvinces = [] as IProvince[]; const wildProvinces = [] as IProvince[];
const colonyNamesMap = createColonyNamesMap();
for (const {i: stateId, name: stateName} of states) { for (const state of states) {
const stateProvinces = stateProvincesMap.get(stateId); if (!isState(state)) continue;
if (!stateProvinces || !stateProvinces.length) continue;
const coreProvinceNames = stateProvinces.map(({name}) => name); let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i);
const colonyNamePool = [stateName, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
const getColonyName = () => {
if (colonyNamePool.length < 1) return null;
const index = rand(colonyNamePool.length - 1);
const spliced = colonyNamePool.splice(index, 1);
return spliced[0] ? `New ${spliced[0]}` : null;
};
let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === stateId);
while (noProvinceCellsInState.length) { while (noProvinceCellsInState.length) {
// add new province
const provinceId = coreProvinces.length + wildProvinces.length; const provinceId = coreProvinces.length + wildProvinces.length;
const burgCell = noProvinceCellsInState.find(i => cells.burg[i]); const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
const center = burgCell || noProvinceCellsInState[0]; const center = burgCell || noProvinceCellsInState[0];
const burg = burgCell ? cells.burg[burgCell] : 0; const cultureId = cells.culture[center];
const burgId = burgCell ? cells.burg[burgCell] : 0;
const burg = burgs[burgId];
const provinceCells = expandWildProvince(center, provinceId, state.i); // mutates provinceIds
const formName = getProvinceForm(center, provinceCells, state.center);
const name = getProvinceName(state.i, formName, burg, cultureId);
const fullName = name + " " + formName;
const coa = generateEmblem(formName, state, burg, cultureId);
const color = brighter(getMixedColor(state.color, 0.2), 0.3);
wildProvinces.push({i: provinceId, name, formName, center, burg: burgId, state: state.i, fullName, color, coa});
// re-check
noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i && !provinceIds[i]);
}
}
return wildProvinces;
function createColonyNamesMap() {
const stateProvincesMap = group(coreProvinces, (province: IProvince) => province.state);
const colonyNamesMap = new Map<number, string[]>(
states.map(state => {
const stateProvinces = stateProvincesMap.get(state.i) || [];
const coreProvinceNames = stateProvinces.map(province => province.name);
const colonyNamePool = unique([state.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)));
return [state.i, colonyNamePool];
})
);
return colonyNamesMap;
}
function getColonyName(stateId: number) {
const namesPool = colonyNamesMap.get(stateId) || [];
if (namesPool.length < 1) return null;
const name = ra(namesPool);
colonyNamesMap.set(
stateId,
namesPool.filter(n => n !== name)
);
return `New ${name}`;
}
function getProvinceName(stateId: number, formName: string, burg: TNoBurg | IBurg, cultureId: number) {
const colonyName = formName === "Colony" && P(0.8) && getColonyName(stateId);
if (colonyName) return colonyName;
if (burg?.name && P(0.5)) return burg.name;
const base = cultures[cultureId].base;
return Names.getState(Names.getBaseShort(base), base);
}
function expandWildProvince(center: number, provinceId: number, stateId: number) {
const maxExpansionCost = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
const provinceCells = [center];
provinceIds[center] = provinceId; provinceIds[center] = provinceId;
// expand province const queue = new FlatQueue<number>();
const costs = []; const cost: number[] = [];
costs[center] = 1; cost[center] = 1;
queue.push(center, 0); queue.push(center, 0);
while (queue.length) { while (queue.length) {
const priority = queue.peekValue(); const priority = queue.peekValue()!;
const next = queue.pop(); const next = queue.pop()!;
cells.c[next].forEach(neibCellId => { cells.c[next].forEach(neibCellId => {
if (cells.province[neibCellId]) return; if (provinceIds[neibCellId]) return;
const land = cells.h[neibCellId] >= 20; if (cells.state[neibCellId] !== stateId) return;
if (cells.state[neibCellId] && cells.state[neibCellId] !== s.i) return;
const cost = land ? (cells.state[neibCellId] === s.i ? 3 : 20) : cells.t[neibCellId] ? 10 : 30;
const totalCost = priority + cost;
if (totalCost > max) return; const isLand = cells.h[neibCellId] >= MIN_LAND_HEIGHT;
if (!costs[neibCellId] || totalCost < costs[neibCellId]) { const cellCost = isLand ? 3 : cells.t[neibCellId] === DISTANCE_FIELD.WATER_COAST ? 10 : 30;
if (land && cells.state[neibCellId] === s.i) cells.province[neibCellId] = provinceId; // assign province to a cell const totalCost = priority + cellCost;
costs[neibCellId] = totalCost; if (totalCost > maxExpansionCost) return;
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (isLand && cells.state[neibCellId] === stateId) {
// assign province to a cell
provinceCells.push(neibCellId);
provinceIds[neibCellId] = provinceId;
}
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost); queue.push(neibCellId, totalCost);
} }
}); });
} }
// generate "wild" province name return provinceCells;
const cultureId = cells.culture[center]; }
const f = pack.features[cells.f[center]];
const color = brighter(getMixedColor(s.color, 0.2), 0.3);
const provCells = noProvinceCellsInState.filter(i => cells.province[i] === provinceId); function getProvinceForm(center: number, provinceCells: number[], stateCenter: number) {
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); const feature = features[cells.f[center]];
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); if (feature === 0) throw new Error("Feature is not defined");
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const name = (function () { const provinceFeatures = unique(provinceCells.map(i => cells.f[i]));
const colonyName = colony && P(0.8) && getColonyName(); const isWholeIsle = provinceCells.length === feature.cells && provinceFeatures.length === 1;
if (colonyName) return colonyName; if (isWholeIsle) return "Island";
if (burgCell && P(0.5)) return burgs[burg].name;
const base = pack.cultures[cultureId].base;
return Names.getState(Names.getBaseShort(base), base); const isIsleGroup = provinceFeatures.every(featureId => (features[featureId] as TPackFeature)?.group === "isle");
})(); if (isIsleGroup) return "Islands";
const formName = (function () { const isColony = P(0.5) && !isConnected(stateCenter, center);
if (singleIsle) return "Island"; if (isColony) return "Colony";
if (isleGroup) return "Islands";
if (colony) return "Colony";
return rw(forms["Wild"]);
})();
const fullName = name + " " + formName; return rw(provinceForms["Wild"]);
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3); // check if two cells are connected by land withing same state
const kinship = dominion ? 0 : 0.4; function isConnected(from: number, to: number) {
const type = getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getPackShield(cultureId, s.i);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(provinceId);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from]; const queue = [from];
const checked: Dict<boolean> = {[from]: true};
const used = new Uint8Array(cells.i.length); const stateId = cells.state[from];
const state = cells.state[from];
while (queue.length) { while (queue.length) {
const current = queue.pop(); const current = queue.pop()!;
if (current === to) return true; // way is found if (current === to) return true;
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return; for (const neibId of cells.c[current]) {
queue.push(c); if (checked[neibId] || cells.state[neibId] !== stateId) continue;
used[c] = 1; queue.push(neibId);
}); checked[neibId] = true;
}
}
return false;
} }
return false; // way is not found
} }
// re-check function generateEmblem(formName: string, state: IState, burg: TNoBurg | IBurg, cultureId: number) {
noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === stateId && !provinceIds[i]); const dominion = P(getDominionChance(formName));
const kinship = dominion ? 0 : 0.4;
const coaType = isBurg(burg) ? burg.type : "Generic";
const coa = COA.generate(state.coa, kinship, dominion, coaType);
const cultureShield = cultures[cultureId].shield;
const stateShield = (state.coa as ICoa)?.shield;
coa.shield = COA.getShield(cultureShield, stateShield);
return coa;
} }
function getDominionChance(formName: string) {
if (formName === "Colony") return 0.95;
if (formName === "Island") return 0.7;
if (formName === "Islands") return 0.7;
return 0.3;
} }
} }