⚓ Seaworth Games

Remnants of the Precursors · ↑ Systems Map · Design series 06

ROTP Diplomacy

How the diplomacy system works in the original design. There is no trust score and no hidden relationship model — there is one number per direction, rebuilt from scratch every turn out of a ledger of grievances. Everything else in the system exists to write entries into that ledger or to read them back out.

Scope base ROTP rules Core EmpireView · DiplomaticEmbassy · DiplomaticIncident AI xilmi AIDiplomat

01The shape of it

Every relationship is two objects, and they are allowed to disagree.

Diplomacy in ROTP is asymmetric. Every empire owns an array of EmpireView objects indexed by the other empire's id, and A.viewForEmpire(B) and B.viewForEmpire(A) are two distinct objects. A can loathe B while B is indifferent.

Treaty status is kept in sync by hand — every signing method writes both embassies — but opinion never is. That asymmetry is the source of most of the system's texture: an empire can be preparing a war you have no idea is coming.

EMPIREVIEW — ONE PER (VIEWER, VIEWED) PAIR DiplomaticEmbassy treaty : DiplomaticTreaty relations : float [-100,100] incidents : Map<key,inc> timers[20], 9 cooldowns contact, contactYear warFooting, casusBelli SpyNetwork espionage, sabotage, observed tech TradeRoute level, profit, trade income incidents DiplomaticTreaty None · Peace · War FinalWar · Pact Alliance · Unity each overrides exactly one isXxx() to true

Each view bundles three subsystems against the same neighbour: the embassy (treaties and opinion), the spy network, and the trade route. All three feed each other — a trade route generates goodwill, a caught spy generates grievance, and treaty status re-tasks the spy network.

Two names that do not exist

There is no TreatyNonAggression — a non-aggression pact is the Pact. There is no trade treaty class — trade lives in TradeRoute, not the treaty. And despite what the interface language suggests, there is no "trust" field anywhere in the codebase. There is exactly one scalar, relations.

02Relations: one number, rebuilt every turn

It is not accumulated and it does not decay. It is recomputed from scratch.

private void recalculateRelationsLevel() {
    float rel = owner().baseRelations(empire());   // race pairing + difficulty
    rel += treatyRelationsAdj();                   // current treaty
    for (DiplomaticIncident ev: incidents.values())
        rel += ev.currentSeverity();               // live grievances, decayed
    relations = bounds(-100, rel, 100);
}

Three inputs, clamped to [−100, +100], recomputed whenever an incident is added and again every turn. The consequence is worth stating plainly: all memory in the diplomatic system lives in the incident map. Clear the incidents and relations snap instantly back to the race baseline. Any notion of a grudge, a reputation, or a slowly rebuilt friendship has to be expressed as an incident, because there is nowhere else to put it.

Treaty adjustment

TreatyAdjustmentEffect
finalWar−200Pins relations to the −100 floor. Unrecoverable by design.
war−10Surprisingly mild — war itself barely moves the needle. Its incidents do the work.
none0—
pact+5
alliance+10
unity+200Pins to the +100 ceiling.

The scale of those numbers is instructive. A declaration of war is worth −10; the DeclareWarIncident it generates is worth −70. The treaty is a small standing bias; the events are what actually drive opinion.

Race baseline

baseRelations is a per-race-pair table plus a flat difficulty offset running from +30 (hostility lowest) to −30 (highest). That option is not merely numerical — the AI tests for ≤ −30 explicitly as "always-war mode" and short-circuits both war-weariness and the opportunity-war test on it, so the hardest hostility setting changes AI behaviour structurally.

03Incidents: the grievance engine

Roughly 56 event types, a keyed map, and linear decay. This is where the game's memory lives.

Every diplomatically meaningful event — a lost colony, a stolen tech, a fleet in your orbit, a council vote, an act of genocide — creates an incident on the offended party's embassy. Each carries a signed severity, a duration in years, and the year it occurred, and its contribution decays linearly to nothing:

