From 73431ec74353a26904f4a180534a3dba0d75668f Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 4 Sep 2022 16:38:46 +0300 Subject: [PATCH 1/4] refactor: generate relations --- src/modules/ui/editors.js | 2 +- src/scripts/generation/generation.ts | 18 ++- src/scripts/generation/grid/precipitation.ts | 2 +- .../generation/pack/burgsAndStates/config.ts | 7 + .../pack/burgsAndStates/defineStateColors.ts | 26 ++++ .../pack/burgsAndStates/defineStateForm.ts | 6 +- .../burgsAndStates/generateBurgsAndStates.ts | 4 +- .../pack/burgsAndStates/generateDiplomacy.ts | 126 ++++++++++++++++++ .../pack/burgsAndStates/specifyStates.ts | 24 +++- src/types/pack/states.d.ts | 3 + 10 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 src/scripts/generation/pack/burgsAndStates/defineStateColors.ts create mode 100644 src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts diff --git a/src/modules/ui/editors.js b/src/modules/ui/editors.js index 7435974a..5c40ddb9 100644 --- a/src/modules/ui/editors.js +++ b/src/modules/ui/editors.js @@ -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"); diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 9091e41b..a1402328 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -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); diff --git a/src/scripts/generation/grid/precipitation.ts b/src/scripts/generation/grid/precipitation.ts index 4b9bfda8..b13ac894 100644 --- a/src/scripts/generation/grid/precipitation.ts +++ b/src/scripts/generation/grid/precipitation.ts @@ -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"); diff --git a/src/scripts/generation/pack/burgsAndStates/config.ts b/src/scripts/generation/pack/burgsAndStates/config.ts index 8461ab76..daf09ca0 100644 --- a/src/scripts/generation/pack/burgsAndStates/config.ts +++ b/src/scripts/generation/pack/burgsAndStates/config.ts @@ -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} +}; diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts b/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts new file mode 100644 index 00000000..0216234b --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts @@ -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 = {}; + + // 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; +} diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts index e6847c61..2668ef59 100644 --- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -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 diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index fc370ff3..43be3e75 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -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}; diff --git a/src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts b/src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts new file mode 100644 index 00000000..18f51dbf --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts @@ -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 +) { + 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 +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index 3bc1fbbc..fcd921b4 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -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; }); diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index beeb7dd0..73e06792 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -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"; From 136e71eae9493f70cc6e22d9622a09d48261ae44 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 4 Sep 2022 22:28:50 +0300 Subject: [PATCH 2/4] refactor: generate relations --- src/scripts/generation/generation.ts | 17 +-------- .../pack/burgsAndStates/defineStateForm.ts | 22 ++++++++---- .../burgsAndStates/generateBurgsAndStates.ts | 27 +++----------- ...erateDiplomacy.ts => generateRelations.ts} | 26 +++++++------- .../pack/burgsAndStates/simulateWars.ts | 35 +++++++++++++++++++ .../pack/burgsAndStates/specifyStates.ts | 10 +++--- src/types/pack/states.d.ts | 14 ++++++-- 7 files changed, 85 insertions(+), 66 deletions(-) rename src/scripts/generation/pack/burgsAndStates/{generateDiplomacy.ts => generateRelations.ts} (88%) create mode 100644 src/scripts/generation/pack/burgsAndStates/simulateWars.ts diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index a1402328..5d90ff9a 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -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, downloadFile} from "modules/ui/editors"; +import {unfog} from "modules/ui/editors"; // @ts-expect-error js module import {applyMapSize, randomizeOptions} from "modules/ui/options"; // @ts-expect-error js module @@ -76,8 +76,6 @@ 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(); @@ -86,19 +84,6 @@ 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); diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts index 2668ef59..67f27d9e 100644 --- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -23,9 +23,16 @@ export function createAreaTiers(statistics: TStateStatistics) { }; } -export function defineStateForm(type: TCultureType, areaTier: AreaTiers, nameBase: number, burgsNumber: number) { +export function defineStateForm( + type: TCultureType, + areaTier: AreaTiers, + nameBase: number, + burgsNumber: number, + relations: TRelation[], + neighbors: number[] +) { const form = defineForm(type, areaTier); - const formName = defineFormName(form, nameBase, areaTier, burgsNumber); + const formName = defineFormName(form, nameBase, areaTier, burgsNumber, relations, neighbors); return {form, formName}; } @@ -51,9 +58,11 @@ function defineFormName( form: ReturnType, nameBase: number, areaTier: AreaTiers, - burgsNumber: number + burgsNumber: number, + relations: TRelation[], + neighbors: number[] ) { - if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier); + if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, relations, neighbors); if (form === "Republic") return defineRepublicForm(areaTier, burgsNumber); if (form === "Union") return rw(StateForms.union); if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier); @@ -63,11 +72,10 @@ function defineFormName( } // Default name depends on area tier, some name bases have special names for tiers -function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, diplomacy = [""], neighbors = []) { +function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, relations: TRelation[], neighbors: number[]) { const form = StateForms.monarchy[areaTier]; - // TODO: specific names for vassals - const isVassal = diplomacy.includes("Vassal"); + const isVassal = relations.includes("Vassal"); if (isVassal) { if (areaTier === AreaTiers.DUCHY && neighbors.length > 1 && rand(6) < neighbors.length) return "Marches"; if (nameBase === NAMEBASE.English && P(0.3)) return "Dominion"; diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index 43be3e75..341e70c7 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -7,31 +7,12 @@ import {createCapitals} from "./createCapitals"; import {createStateData} from "./createStateData"; import {createTowns} from "./createTowns"; import {expandStates} from "./expandStates"; -import {generateDiplomacy} from "./generateDiplomacy"; +import {generateRelations} from "./generateRelations"; import {specifyBurgs} from "./specifyBurgs"; import {specifyStates} from "./specifyStates"; -type TCellsData = Pick< - IPack["cells"], - | "v" - | "c" - | "p" - | "b" - | "i" - | "g" - | "area" - | "h" - | "f" - | "t" - | "haven" - | "harbor" - | "r" - | "fl" - | "biome" - | "s" - | "pop" - | "culture" ->; +// prettier-ignore +type TCellsData = Pick; export function generateBurgsAndStates( cultures: TCultures, @@ -87,7 +68,7 @@ export function generateBurgsAndStates( const burgIds = assignBurgIds(burgs); const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs); - const {chronicle, diplomacy} = generateDiplomacy(statesData, statistics, pick(cells, "f")); + const diplomacy = generateRelations(statesData, statistics, pick(cells, "f")); const states = specifyStates(statesData, statistics, diplomacy, cultures, burgs); return {burgIds, stateIds, burgs, states}; diff --git a/src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts b/src/scripts/generation/pack/burgsAndStates/generateRelations.ts similarity index 88% rename from src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts rename to src/scripts/generation/pack/burgsAndStates/generateRelations.ts index 18f51dbf..2f84eae4 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateRelations.ts @@ -1,34 +1,32 @@ 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[]}; +import type {TStateStatistics} from "./collectStatistics"; +import type {TStateData} from "./createStateData"; + +export type TDiplomacy = {[key: number]: TRelation[]}; interface IDiplomacyData { i: number; type: TCultureType; center: number; + expansionism: number; area: number; neighbors: number[]; } -export function generateDiplomacy( +export function generateRelations( statesData: TStateData[], statistics: TStateStatistics, cells: Pick ) { - TIME && console.time("generateDiplomacy"); + TIME && console.time("generateRelations"); - const chronicle: string[] = []; const diplomacy = getBlankDiplomacyMatrix(statesData); - - if (statesData.length < 2) { - return {chronicle, diplomacy}; - } + if (statesData.length < 2) return diplomacy; const stateAreas = Object.values(statistics).map(({area}) => area); const averageStateArea = d3.mean(stateAreas)!; @@ -81,8 +79,8 @@ export function generateDiplomacy( } } - TIME && console.timeEnd("generateDiplomacy"); - return {chronicle, diplomacy}; + TIME && console.timeEnd("generateRelations"); + return diplomacy; } function getBlankDiplomacyMatrix(statesData: TStateData[]) { @@ -95,9 +93,9 @@ function getBlankDiplomacyMatrix(statesData: TStateData[]) { } function getDiplomacyData(stateData: TStateData, statistics: TStateStatistics): IDiplomacyData { - const {i, type, center} = stateData; + const {i, type, center, expansionism} = stateData; const {neighbors, area} = statistics[i]; - return {i, type, center, neighbors, area}; + return {i, type, center, expansionism, neighbors, area}; } function detectVassalState(from: IDiplomacyData, to: IDiplomacyData, averageStateArea: number) { diff --git a/src/scripts/generation/pack/burgsAndStates/simulateWars.ts b/src/scripts/generation/pack/burgsAndStates/simulateWars.ts new file mode 100644 index 00000000..abe3c2b7 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/simulateWars.ts @@ -0,0 +1,35 @@ +import {TIME} from "config/logging"; + +import type {TStateData} from "./createStateData"; +import type {TDiplomacy} from "./generateRelations"; + +export function simulateWars(statesData: TStateData[], diplomacy: TDiplomacy) { + TIME && console.time("simulateWars"); + + // declare wars + for (const {i} of statesData) { + const relations = diplomacy[i]; + if (!relations.includes("Rival")) continue; // no rivals to attack + if (relations.includes("Vassal")) continue; // not independent + if (relations.includes("Enemy")) continue; // already at war + + // select candidates to attack: rival independent states + const candidates = relations + .map((relation, index) => (relation === "Rival" && !diplomacy[index].includes("Vassal") ? index : 0)) + .filter(index => index); + if (!candidates.length) continue; + + const attacker = getDiplomacyData(statesData[i], statistics); + const defender = getDiplomacyData(statesData[ra(candidates)], statistics); + + const attackerPower = attacker.area * attacker.expansionism; + const defenderPower = defender.area * defender.expansionism; + if (attackerPower < defenderPower * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong + + const attackers = [attacker]; + const defenders = [defender]; + } + + TIME && console.timeEnd("simulateWars"); + return null; +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index fcd921b4..bdc718a3 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -7,7 +7,7 @@ import {isBurg} from "utils/typeUtils"; import type {TStateStatistics} from "./collectStatistics"; import type {TStateData} from "./createStateData"; -import type {TDiplomacy} from "./generateDiplomacy"; +import type {TDiplomacy} from "./generateRelations"; export function specifyStates( statesData: TStateData[], @@ -24,16 +24,17 @@ export function specifyStates( const states: IState[] = statesData.map(stateData => { const {i, center, type, culture, capital} = stateData; - const {area, burgs: burgsNumber, ...stats} = statistics[i]; + const {area, burgs: burgsNumber, neighbors, ...stats} = statistics[i]; const color = colors[i]; const capitalBurg = burgs[capital]; const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null; if (!capitalName) throw new Error("State capital is not a burg"); + const relations = diplomacy[i]; const nameBase = getNameBase(culture); const areaTier = getAreaTier(area); - const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber); + const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, relations, neighbors); const name = defineStateName(center, capitalName, nameBase, formName); const fullName = defineFullStateName(name, formName); @@ -47,7 +48,8 @@ export function specifyStates( area, burgs: burgsNumber, ...stats, - diplomacy: diplomacy[i] + neighbors, + relations }; return state; }); diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index 73e06792..8136e6ce 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -18,7 +18,7 @@ interface IState { rural: number; urban: number; neighbors: number[]; - diplomacy: TRelations[]; + relations: TRelation[]; removed?: boolean; } @@ -38,4 +38,14 @@ interface ICoa { t1: string; } -type TRelations = "Ally" | "Friendly" | "Neutral" | "Suspicion" | "Rival" | "Unknown" | "Suzerain" | "Vassal" | "x"; +type TRelation = + | "Ally" + | "Friendly" + | "Neutral" + | "Suspicion" + | "Rival" + | "Unknown" + | "Suzerain" + | "Vassal" + | "Enemy" + | "x"; From a2192fb9842fa47371fcc077411fe0876f611fbe Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 5 Sep 2022 01:27:40 +0300 Subject: [PATCH 3/4] refactor: generate conflicts --- .../pack/burgsAndStates/defineStateForm.ts | 15 +- .../pack/burgsAndStates/generateConflicts.ts | 158 ++++++++++++++++++ .../pack/burgsAndStates/simulateWars.ts | 35 ---- .../pack/burgsAndStates/specifyStates.ts | 12 +- src/scripts/generation/pack/pack.ts | 1 - src/types/events.d.ts | 15 ++ src/types/globals.d.ts | 1 + 7 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 src/scripts/generation/pack/burgsAndStates/generateConflicts.ts delete mode 100644 src/scripts/generation/pack/burgsAndStates/simulateWars.ts create mode 100644 src/types/events.d.ts diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts index 67f27d9e..0610edd0 100644 --- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -28,11 +28,11 @@ export function defineStateForm( areaTier: AreaTiers, nameBase: number, burgsNumber: number, - relations: TRelation[], - neighbors: number[] + neighbors: number[], + isVassal: boolean ) { const form = defineForm(type, areaTier); - const formName = defineFormName(form, nameBase, areaTier, burgsNumber, relations, neighbors); + const formName = defineFormName(form, nameBase, areaTier, burgsNumber, neighbors, isVassal); return {form, formName}; } @@ -59,10 +59,10 @@ function defineFormName( nameBase: number, areaTier: AreaTiers, burgsNumber: number, - relations: TRelation[], - neighbors: number[] + neighbors: number[], + isVassal: boolean ) { - if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, relations, neighbors); + if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, neighbors, isVassal); if (form === "Republic") return defineRepublicForm(areaTier, burgsNumber); if (form === "Union") return rw(StateForms.union); if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier); @@ -72,10 +72,9 @@ function defineFormName( } // Default name depends on area tier, some name bases have special names for tiers -function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, relations: TRelation[], neighbors: number[]) { +function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, neighbors: number[], isVassal: boolean) { const form = StateForms.monarchy[areaTier]; - const isVassal = relations.includes("Vassal"); if (isVassal) { if (areaTier === AreaTiers.DUCHY && neighbors.length > 1 && rand(6) < neighbors.length) return "Marches"; if (nameBase === NAMEBASE.English && P(0.3)) return "Dominion"; diff --git a/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts new file mode 100644 index 00000000..46bfeba0 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts @@ -0,0 +1,158 @@ +import * as d3 from "d3"; + +import {TIME} from "config/logging"; +import {list, trimVowels} from "utils/languageUtils"; +import {gauss, ra} from "utils/probabilityUtils"; + +export function generateConflicts(states: IState[]) { + TIME && console.time("generateConflicts"); + + const statesMap = new Map(states.map(state => [state.i, state])); + const wars: IConflict[] = []; + + for (const {i: stateId, relations} of states) { + if (!relations.includes("Rival")) continue; // no rivals to attack + if (relations.includes("Vassal")) continue; // not independent + if (relations.includes("Enemy")) continue; // already at war + + // select candidates to attack: rival independent states + const candidates = relations + .map((relation, stateId) => { + const state = statesMap.get(stateId); + const isVassal = state?.relations.includes("Vassal"); + return relation === "Rival" && state && !isVassal ? stateId : 0; + }) + .filter(index => index); + if (!candidates.length) continue; + + const attacker = statesMap.get(stateId); + const defender = statesMap.get(ra(candidates)); + if (!attacker || !defender) continue; + + const attackerPower = getStatePower(attacker); + const defenderPower = getStatePower(defender); + if (attackerPower < defenderPower * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong + + const war = simulateWar(attacker, defender); + wars.push(war); + } + + TIME && console.timeEnd("generateConflicts"); + return wars; + + function simulateWar(attacker: IState, defender: IState): IConflict { + const history = [`${attacker.name} declared a war on its rival ${defender.name}`]; + + // vassals join the war + function addVassals(state: IState, side: "attackers" | "defenders") { + const vassals = getVassals(state); + if (vassals.length === 0) return []; + const names = list(vassals.map(({name}) => name)); + history.push(`${state.name}'s vassal${vassals.length > 1 ? "s" : ""} ${names} joined the war on ${side} side`); + return vassals; + } + + const attackers = [attacker, ...addVassals(attacker, "attackers")]; + const defenders = [defender, ...addVassals(defender, "defenders")]; + + let attackersPower = d3.sum(attackers.map(getStatePower)); + let defendersPower = d3.sum(defenders.map(getStatePower)); + + defender.relations.forEach((relation, stateId) => { + if (relation !== "Ally" || !stateId) return; + const ally = statesMap.get(stateId)!; + if (ally.relations.includes("Vassal")) return; + + const allyParty = [ally, ...getVassals(ally)]; + + const joinedPower = defendersPower + d3.sum(allyParty.map(getStatePower)); + const isWeak = joinedPower < attackersPower * gauss(1.6, 0.8, 0, 10, 2); + const isRival = ally.relations[attacker.i] !== "Rival"; + if (!isRival && isWeak) { + // defender's ally does't involve: break the pact + const reason = ally.relations.includes("Enemy") ? "Being already at war," : `Frightened by ${attacker.name},`; + history.push(`${reason} ${ally.name} severed the defense pact with ${defender.name}`); + + allyParty.forEach(ally => { + defender.relations[ally.i] = "Suspicion"; + ally.relations[defender.i] = "Suspicion"; + }); + + return; + } + + // defender's ally and its vassals join the war + defenders.push(...allyParty); + const withVassals = allyParty.length > 1 ? " and its vassals " : ""; + history.push(`Defender's ally ${ally.name}${withVassals}joined the war`); + + defendersPower = joinedPower; + }); + + attacker.relations.forEach((relation, stateId) => { + if (relation !== "Ally" || !stateId) return; + if (defenders.some(defender => defender.i === stateId)) return; + const ally = statesMap.get(stateId)!; + if (ally.relations.includes("Vassal")) return; + + const allyParty = [ally, ...getVassals(ally)]; + + const joinedPower = attackersPower + d3.sum(allyParty.map(getStatePower)); + const isWeak = joinedPower < defendersPower * 1.2; + const isRival = ally.relations[defender.i] !== "Rival"; + if (!isRival || isWeak) { + history.push(`Attacker's ally ${ally.name} avoided entering the war`); + return; + } + + const allies = ally.relations.map((relation, stateId) => (relation === "Ally" ? stateId : 0)); + if (defenders.some(({i}) => allies.includes(i))) { + history.push(`Attacker's ally ${ally.name} did not join the war as it has allies on both sides`); + return; + } + + // attacker's ally and its vassals join the war + attackers.push(...allyParty); + const withVassals = allyParty.length > 1 ? " and its vassals " : ""; + history.push(`Attacker's ally ${ally.name}${withVassals}joined the war`); + + attackersPower = joinedPower; + }); + + // change relations to Enemy for all participants + attackers.forEach(attacker => { + defenders.forEach(defender => { + defender.relations[attacker.i] = "Enemy"; + attacker.relations[defender.i] = "Enemy"; + }); + }); + + const advantage = getAdvantage(attackersPower, defendersPower); + const winning = attackersPower > defendersPower ? "attackers" : "defenders"; + history.push(`At the moment, the ${advantage} advantage is on the side of the ${winning}`); + + const name = `${attacker.name}-${trimVowels(defender.name)}ian War`; + const parties = {attackers: attackers.map(({i}) => i), defenders: defenders.map(({i}) => i)}; + const start = options.year - gauss(2, 2, 0, 5); + return {type: "conflict", name, start, parties, description: history.join(". ")}; + } + + function getStatePower(state: IState) { + return state.area * state.expansionism; + } + + function getVassals(state: IState) { + return state.relations + .map((relation, stateId) => (relation === "Suzerain" ? stateId : 0)) + .filter(stateId => stateId) + .map(stateId => statesMap.get(stateId)!); + } + + function getAdvantage(p1: number, p2: number) { + const advantage = p1 > p2 ? p1 / p2 : p2 / p1; + if (advantage > 3) return "overwhelming"; + if (advantage > 2) return "decisive"; + if (advantage > 1.3) return "significant"; + return "minor"; + } +} diff --git a/src/scripts/generation/pack/burgsAndStates/simulateWars.ts b/src/scripts/generation/pack/burgsAndStates/simulateWars.ts deleted file mode 100644 index abe3c2b7..00000000 --- a/src/scripts/generation/pack/burgsAndStates/simulateWars.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {TIME} from "config/logging"; - -import type {TStateData} from "./createStateData"; -import type {TDiplomacy} from "./generateRelations"; - -export function simulateWars(statesData: TStateData[], diplomacy: TDiplomacy) { - TIME && console.time("simulateWars"); - - // declare wars - for (const {i} of statesData) { - const relations = diplomacy[i]; - if (!relations.includes("Rival")) continue; // no rivals to attack - if (relations.includes("Vassal")) continue; // not independent - if (relations.includes("Enemy")) continue; // already at war - - // select candidates to attack: rival independent states - const candidates = relations - .map((relation, index) => (relation === "Rival" && !diplomacy[index].includes("Vassal") ? index : 0)) - .filter(index => index); - if (!candidates.length) continue; - - const attacker = getDiplomacyData(statesData[i], statistics); - const defender = getDiplomacyData(statesData[ra(candidates)], statistics); - - const attackerPower = attacker.area * attacker.expansionism; - const defenderPower = defender.area * defender.expansionism; - if (attackerPower < defenderPower * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong - - const attackers = [attacker]; - const defenders = [defender]; - } - - TIME && console.timeEnd("simulateWars"); - return null; -} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index bdc718a3..cebb55db 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -4,6 +4,7 @@ import {createAreaTiers, defineStateForm} from "./defineStateForm"; import {defineFullStateName, defineStateName} from "./defineStateName"; import {defineStateColors} from "./defineStateColors"; import {isBurg} from "utils/typeUtils"; +import {generateConflicts} from "./generateConflicts"; import type {TStateStatistics} from "./collectStatistics"; import type {TStateData} from "./createStateData"; @@ -32,13 +33,15 @@ export function specifyStates( if (!capitalName) throw new Error("State capital is not a burg"); const relations = diplomacy[i]; + const isVassal = relations.includes("Vassal"); + const nameBase = getNameBase(culture); const areaTier = getAreaTier(area); - const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, relations, neighbors); + const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, neighbors, isVassal); const name = defineStateName(center, capitalName, nameBase, formName); const fullName = defineFullStateName(name, formName); - const state: IState = { + return { name, ...stateData, form, @@ -51,9 +54,12 @@ export function specifyStates( neighbors, relations }; - return state; }); + const wars = generateConflicts(states); // mutates states + console.log(wars); + console.log(states); + TIME && console.timeEnd("specifyStates"); return [NEUTRALS, ...states]; } diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 8ff96f6b..85af9c85 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack { } }); - // BurgsAndStates.defineStateForms(); // BurgsAndStates.generateProvinces(); // BurgsAndStates.defineBurgFeatures(); diff --git a/src/types/events.d.ts b/src/types/events.d.ts new file mode 100644 index 00000000..25ef3a08 --- /dev/null +++ b/src/types/events.d.ts @@ -0,0 +1,15 @@ +interface IEvent { + type: string; + name: string; + start: number; + end?: number; // undefined for ongoing events + description: string; +} + +interface IConflict extends IEvent { + type: "conflict"; + parties: { + attackers: number[]; + defenders: number[]; + }; +} diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 005d054a..c1c18074 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -24,6 +24,7 @@ interface IOptions { showMFCGMap: boolean; winds: [number, number, number, number, number, number]; stateLabelsMode: "auto" | "short" | "full"; + year: number; } declare let populationRate: number; From 9b3a3f2e481c33516085c57d76d0e5de5a2e6ef1 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 5 Sep 2022 21:00:09 +0300 Subject: [PATCH 4/4] refactor: generate historical conflicts --- public/libs/define-globals.js | 4 +- src/modules/markers-generator.js | 1 + src/modules/names-generator.js | 7 +-- src/scripts/generation/generation.ts | 3 +- .../generation/pack/burgsAndStates/config.ts | 12 +++++ .../burgsAndStates/generateBurgsAndStates.ts | 9 ++-- .../pack/burgsAndStates/generateConflicts.ts | 47 +++++++++++++++++-- .../pack/burgsAndStates/specifyStates.ts | 8 ++-- src/scripts/generation/pack/pack.ts | 6 +-- src/types/events.d.ts | 8 ++-- src/types/globals.d.ts | 4 +- 11 files changed, 81 insertions(+), 28 deletions(-) diff --git a/public/libs/define-globals.js b/public/libs/define-globals.js index d1cf0020..fb1890a0 100644 --- a/public/libs/define-globals.js +++ b/public/libs/define-globals.js @@ -4,13 +4,15 @@ let grid = {}; // initial graph based on jittered square grid and data let pack = {}; // packed graph and data +let notes = []; +let events = {}; + let seed; let mapId; let mapHistory = []; let elSelected; -let notes = []; let customization = 0; let rulers; diff --git a/src/modules/markers-generator.js b/src/modules/markers-generator.js index bcd12577..fdef557f 100644 --- a/src/modules/markers-generator.js +++ b/src/modules/markers-generator.js @@ -559,6 +559,7 @@ window.Markers = (function () { const {cells, states} = pack; const state = states[cells.state[cell]]; + // TODO: use events if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state); const campaign = ra(state.campaigns); const date = generateDate(campaign.start, campaign.end); diff --git a/src/modules/names-generator.js b/src/modules/names-generator.js index 21e8e062..a480ac77 100644 --- a/src/modules/names-generator.js +++ b/src/modules/names-generator.js @@ -149,11 +149,8 @@ window.Names = (function () { // generate short name for base const getBaseShort = function (base) { if (nameBases[base] === undefined) { - tip( - `Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, - false, - "error" - ); + const message = `Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`; + tip(message, false, "error"); base = 1; } const min = nameBases[base].min - 1; diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 5d90ff9a..209afeb2 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -53,13 +53,14 @@ async function generate(options?: IGenerationOptions) { window.mapCoordinates = calculateMapCoordinates(); const newGrid = await createGrid(grid, precreatedGraph); - const newPack = createPack(newGrid); + const {pack: newPack, conflicts} = createPack(newGrid); // TODO: draw default ruler // redefine global grid and pack grid = newGrid; pack = newPack; + events = {conflicts}; // temp rendering for debug // renderLayer("cells"); diff --git a/src/scripts/generation/pack/burgsAndStates/config.ts b/src/scripts/generation/pack/burgsAndStates/config.ts index daf09ca0..8d038625 100644 --- a/src/scripts/generation/pack/burgsAndStates/config.ts +++ b/src/scripts/generation/pack/burgsAndStates/config.ts @@ -113,3 +113,15 @@ export const relations = { farStates: {Friendly: 1, Neutral: 12, Suspicion: 2}, navalToNaval: {Neutral: 2, Suspicion: 2, Rival: 1} }; + +export const conflictTypes = { + War: 6, + Conflict: 2, + Campaign: 4, + Invasion: 2, + Rebellion: 2, + Conquest: 2, + Intervention: 1, + Expedition: 1, + Crusade: 1 +}; diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index 341e70c7..d2d2dc8f 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -21,7 +21,7 @@ export function generateBurgsAndStates( rivers: Omit[], vertices: IGraphVertices, cells: TCellsData -): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates} { +): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates; conflicts: IConflict[]} { const cellsNumber = cells.i.length; const scoredCellIds = getScoredCellIds(); @@ -31,7 +31,8 @@ export function generateBurgsAndStates( burgIds: new Uint16Array(cellsNumber), stateIds: new Uint16Array(cellsNumber), burgs: [NO_BURG], - states: [NEUTRALS] + states: [NEUTRALS], + conflicts: [] }; } @@ -69,9 +70,9 @@ export function generateBurgsAndStates( const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs); const diplomacy = generateRelations(statesData, statistics, pick(cells, "f")); - const states = specifyStates(statesData, statistics, diplomacy, cultures, burgs); + const {states, conflicts} = specifyStates(statesData, statistics, diplomacy, cultures, burgs); - return {burgIds, stateIds, burgs, states}; + return {burgIds, stateIds, burgs, states, conflicts}; function getScoredCellIds() { const score = new Int16Array(cells.s.map(s => s * Math.random())); diff --git a/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts index 46bfeba0..30f7f8c8 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts @@ -1,12 +1,22 @@ import * as d3 from "d3"; import {TIME} from "config/logging"; -import {list, trimVowels} from "utils/languageUtils"; -import {gauss, ra} from "utils/probabilityUtils"; +import {getAdjective, list, trimVowels} from "utils/languageUtils"; +import {gauss, P, ra, rw} from "utils/probabilityUtils"; +import {conflictTypes} from "./config"; -export function generateConflicts(states: IState[]) { +const {Names} = window; + +export function generateConflicts(states: IState[], cultures: TCultures): IConflict[] { TIME && console.time("generateConflicts"); + const historicalWars = generateHistoricalConflicts(states, cultures); + const ongoingWars = generateOngoingConflicts(states); + TIME && console.timeEnd("generateConflicts"); + return [...historicalWars, ...ongoingWars].sort((a, b) => a.start - b.start); +} + +function generateOngoingConflicts(states: IState[]): IConflict[] { const statesMap = new Map(states.map(state => [state.i, state])); const wars: IConflict[] = []; @@ -37,7 +47,6 @@ export function generateConflicts(states: IState[]) { wars.push(war); } - TIME && console.timeEnd("generateConflicts"); return wars; function simulateWar(attacker: IState, defender: IState): IConflict { @@ -134,7 +143,7 @@ export function generateConflicts(states: IState[]) { const name = `${attacker.name}-${trimVowels(defender.name)}ian War`; const parties = {attackers: attackers.map(({i}) => i), defenders: defenders.map(({i}) => i)}; const start = options.year - gauss(2, 2, 0, 5); - return {type: "conflict", name, start, parties, description: history.join(". ")}; + return {name, start, parties, description: history.join(". ")}; } function getStatePower(state: IState) { @@ -156,3 +165,31 @@ export function generateConflicts(states: IState[]) { return "minor"; } } + +function generateHistoricalConflicts(states: IState[], cultures: TCultures): IConflict[] { + const statesMap = new Map(states.map(state => [state.i, state])); + const isConflict = (conflict: IConflict | null): conflict is IConflict => conflict !== null; + const getNameBase = (cultureId: number) => cultures[cultureId].base; + return states.map(generateConflicts).flat(); + + function generateConflicts(state: IState): IConflict[] { + const conflicts = state.neighbors + .map((neighbor, index) => { + if (index && P(0.8)) return null; + const enemy = statesMap.get(neighbor); + if (!enemy) return null; + + const properName = P(0.8) ? enemy.name : Names.getBaseShort(getNameBase(enemy.culture)); + const name = getAdjective(properName) + " " + rw(conflictTypes); + const start = gauss(options.year - 100, 150, 1, options.year - 6); + const end = start + gauss(4, 5, 1, options.year - start - 1); + const parties = {attackers: [state.i], defenders: [enemy.i]}; + + const conflict: IConflict = {name, start, end, parties}; + return conflict; + }) + .filter(isConflict); + + return conflicts; + } +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index cebb55db..ba9be9ee 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -16,7 +16,7 @@ export function specifyStates( diplomacy: TDiplomacy, cultures: TCultures, burgs: TBurgs -): TStates { +): {states: TStates; conflicts: IConflict[]} { TIME && console.time("specifyStates"); const colors = defineStateColors(statistics); @@ -56,10 +56,8 @@ export function specifyStates( }; }); - const wars = generateConflicts(states); // mutates states - console.log(wars); - console.log(states); + const conflicts = generateConflicts(states, cultures); // mutates states TIME && console.timeEnd("specifyStates"); - return [NEUTRALS, ...states]; + return {states: [NEUTRALS, ...states], conflicts}; } diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 85af9c85..f5be5c3c 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -18,7 +18,7 @@ import {generateReligions} from "./religions/generateReligions"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {Biomes} = window; -export function createPack(grid: IGrid): IPack { +export function createPack(grid: IGrid): {pack: IPack; conflicts: IConflict[]} { const {temp, prec} = grid.cells; const {vertices, cells} = repackGrid(grid); @@ -97,7 +97,7 @@ export function createPack(grid: IGrid): IPack { pop: population }); - const {burgIds, stateIds, burgs, states} = generateBurgsAndStates( + const {burgIds, stateIds, burgs, states, conflicts} = generateBurgsAndStates( cultures, mergedFeatures, temp, @@ -199,7 +199,7 @@ export function createPack(grid: IGrid): IPack { religions }; - return pack; + return {pack, conflicts}; } // repack grid cells: discart deep water cells, add land cells along the coast diff --git a/src/types/events.d.ts b/src/types/events.d.ts index 25ef3a08..814ba4af 100644 --- a/src/types/events.d.ts +++ b/src/types/events.d.ts @@ -1,13 +1,15 @@ +interface IEvents { + conflicts: IConflict[]; +} + interface IEvent { - type: string; name: string; start: number; end?: number; // undefined for ongoing events - description: string; + description?: string; } interface IConflict extends IEvent { - type: "conflict"; parties: { attackers: number[]; defenders: number[]; diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index c1c18074..15f81616 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -1,11 +1,13 @@ declare let grid: IGrid; declare let pack: IPack; +declare let notes: INote[]; +declare let events: IEvents; + declare let seed: string; declare let mapId: number; declare let mapHistory: IMapHistoryEntry[]; -declare let notes: INote[]; declare let customization: number; declare let rulers: Rulers;