refactor: generate relations

This commit is contained in:
Azgaar 2022-09-04 16:38:46 +03:00
parent 907e916b45
commit 73431ec743
10 changed files with 204 additions and 14 deletions

View file

@ -673,7 +673,7 @@ function getFileName(dataType) {
return name + " " + type + dateString;
}
function downloadFile(data, name, type = "text/plain") {
export function downloadFile(data, name, type = "text/plain") {
const dataBlob = new Blob([data], {type});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");

View file

@ -7,7 +7,7 @@ import {initLayers, renderLayer, restoreLayers} from "layers";
// @ts-expect-error js module
import {drawScaleBar, Rulers} from "modules/measurers";
// @ts-expect-error js module
import {unfog} from "modules/ui/editors";
import {unfog, downloadFile} from "modules/ui/editors";
// @ts-expect-error js module
import {applyMapSize, randomizeOptions} from "modules/ui/options";
// @ts-expect-error js module
@ -26,7 +26,6 @@ import {createGrid} from "./grid/grid";
import {createPack} from "./pack/pack";
import {getInputValue, setInputValue} from "utils/nodeUtils";
import {calculateMapCoordinates} from "modules/coordinates";
import {drawPoint} from "utils/debugUtils";
const {Zoom, ThreeD} = window;
@ -77,6 +76,8 @@ async function generate(options?: IGenerationOptions) {
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
// });
downloadDiplomacyData();
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
// showStatistics();
INFO && console.groupEnd();
@ -85,6 +86,19 @@ async function generate(options?: IGenerationOptions) {
}
}
function downloadDiplomacyData() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
states.forEach(s => {
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
data += s.name + "," + rels.join(",") + "\n";
});
// downloadFile(data, "relations.csv");
}
function showGenerationError(error: Error) {
clearMainTip();
ERROR && console.error(error);

View file

@ -126,7 +126,7 @@ export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Arr
return precipitation;
}
// TODO: move to renderer
// TODO: move to renderers
function drawWindDirection() {
const wind = prec.append("g").attr("id", "wind");

View file

@ -106,3 +106,10 @@ export const adjectivalForms = [
"Horde",
"Marches"
];
export const relations = {
neighbors: {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9},
neighborsOfNeighbors: {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1},
farStates: {Friendly: 1, Neutral: 12, Suspicion: 2},
navalToNaval: {Neutral: 2, Suspicion: 2, Rival: 1}
};

View file

@ -0,0 +1,26 @@
import * as d3 from "d3";
import {getMixedColor} from "utils/colorUtils";
import {ra} from "utils/probabilityUtils";
import type {TStateStatistics} from "./collectStatistics";
export function defineStateColors(statistics: TStateStatistics) {
const scheme: Hex[] = d3.shuffle(["#e78ac3", "#a6d854", "#ffd92f", "#66c2a5", "#fc8d62", "#8da0cb"]);
const colors: Record<number, Hex> = {};
// assign colors using greedy algorithm
for (const i in statistics) {
const {neighbors} = statistics[i];
const schemeColor = scheme.find(schemeColor => neighbors.every(neighbor => colors[neighbor] !== schemeColor));
colors[i] = schemeColor || ra(scheme);
scheme.push(scheme.shift()!);
}
// make each color unique
for (const i in colors) {
const isColorReused = Object.values(colors).some(color => color === colors[i]);
if (isColorReused) colors[i] = getMixedColor(colors[i], 0.3);
}
return colors;
}

View file

@ -1,10 +1,10 @@
import {NAMEBASE} from "config/namebases";
import * as d3 from "d3";
import {NAMEBASE} from "config/namebases";
import {getInputNumber} from "utils/nodeUtils";
import {P, rand, rw} from "utils/probabilityUtils";
import type {TStateStatistics} from "./collectStatistics";
import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config";
import type {TStateStatistics} from "./collectStatistics";
// create 5 area tiers, 4 is the biggest, 0 the smallest
export function createAreaTiers(statistics: TStateStatistics) {
@ -59,7 +59,7 @@ function defineFormName(
if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier);
if (form === "Anarchy") return rw(StateForms.anarchy);
return "test";
throw new Error("Unknown state form: " + form);
}
// Default name depends on area tier, some name bases have special names for tiers

View file

@ -7,6 +7,7 @@ import {createCapitals} from "./createCapitals";
import {createStateData} from "./createStateData";
import {createTowns} from "./createTowns";
import {expandStates} from "./expandStates";
import {generateDiplomacy} from "./generateDiplomacy";
import {specifyBurgs} from "./specifyBurgs";
import {specifyStates} from "./specifyStates";
@ -86,7 +87,8 @@ export function generateBurgsAndStates(
const burgIds = assignBurgIds(burgs);
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
const states = specifyStates(statesData, statistics, cultures, burgs);
const {chronicle, diplomacy} = generateDiplomacy(statesData, statistics, pick(cells, "f"));
const states = specifyStates(statesData, statistics, diplomacy, cultures, burgs);
return {burgIds, stateIds, burgs, states};

View file

@ -0,0 +1,126 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {TStateStatistics} from "./collectStatistics";
import {TStateData} from "./createStateData";
import {P, rw} from "utils/probabilityUtils";
import {relations} from "./config";
export type TDiplomacy = {[key: number]: TRelations[]};
interface IDiplomacyData {
i: number;
type: TCultureType;
center: number;
area: number;
neighbors: number[];
}
export function generateDiplomacy(
statesData: TStateData[],
statistics: TStateStatistics,
cells: Pick<IPack["cells"], "f">
) {
TIME && console.time("generateDiplomacy");
const chronicle: string[] = [];
const diplomacy = getBlankDiplomacyMatrix(statesData);
if (statesData.length < 2) {
return {chronicle, diplomacy};
}
const stateAreas = Object.values(statistics).map(({area}) => area);
const averageStateArea = d3.mean(stateAreas)!;
for (let i = 0; i < statesData.length; i++) {
const from = getDiplomacyData(statesData[i], statistics);
if (diplomacy[from.i].includes("Vassal")) {
// Vassal copy relations from its Suzerain
const suzerain = diplomacy[from.i].indexOf("Vassal");
for (const to of statesData) {
if (from.i === to.i || to.i === suzerain) continue;
diplomacy[from.i][to.i] = diplomacy[suzerain][to.i];
// vassals are Ally to each other
if (diplomacy[suzerain][to.i] === "Suzerain") diplomacy[from.i][to.i] = "Ally";
for (let e = 0; e < statesData.length; e++) {
const nested = statesData[e];
if (nested.i === from.i || nested.i === suzerain) continue;
if (diplomacy[nested.i][suzerain] === "Suzerain" || diplomacy[nested.i][suzerain] === "Vassal") continue;
diplomacy[nested.i][from.i] = diplomacy[nested.i][suzerain];
}
}
continue;
}
for (let j = i + 1; j < statesData.length; j++) {
const to = getDiplomacyData(statesData[j], statistics);
if (diplomacy[to.i].includes("Vassal")) {
// relations to vassal is the same as to its Suzerain
const suzerain = diplomacy[to.i].indexOf("Vassal");
diplomacy[from.i][to.i] = diplomacy[from.i][suzerain];
continue;
}
const isVassal = detectVassalState(from, to, averageStateArea);
if (isVassal) {
diplomacy[from.i][to.i] = "Suzerain";
diplomacy[to.i][from.i] = "Vassal";
continue;
}
const relations = defineRelations(from, to, cells.f);
diplomacy[from.i][to.i] = relations;
diplomacy[to.i][from.i] = relations;
}
}
TIME && console.timeEnd("generateDiplomacy");
return {chronicle, diplomacy};
}
function getBlankDiplomacyMatrix(statesData: TStateData[]) {
const length = statesData.length + 1;
return statesData.reduce((acc, {i}) => {
acc[i] = new Array(length).fill("x");
return acc;
}, {} as TDiplomacy);
}
function getDiplomacyData(stateData: TStateData, statistics: TStateStatistics): IDiplomacyData {
const {i, type, center} = stateData;
const {neighbors, area} = statistics[i];
return {i, type, center, neighbors, area};
}
function detectVassalState(from: IDiplomacyData, to: IDiplomacyData, averageStateArea: number) {
if (P(0.2)) return false;
const isNeighbor = from.neighbors.includes(to.i);
if (!isNeighbor) return false;
const isMuchSmaller = from.area * 2 < to.area && from.area < averageStateArea && to.area > averageStateArea;
if (isMuchSmaller) return true;
return false;
}
function defineRelations(from: IDiplomacyData, to: IDiplomacyData, featureIds: Uint16Array) {
const isNeighbor = from.neighbors.includes(to.i);
if (isNeighbor) return rw(relations.neighbors); // relations between neighboring states
const isNeighborOfNeighbor = from.neighbors.some(neighbor => to.neighbors.includes(neighbor));
if (isNeighborOfNeighbor) return rw(relations.neighborsOfNeighbors); // relations between neighbors of neighbors
const isNaval = from.type === "Naval" && to.type === "Naval" && featureIds[from.center] !== featureIds[to.center];
if (isNaval) return rw(relations.navalToNaval); // relations between naval states on different islands
return rw(relations.farStates); // relations between far states
}

View file

@ -1,28 +1,31 @@
import {TIME} from "config/logging";
import {getColors} from "utils/colorUtils";
import {NEUTRALS} from "./config";
import {createAreaTiers, defineStateForm} from "./defineStateForm";
import {defineFullStateName, defineStateName} from "./defineStateName";
import {defineStateColors} from "./defineStateColors";
import {isBurg} from "utils/typeUtils";
import type {TStateStatistics} from "./collectStatistics";
import type {TStateData} from "./createStateData";
import type {TDiplomacy} from "./generateDiplomacy";
export function specifyStates(
statesData: TStateData[],
statistics: TStateStatistics,
diplomacy: TDiplomacy,
cultures: TCultures,
burgs: TBurgs
): TStates {
TIME && console.time("specifyStates");
const colors = getColors(statesData.length);
const colors = defineStateColors(statistics);
const getAreaTier = createAreaTiers(statistics);
const getNameBase = (cultureId: number) => cultures[cultureId].base;
const states: IState[] = statesData.map((stateData, index) => {
const states: IState[] = statesData.map(stateData => {
const {i, center, type, culture, capital} = stateData;
const {area, burgs: burgsNumber, ...stats} = statistics[i];
const color = colors[i];
const capitalBurg = burgs[capital];
const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null;
@ -34,9 +37,18 @@ export function specifyStates(
const name = defineStateName(center, capitalName, nameBase, formName);
const fullName = defineFullStateName(name, formName);
const color = colors[index];
const state: IState = {name, ...stateData, form, formName, fullName, color, area, burgs: burgsNumber, ...stats};
const state: IState = {
name,
...stateData,
form,
formName,
fullName,
color,
area,
burgs: burgsNumber,
...stats,
diplomacy: diplomacy[i]
};
return state;
});

View file

@ -18,6 +18,7 @@ interface IState {
rural: number;
urban: number;
neighbors: number[];
diplomacy: TRelations[];
removed?: boolean;
}
@ -36,3 +37,5 @@ interface ICoa {
shield: string;
t1: string;
}
type TRelations = "Ally" | "Friendly" | "Neutral" | "Suspicion" | "Rival" | "Unknown" | "Suzerain" | "Vassal" | "x";