public float currentSeverity() { return severity * remainingTime() / duration(); }
public boolean isForgotten()   { return remainingTime() <= 0; }
private float remainingTime()  { return max(0, dateOccurred + duration - currentYear()); }
A handful of subclasses override currentSeverity() to skip decay entirely.

The map is a slot, not a log

Incidents are keyed, and adding one keeps only the more severe of the incoming and existing entry. Whether an event type accumulates or is a singleton is decided purely by whether its key embeds a date:

Key shapeBehaviourExamples
"...:" + date One slot per year — grievances stack Skirmish, Trespassing, Sabotage, Paranoia, aid
constant string One slot ever, overwritten only by something worse Declare War, Break Pact, First Contact, Coexist

The ones that matter

IncidentSeverityDur.Notes
Final War−200∞No decay. Declared by the council against rebels.
Declare War−7010The default, casus-belli-less declaration.
Assassination−5020Victim sees −50; onlookers see +50 and approve. The only incident that triggers war immediately.
Genocide−500–150Duration is the observer's personality: a pacifist remembers for 150 turns, an aggressive leader forgets instantly.
Paranoiato −1001Scales with how much of the galaxy they occupy. Added every single turn, unconditionally.
Break Alliance−3010
First Contact−3010Meeting someone is itself a mild grievance.
Colony Captured−20 − popLost10Floored at −50. Charges the attack-warning timer.
Break Pact−1510
Espionage & sabotage−20 to −3010–25Compounding — each includes the running sum of all other live spy incidents.
Sign Peace+2010–15Duration equals the rolled peace duration.
Trade incomeup to +303Scales with profit as a share of your production.
Kill Guardian+10050Killing the Orion guardian buys a great deal of goodwill.

Note what Paranoia does structurally. It is regenerated every turn, scaled by the fraction of the galaxy the other empire holds, and modified by xenophobia and by your current treaty. It is the mechanism by which simply being large makes everyone dislike you, with no action required on either side — the system's built-in rubber band against a runaway leader.

The forgiveness counterweight

Against that, one positive incident charges slowly upward at +1 per turn toward a goal of the trade/pact/alliance bonus minus the sum of every negative incident, floored at zero — and drops instantly when that goal falls:

severityGoal = 0;
if (trade active)      severityGoal += 15;
if (pact)              severityGoal += 20;
else if (alliance)     severityGoal += 35;
for (inc : allIncidents)
    if (inc.currentSeverity() < 0) severityGoal += inc.currentSeverity();
severityGoal = max(0, severityGoal);
if (severity < severityGoal) severity++;   // +1/turn, no faster

This is the slow-forgiveness mechanism that keeps the AI from spiralling into permanent hatred. It is deliberately asymmetric: goodwill accrues one point per turn, but collapses the moment the underlying relationship worsens. A second incident does the same job for erratic leaders, drifting 10% per turn toward a fresh random target in [−50, +50] — which is why an erratic neighbour's intentions genuinely flip-flop.

04The seven treaties

Five are pure state. Two carry their own machinery.

TreatyMeaningState it carries
NoneNo relationship. You may attack without diplomatic penalty.—
PeaceA timed ceasefire. Attacking carries a penalty.A duration, rolled 10–15 turns at signing. Counts down and reverts both embassies to None. Signing recalls attacking forces on both sides.
WarOpen war.Snapshots colonies, population, factories and fleet size at the start and again each turn — the raw material for war-weariness scoring.
Final WarThe council's war of annihilation against rebels. Diplomacy is over.—
PactNon-aggression. You may only attack each other at systems one of you has colonised.—
AllianceMutual defence, shared vision, joint wars.A standing per side (starting 50) and a want-to-break flag — see below.
UnityEffectively one empire. Free tech sharing, no spying.—

Alliances decay on their own

