refactor: generate conflicts

This commit is contained in:
Azgaar 2022-09-05 01:27:40 +03:00
parent 136e71eae9
commit a2192fb984
7 changed files with 190 additions and 47 deletions

View file

@ -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";

View file

@ -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";
}
}

View file

@ -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;
}

View file

@ -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];
}

View file

@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack {
}
});
// BurgsAndStates.defineStateForms();
// BurgsAndStates.generateProvinces();
// BurgsAndStates.defineBurgFeatures();