The alliance is the only treaty with an internal life. Each turn it counts how many enemies the two allies actually share, and adjusts each side's standing accordingly:

switch (jointEnemies) {
    case 0: if (v > 50) standings[i] = v - max(1, (100-v)/10);  break;  // decay toward 50
    case 1: standings[i] -= 2; break;
    case 2: standings[i] -= 4; break;
    default: standings[i] -= 5; break;
}
// then: chance to want out = -standings[i]/100, and only once standings drop below -5

The design intent is clear and slightly counterintuitive: an alliance with no shared enemies drifts toward neutral, and an alliance with many shared enemies decays fastest of all. Alliances erode under the strain of the wars they commit you to. Aid, and attacking each other's enemies, feed directly back into the standing.

Worth knowing: the AI does not consult this standings model when deciding to break an alliance. It uses its own viability test instead — so the decay drives the player-facing warnings while the AI breaks alliances for its own reasons.

05Timers, cooldowns, praise and warnings

Nine countdowns, and one branch that decides whether the AI spams you or ignores you.

Each embassy carries offer cooldowns, a diplomat-recall timer, and a 20-slot array of incident timers. The base delays:

CooldownTurnsCooldownTurns
Tech exchange1Alliance30
Trade10Joint war20
Peace10Break alliance30
Pact20Max requests/turn4
The asymmetry that makes it work
// player refusals are remembered for the entire duration to avoid the AI
// spamming the player. AI refusals are completely reset after each turn
// to allow players to continue asking once each turn if they want
if (view.owner().isPlayerControlled()) {
    tradeTimer--; techTimer--; peaceTimer--; pactTimer--; allianceTimer--;
} else {
    tradeTimer = 0; techTimer = 0; peaceTimer = 0; pactTimer = 0; allianceTimer = 0;
}

AI-owned embassies zero every cooldown, every turn; only player-owned ones count down. The source comment explains the intent exactly: an AI's refusal of the player sticks for the full delay, but the player may re-ask once per turn. It is a deliberate anti-spam measure pointing in one direction only.

Praise and warnings

Each turn the AI scans this turn's new incidents for the most severe one worth praising or warning about. Both have a rising floor: each praise raises the praise bar by 10 (minimum 10 at peace, 50 at war), each warning raises the warn bar by 5. Both decay by one per turn and are capped at 20, so the bar rises quickly under a stream of events and relaxes during quiet periods. The warning threshold is also softened by friendship — a quarter of the normal bar for an ally, half for a pact partner.

Warnings are what make war legal

Issuing a warning also charges the offending incident's own timer by its duration. That matters because several incident types — espionage and sabotage especially — explicitly do not trigger war on their own. The only route from those grievances to a declaration runs through the timer that the warning charges.

The loop is therefore: offend → get warned → keep offending → get invaded. An empire that has never warned you cannot go to war over espionage, and when the timer lapses the grievance stops being a casus belli even though the relations damage remains.

06The friendship ladder

Strictly sequential, and it starts with a trade route.

Once per turn, per contacted neighbour, the AI works through a single ordered list of decisions — deliberately worst offer to best, so a decision to declare war short-circuits everything friendly below it:

1.  decidedToDeclareWar(v)          // returns -> nothing else happens this turn
2.  decidedToBreakAlliance(v)
3.  decidedToBreakPact(v)
    // -- war and alliance-breaks work even with the diplomat gone --
4.  decidedToBreakTrade(v)
5.  decidedToIssueWarning(v)
6.  willingToOfferPeace(v)
    // -- if v is an enemy: return. No carrots for anyone we're preparing war on --
7.  willingToOfferJointWar(v, target)   // for each of our enemies
8.  willingToOfferTrade(v, maxLevel)
9.  canOfferPact(v)     && willingToOfferPact(v)
10. canOfferAlliance(v) && willingToOfferAlliance(v)
11. decidedToExchangeTech(v)
12. decidedToIssuePraise(v)

Offering a pact requires an existing trade route. Offering an alliance requires an existing pact. There is no shortcut:

contact trade route relations > pact threshold pact relations > alliance threshold alliance

Trade is therefore the gateway to every friendly relationship in the game — and it is also what makes you vulnerable to espionage, since having no trade treaties at all grants a flat counter-espionage bonus. Opening your economy is what opens your borders, in both directions.

The thresholds slide with desperation

// pact threshold; the alliance threshold is identical with base = 170
float base = 130;
float enemyPower  = 200;
float friendPower = 200 + empire.militaryPowerLevel();
for (emp : empire.enemies()) enemyPower  += emp.militaryPowerLevel();
for (emp : empire.allies())  friendPower += emp.militaryPowerLevel();
base *= min(1.0f, friendPower / enemyPower);
if (empire.leader().isDiplomat()) base *= 124f / 130f;
return base - 100;   // +30 unpressured ... -100 when badly outgunned

A comfortable AI wants relations > +30 for a pact and > +70 for an alliance. An AI that is losing will pact with almost anyone, because the threshold slides all the way to the floor. This is the AI's main survival behaviour and it falls out of two power ratios — no special-case panic logic.

One shortcut bypasses the arithmetic entirely: if a prospective partner is already allied to one of your allies, the answer is an automatic yes. Alliance blocs consolidate themselves.

07Going to war

Three motives, plus an incident route — and a preparation phase before any declaration.

PathTest
Incident A new incident this turn either triggers war outright, or carries a currently-active warning timer. This is the route for espionage, sabotage and colony attacks.
Hate relations <= warThreshold. The threshold starts at the target's leader personality — aggressive −70, pacifist −90, everyone else −80 — then adjusts by 10 × (theirEnemies − myEnemies), +30 if they are allied with an enemy of mine, and 10 × scaleOfContempt for the power gap.
Opportunity Pure military calculus — relations are not consulted at all. Not still needing colony ships, tech adequate for war, the target is the best available victim, and my alliance web outguns theirs after a personality-scaled exploitation ratio.
Desperation No current enemies, the target holds the largest reachable population capacity, I am first in factory capacity, tech is adequate, and I am no longer expanding. The "I have run out of room to grow" war.

War preparations come first

None of these declare war directly. All of them call beginWarPreparations, which sets a warFooting flag and records a casus belli. That intermediate state means "I intend to attack you": it makes the target count as an enemy, blocks every friendly offer in the ladder, and is converted into an actual declaration later by the military AI when it commits forces.

Only assassination declares war immediately. Every other path gives the target a window — if they can see it — and gives the aggressor a chance to back down. War preparations are re-evaluated each turn and dropped if the grievance expires, if the target becomes unreachable, or if the military assessment reverses.

Declaring war has wide side effects: it ends the existing treaty, stops the trade route, resets the threat timers, sets a three-cycle peace cooldown, withdraws both ambassadors, and — if the war breaks a pact or alliance — files an oath-breaker incident that every observer can see.

08Making peace

Decided by exhaustion, not by affection.

This is the part of the system most likely to surprise. When an AI receives a peace offer it never reads relations. It asks one question: am I war-weary?

  1. Follow your allies. If any ally has already made peace with this empire, accept.
  2. Never strand an invasion. If troop transports are in flight toward their systems, refuse.
  3. Unreachable wars end. If the target is out of ship range and no orbiting fleet could extend that range, accept.
  4. Otherwise, assess. Accept if every colony is under siege while outgunned; or if the war was one I started opportunistically and I am no longer winning; or if they declared on me and now out-power me by more than my exploitation ratio; or if the war has already cost me a colony.

Peace, once signed, is a timed ceasefire of 10–15 turns, not a permanent state. Both sides recall attacking forces, both hide their spies against each other, and when the timer runs out the treaty reverts to None on both embassies — not to war, but to the neutral state from which either side may re-escalate.

Design consequence

Because acceptance is exhaustion-based, you cannot buy peace with goodwill, and an AI that is winning will not stop no matter how much it likes you. Conversely an AI that is losing will make peace with an empire it hates. The relations scalar governs whether wars start; the military situation governs whether they end.

09Trade, technology, tribute and threats

The everyday transactions, and why AI tech trading feels so stingy.

Trade routes

Trade is offered at one of four levels, computed as 6.25 / 12.5 / 18.75 / 25 percent of the smaller partner's production and rounded to multiples of 25. A route takes time to become profitable and generates a recurring positive incident scaled by profit as a share of your own production. Acceptance is nearly unconditional — the AI checks the cooldown and whether you are an enemy, and otherwise says yes without consulting relations at all.

Re-asking is throttled by an escalating rule: a new offer must exceed the last requested level by 25% for each consecutive refusal.

Technology exchange

Tech trades are two-sided: you name what you want, and the partner names a counter-basket of up to three techs. Both sides must find the deal acceptable, and the exchange is deferred one turn — the agreement records intent, and the techs are actually learned at end of turn.

The stingy part is the willingness test. The AI will give away a technology only if it is obsolete, or the partner is a full ally, or the tech is already known to someone's spies or about to be stolen anyway. Everything else is refused outright. It will also never ask for a tech it already considers obsolete, and it deprioritises researching anything it expects to acquire by trade.

Tribute, aid and threats

  • Tribute — demanding it is always permitted; the AI's chance of paying is a flat coin-weighted 50%. Paying generates a small positive incident for the payer, which is to say tribute buys a little genuine goodwill.
  • Aid — financial and technological gifts generate positive incidents scaled against the recipient's production, capped at +10 and +15 respectively. Aid to an empire at war with someone also generates a matching negative incident on that third party's embassy.
  • Threats — a demand to stop spying is the only one that reliably does anything: an AI that decides to comply hides its spies, and charges the offended party's spy timer in the process. Threats to stop attacking or to evict spies are simply ignored.

10Leaders

Six personalities and six objectives, modifying nearly every threshold in the system.

Every empire has a leader with one personality — Erratic, Pacifist, Honorable, Ruthless, Aggressive, Xenophobic — and one objective — Militarist, Ecologist, Diplomat, Industrialist, Expansionist, Technologist. Between them they touch almost every number in this document:

TraitEffect on diplomacy
AggressiveGoes to war at −70 rather than −80. Forgets genocide instantly. Exploits weakness more readily.
PacifistNeeds −90 to hate-war. Remembers genocide for 150 turns. Doubles the price of joining a joint war. Accepts council rulings unconditionally.
HonorableRemembers oath-breaking far longer than anyone else.
RuthlessForgets oath-breaking immediately. Will break a pact for an opportunity war.
XenophobicDoubles paranoia, trespassing and buildup severity. Doubles the diplomat-recall time. Treats any spy loss as a confession. Carries council grudges into alliance decisions.
ErraticGains a random-walk incident that drifts ±50 and directly gates war readiness — genuinely unpredictable.
DiplomatLowers both the pact and alliance thresholds. Halves diplomat-recall time. Softens paranoia and encroachment.
Technologist / IndustrialistRemember tech theft and factory sabotage respectively about twice as long.
Militarist / ExpansionistMore willing to exploit a weaker neighbour.

Personalities are assigned per race by default, or randomised when that option is enabled. Note that several thresholds read the target's leader rather than the actor's — the hate-war threshold in particular — so it is the victim's temperament that decides how much abuse the relationship can absorb.

11The Galactic Council

The game's actual victory condition, and the one thing that can end diplomacy permanently.

Once colonised systems reach two-thirds of the galaxy, the council forms and thereafter convenes every 20 turns, with five turns' notice. Votes are weighted by population — one vote per hundred population — and the two largest empires stand as candidates. Electing a leader requires two-thirds of all votes.

How the AI votes

  1. Vote for a winning ally if that produces an alliance victory (unless xenophobic).
  2. Vote for yourself.
  3. Vote for an ally.
  4. Vote for whichever candidate you are not at war with.
  5. Abstain if either candidate is uncontacted or out of economic range.
  6. Otherwise vote for whichever candidate you have better relations with, provided they are not at war with you.

Every vote generates an incident on both candidates' embassies. Voting for someone is worth up to +25; voting against an ally is −15. Council votes are one of the larger single swings in the game, and they arrive in a block every twenty turns.

Accepting the ruling

When a leader is elected, every other empire independently decides whether to submit. Submission is automatic if you voted for them, if they hold the Orion council bonus, if you are allied to them, or if you are a pacifist. Otherwise it is a roll — roughly 90% for most leaders, 75% for erratic, 50% for xenophobic, and much lower if you are at war with them — adjusted by the relative power of the two empires.

Final war

Empires that refuse the ruling become rebels, and the entire winning bloc declares final war on them. Final war is unlike ordinary war in every respect: it carries a −200 treaty adjustment and a −200 non-decaying incident, both of which pin relations to the floor permanently. Diplomats are withdrawn for effectively forever. There is no peace offer, no negotiation, and no path back.

Meanwhile the winning bloc establishes unity among themselves — free tech sharing, no spying, relations pinned to the ceiling. The council is the moment the galaxy collapses from many bilateral relationships into exactly two blocs.

12Where diplomacy sits in the turn

Incidents are produced in four phases and consumed in one.

#PhaseDiplomatic effect
1Random eventsAssassination — the one immediate-war incident
2CouncilTwo vote incidents per voter; final war declarations
3Ship combatSkirmish, colony attacked/destroyed, attacked ally/enemy, bioweapon
4Refresh viewsFirst contact — set when any of their systems falls within your ship range
5Bombardment & invasionColony captured/invaded; peace treaties expire; alliance standings decay; spy incidents
6Assess turnThe diplomacy phase proper — see below
7AI decisionsThe military AI converts a war footing into an actual declaration
// the diplomacy phase — note the two separate full passes
for (Empire e: empires) e.completeResearch();
for (Empire e: empires) e.assessTurn();            // evaluate war prep -> generate ambient
                                                  // incidents -> recompute relations -> decay timers
ships.launchRalliedFleets();
for (Empire e: empires) e.makeDiplomaticOffers();  // the AI ladder, in empire order
for (Empire e: empires) e.acquireTradedTechs();
One ordering property to know

All empires finish assessing before any empire makes an offer, so every AI negotiates against a fully-refreshed, same-turn relations value. But offers are then processed in empire order, and an accepted offer mutates both embassies immediately — so later empires in the loop negotiate against state the earlier ones have already changed. Turn order genuinely matters.

13Quirks worth knowing

Places where the implemented system differs from what the design appears to intend.

  • "Worst incident" actually means "last one encountered". Both the war-declaration scan and the warning scan initialise their running worst-severity to zero and never update it, so the incident that gets picked is the last negative one in hash-map iteration order rather than the most severe. The choice is effectively arbitrary.
  • Military buildup incidents can essentially never fire. The severity multiplier is negative and the firepower term is non-negative, so the guard that requires a positive severity is unreachable. The AI does not, in practice, get annoyed at fleets massing on its border.
  • The "you're annoying me" refusal is unreachable. The annoyance modifier is always zero or negative and the callers roll a random percentage against it, so that decline reason never fires.
  • Two incident types have no live creator — encroachment and no-relations. The methods exist and the severity formulas are written, but nothing constructs them.
  • A preventive-war evaluation exists and is never called. It computes incoming bombard and invasion potential and is fully implemented, but no code path reaches it.
  • Several timers are one-way latches. The joint-war timer and the expansion warning level are set but never decremented anywhere; the per-turn request cap only ever ratchets upward.