01Why a council at all
A 4X game needs a way to end that isn't "kill everyone." ROTP has exactly one.
Conquest victory in ROTP is unforgiving: you win militarily only when every other empire is literally extinct — not reduced, not vassalised, not down to one world. There is no surrender, no capitulation, no capital-capture rule. Left alone, a 4X built that way turns into a two-hour mop-up of undefended one-planet remnants.
The council is the answer. It is a scheduled, galaxy-wide referendum in which every surviving empire spends votes proportional to its population on one of the two largest empires. Win two-thirds of the galaxy's vote weight and the game stops — you have won without firing a shot at the last holdout.
What makes it load-bearing rather than decorative is that the council reads directly from three other subsystems and nothing else. Population comes from colonies. Willingness to vote for you comes from the diplomacy relations scalar and the treaty state built on top of it. The tiebreaking bonuses come from races (the Human council bonus) and from the map (owning Orion). The council does no modelling of its own; it is a scoreboard for everything else, cashed out on a fixed schedule.
Because the council is the only non-genocidal ending, and because it counts population rather than territory or fleet strength, ROTP quietly makes growing tall and staying liked a complete strategy. A player who never wins a battle can win the game. That is unusual for a 4X of this era, and it is why the diplomacy system has teeth: relations are not flavour, they are votes.
02When the council forms, and how often it sits
Not a year. A colonization threshold — and then a metronome.
There is no "the council convenes in 2450" rule. Each turn, while the council is still inactive, checkFormation() counts colonized systems:
// GalacticCouncil.checkFormation()
int limit = (int) Math.ceil(gal.numStarSystems() * pctRequired());
float colonized = 0;
for (int i=0; i<gal.numStarSystems(); i++) {
StarSystem sys = gal.system(i);
if (sys.isColonized())
colonized++;
}
if (colonized >= limit) {
currentStatus = ACTIVE;
schedule();
}
pctRequired() defaults to 2/3. So the council forms the turn two-thirds of the galaxy's stars have been settled — which ties the endgame clock to the pace of expansion rather than to a calendar. A cramped small galaxy reaches it fast; a sprawling huge galaxy with a slow start may run a hundred extra years before anyone is asked to vote.
Once formed, the cadence is two constants:
noticeDuration — turns between the GNN announcement and the first sessioninterval — turns of quiet after a deadlocked sessionEach turn nextTurn() decrements a countdown and steps a tiny three-state machine — CHECK → SCHEDULE → CONVENE — with three guards in front of it:
options().noGalacticCouncil()— the council can be switched off entirely at setup, which makes conquest the only ending.galaxy().numActiveEmpires() < 3— a two-empire galaxy never votes.disbanded()— once ended, permanently ended.
Disbanding
Every turn, checkIfDisband() runs from the galaxy's post-turn pass: if the council is active and fewer than three empires remain, it shuts down for good (with a GNN bulletin when exactly two are left). This is the rule that makes endgame duels always military. Grind the galaxy down to you and one rival and the diplomatic exit disappears — you now have to finish the job.
The option that sets the vote threshold, counciRequiredPct, is the same value used as the colonization fraction in checkFormation(). Lowering "percentage needed to win the council vote" also makes the council appear earlier in the game. The setting's description says nothing about this. (The option name is also missing its 'l'.)
03Candidates and vote weight
Population, rounded up, divided by a hundred.
When a session opens, every active empire is sorted by total planetary population, descending. The top two become the candidates — there is no nomination, no campaigning, no way to decline:
Collections.sort(empires, Empire.TOTAL_POPULATION); ... candidate1 = empires.get(0); candidate2 = empires.get(1);
Then every empire, candidates included, is assigned votes:
votes[i] = (int) Math.ceil(voter.totalPlanetaryPopulation() / 100);
Three details matter here:
ceil, notfloor. Any surviving empire, however small, gets at least one vote. A dying two-planet remnant is still a voice in the chamber — and in a close vote, a kingmaker.- Planetary population only.
totalPlanetaryPopulation()sums colony population across colonized systems. Population riding in transports is not counted (the separatetotalEmpirePopulation()does count it, and is not the method used here). Mass-shipping colonists on the turn the council sits costs you votes. - The total is not fixed.
totalVotesis recomputed every session from that turn's populations, and the winning threshold is derived from it. There is no fixed senate of N seats.
Voting order is descending population by default, so the giants commit first and the small empires watch the tally build — a setup option lets the human vote first or last instead. Ties in the population sort are not broken explicitly; Java's stable sort just leaves equal empires in empire-id order.
04How an AI decides its vote
Four hard rules, then one probabilistic roll built from five terms.
This is the function the whole diplomacy system feeds into. In the default AI it is entirely deterministic until the last step:
// AIDiplomat.councilVoteFor(civ1, civ2) — hard rules if (civ1 == empire) return castVoteFor(civ1); // always vote for yourself if (civ2 == empire) return castVoteFor(civ2); if (cv1.embassy().alliance() && !cv2.embassy().alliance()) return castVoteFor(civ1); if (cv2.embassy().alliance() && !cv1.embassy().alliance()) return castVoteFor(civ2); if (cv1.embassy().anyWar() && !cv2.embassy().anyWar()) { if (cv2.embassy().contact()) return castVoteFor(civ2); else return castVoteFor(null); // abstain }
So: an alliance is an unconditional vote, and a war is an unconditional vote for the other guy — unless you have never met the other guy, in which case you abstain rather than endorse a stranger. Everything else falls through to a weighted coin flip, evaluated for candidate 1 first and candidate 2 second:
pct = cv1.embassy().relations()/100.0f
+ civ1.councilBonus()
+ civ1.orionCouncilBonus()
+ previousVoteBonus(civ1)
+ powerBonus1;
if (random() <= Math.abs(pct)) {
if (pct > 0)
return castVoteFor(civ1);
}
// ... same for civ2, then:
return castVoteFor(null); // undecided → abstain
| Term | Range | Where it comes from |
|---|---|---|
relations()/100 | −1.00 … +1.00 | The diplomacy relations scalar, clamped to ±100. The dominant term, and the one every treaty, gift, incident and betrayal has been quietly adjusting all game. |
councilBonus() | 0 … +0.20 | A race trait, from the 4th field of diplomod in definition.txt, divided by 100. Humans have 0.20. Every other classic race has 0.00. |
orionCouncilBonus() | 0 or +0.20 | Flat 0.2 if you own the Orion system. The map's single most politically valuable star. |
previousVoteBonus() | 0 or +0.60 | 0.6 if this empire voted for the same candidate at the last council. Incumbency is the largest single bonus in the formula. |
powerBonus | 0 … +1.00 | The candidate's share of galaxy-wide military + industrial power, as seen through this empire's spies. Bandwagoning. |
Read together, the shape is clear. A candidate at neutral relations (0.0) with a quarter of the galaxy's power (0.25) gets a one-in-four vote. Add last council's endorsement (+0.6) and it's a lock. Add Orion (+0.2) and being Human (+0.2) and pct exceeds 1.0 — the roll can no longer fail.
The previousVoteBonus of 0.6 means the first council session is not really about winning — it is about establishing a bloc. Every empire that votes for you once becomes dramatically more likely to vote for you again 25 years later. The first vote is the expensive one; subsequent ones compound. A player who understands this front-loads gifts and tech trades into the years immediately before the first session, not the last.
Notably absent from this function: leader personality, diplomat traits, empire size, and any awareness of whether the vote would actually elect someone. AIs do not vote tactically to deny a majority — an unfinished helper for exactly that (nextVoteWouldElect) sits commented out in the source with the note "Never used not updated for new options!"
05The threshold, abstention, and deadlock
Abstaining is not neutral. It is a vote against everybody.
public int votesToElect() {
return (int) Math.ceil(totalVotes * pctRequired()); // default 2/3
}
The critical detail is the denominator. totalVotes is fixed when the session opens, from every empire's population — including the empires that will go on to abstain. Abstained weight is simply never added to either tally. So:
If empires holding more than one-third of the galaxy's vote weight abstain, the council mathematically cannot elect anyone, no matter how the rest vote. Abstention is the galaxy's veto, and it is the default outcome of "I have no strong feelings and I've met neither of you well." A fragmented galaxy of lukewarm mid-sized empires deadlocks forever.
When no one clears the bar, the session closes, incidents are filed (below), and the council is rescheduled 25 turns out. Nothing else changes. The game continues.
The tie
The election test is not symmetric:
if (votes1 > votes2) { // both can be above the threshold!
if (votes1 >= minVotes) { leader = candidate1; rebelLeader = candidate2; }
}
else if (votes2 >= minVotes) { leader = candidate2; rebelLeader = candidate1; }
On an exact tie the first branch is false, so control falls to the else if — and candidate 2, the smaller of the two empires, wins. The vote-total panel in the UI uses the opposite comparison (votes1 >= votes2 && votes1 >= votesToElect) and highlights candidate 1 as the winner in exactly that case. Model and display disagree on ties.
06The vote is remembered — and resented
A deadlocked council fires ten years of diplomatic fallout.
If nobody is elected, each voter earns an incident in both candidates' ledgers. CouncilVoteIncident lasts 10 years and its severity depends on the relationship that already existed:
| Candidate's relationship to the voter | Voted FOR | Abstained | Voted AGAINST |
|---|---|---|---|
| Allied with the voter | +5 | −5 | −15 |
| At war with the voter | +25 | 0 | −5 |
| Neutral / any other state | +25 | 0 | −15 |
Voting for yourself scores zero, and a candidate never files an incident about their own vote. The asymmetries are all deliberate and all sharp:
- Backing an enemy is worth five times backing an ally (+25 vs +5). Supporting someone you are already allied with is expected; crossing the floor to support a rival you are at war with is a genuine olive branch, and the ledger says so.
- Abstaining is only punished by allies (−5). To everyone else, staying out of it is free. This is what makes the blocking third stable: the safe play carries no diplomatic cost.
- Voting against an ally (−15) hurts as much as voting against a stranger, and considerably more than voting against someone you are at war with (−5), who expected it anyway.
Because these incidents fire only in the no-winner branch, they exist purely to shape the next council. A deadlocked session redraws the relations map, which redraws the next vote, which — with the 0.6 incumbency bonus layered on top — is why councils tend to deadlock twice and then resolve decisively.
07Elected — and then everyone gets a choice
The vote does not end the game. The submission does.
Winning the vote makes you Emperor on paper only. The moment a leader is declared, the code does something counterintuitive:
if (leader != null) {
rebels.addAll(allVoters); // EVERYONE starts as a rebel
for (Empire c : allVoters)
c.diplomatAI().acceptCouncilRuling(this);
return;
}
Every empire in the galaxy is placed in the rebel list, and then each is asked, one at a time, whether to submit. Submitting moves you from rebels to allies. Defying leaves you where you are. Legitimacy is opt-in.
What an AI does when asked
An elected AI always accepts its own coronation. Otherwise it runs giveLoyaltyTo() — five unconditional yes-rules, then a personality-weighted roll:
float powerBonus = 0.5f * (empire.powerLevel(c) / (empire.powerLevel(c) + empire.powerLevel(empire)) - 0.5f); // ranges −0.25 (I dwarf him) … +0.25 (he dwarfs me) if (empire.lastCouncilVoteEmpId() == c.id) return true; // I voted for him if (c.orionCouncilBonus() > 0) return true; // he holds Orion if (cv1.embassy().alliance()) return true; if (empire.leader().isPacifist()) return true; if (cv1.embassy().pact() && empire.leader().isHonorable()) return true; if (cv1.embassy().anyWar()) { if (empire.leader().isXenophobic()) return random() < powerBonus; else if (empire.leader().isAggressive()) return random() < 0.50f + powerBonus; else return random() < 0.75f + powerBonus; } if (empire.leader().isXenophobic()) return random() < 0.50f + powerBonus; else if (empire.leader().isErratic()) return random() < 0.75f + powerBonus; return random() < 0.90f + powerBonus;
A peaceful galaxy submits almost automatically — 90% base, plus up to 25% more if the new emperor towers over you. A xenophobe you are already at war with essentially never submits: their acceptance chance is powerBonus alone, which is at most 0.25 and is negative whenever they are the stronger party.
This is where the leader-personality system finally pays off. Personality plays no part in voting — it decides only whether you accept the result. The council counts populations; the war that follows is decided by temperament.
The human player is always offered the accept/defy prompt — including when the player is the empire that just won. Reject your own election and the "crazy leader" branch fires: leadership transfers to the runner-up, every other rebel is conscripted into the loyalist camp, and you become the sole rebel against a unified galaxy. It is the hardest start position in the game and it is entirely opt-in.
When the game ends right here
| Situation | Result |
|---|---|
| Player elected, nobody defies | WIN_DIPLOMATIC — the clean diplomatic victory |
| Player elected, "immediate council win" or "Realms Beyond" option set | WIN_DIPLOMATIC — rebellion is disallowed |
| Player accepts an AI's rule, while allied to that AI | WIN_COUNCIL_ALLIANCE — junior partner in the new order |
| Player accepts an AI's rule with no alliance | LOSS_DIPLOMATIC — submission |
| Player defies, or player is leader with ≥1 AI rebel | Game continues → Final War |
That third row is the subtle one. Bowing to the winner is scored a win if you were already their ally when the vote closed, and a loss if you weren't. The mechanical difference is one treaty. It is the game's clearest statement that being second in a coalition you helped build is a legitimate way to finish.
08The Final War
Everything the game has been carefully keeping separate gets fused into two blocs — one of which is not actually a bloc.
Six things happen the moment the last empire has answered and at least one has defied:
- A permanent incident.
FinalWarIncidentis filed by every loyalist against every rebel withseverity = -200andduration = 9999999. Ordinary diplomacy is over; incident processing short-circuits once final war is declared. - War declared, both directions.
declareFinalWar()installs aTreatyFinalWaron both sides of every loyalist–rebel pair and stops the trade routes between them. There is no peace treaty that ends it. - Unity among loyalists. Every pair of allies gets
establishUnity()— aTreatyUnity, forced mutual contact, war preparations cancelled, full star-system intelligence shared, and spying zeroed out (max spies set to 0, allocation set to 0, active spies cleared). - Tech pooled upward. Every ally gives the leader every technology the leader lacks.
- Tech redistributed downward. The leader then gives every ally everything they lack — which, after step 4, is the union of the entire coalition's research.
- Rebels get nothing. No unity loop runs over the rebel list. No shared tech, no shared maps, no treaties with each other, no coordination.
The New Republic is not an alliance — it is functionally a single empire with the combined tech tree of everyone in it, perfect mutual intelligence, and no internal espionage overhead. The rebels are just a list of empires that all said no. Two rebels are not allies; they may even still be at war with each other. Defying the council is therefore never a coalition play. It is a bet that you personally can outfight a fused galaxy.
How it resolves
Once finalWar() is true — which is simply "the rebel list is non-empty" — extinction stops being evaluated by the normal military rules and routes through GalacticCouncil.removeEmpire() instead. The two win conditions are set-emptiness:
- Rebel list empties → the New Republic wins. Every rebel must be extinct, not merely beaten.
- Ally list empties → the rebellion wins. Every loyalist including the emperor must be extinct.
The precise ending you get depends on which side you were on and which role you held, producing five distinct statuses: WIN_NEW_REPUBLIC, WIN_REBELLION, WIN_REBELLION_ALLIANCE, LOSS_NEW_REPUBLIC, LOSS_REBELLION. Losing in a final war still means literal extinction of your empire — there is no "you lost the war" state short of that.
09Every way the game ends
Thirteen statuses, five ways to reach them.
| Status | Trigger | Detected in |
|---|---|---|
| WIN_MILITARY | An AI goes extinct and exactly one empire is left standing. Literal galactic genocide — no surrender, no threshold. | Empire.goExtinct() |
| WIN_MILITARY_ALLIANCE | Every surviving empire is allied (or in unity) with the player. Checked both when an AI dies and at the moment an alliance is signed. | goExtinct(), DiplomaticEmbassy |
| WIN_DIPLOMATIC | Player elected and no one defies (or rebellion is disabled by option). | GalacticCouncil.end() |
| WIN_COUNCIL_ALLIANCE | Player accepts an AI's election while allied to that AI. | GalacticCouncil.end() |
| WIN_NEW_REPUBLIC | Player was elected, was defied, and exterminated every rebel. | removeEmpire() |
| WIN_REBELLION / …_ALLIANCE | Player defied and outlived the entire loyalist bloc. The two variants distinguish a lone survivor from several surviving rebels. | removeEmpire() |
| LOSS_MILITARY | Player's last colony taken by an enemy. | goExtinct() |
| LOSS_NO_COLONIES | Player's last colony lost with no attacker — abandonment or self-destruction. | goExtinct() |
| LOSS_DIPLOMATIC | Player accepts a council ruling without an alliance to the winner. | GalacticCouncil.end() |
| LOSS_NEW_REPUBLIC | Player was the elected emperor and was exterminated anyway. | removeEmpire() |
| LOSS_REBELLION | Player defied and was exterminated. | removeEmpire() |
| LOSS_OVERTHROWN | Internal revolt deposes the player. Never fires — see quirks. | — |
Notice what is not in this list. There is no economic collapse, no morale defeat, no timeout, no "galaxy exhausted", no score threshold, no cultural or research victory. ROTP recognises exactly two shapes of ending: everyone else is dead, or everyone else agrees.
10How the player actually loses
Extinction means zero colonies, and only zero colonies.
Every military ending in ROTP funnels through Empire.goExtinct(), and that is reached from exactly three places, all of them "your colony count hit zero":
removeColonizedSystem()— the list of colonized systems became empty.Empire.nextTurn()—allColonies.isEmpty() && transports.isEmpty(). Note the conjunction: colonists in flight keep a colony-less empire alive for as long as they are in transit. Losing your last world with a transport already launched at an empty planet is survivable.- Post-invasion, in
Colony: if the defender now has zero colonies, they go extinct immediately.
The distinction between LOSS_MILITARY and LOSS_NO_COLONIES is a single field — whether lastAttacker is null. Die to an invasion and it's a military defeat; abandon or bomb your own last world and the game records it as something else entirely.
The one ending shape the design gestures at but never delivers is internal collapse. Empire.inRevolt() exists and works — it returns true when rebelling population exceeds loyal population — and overthrowLeader() genuinely fires for AI empires, installing a new Leader and breaking every treaty. For the player it is disabled at the first line:
public void overthrowLeader() {
if (isPlayer()) {
//session().status().loseOverthrown();
return;
}
leader = new Leader(this);
// ... AI empires get a new leader and lose all treaties
So rebellion in your own empire is purely an economic problem — lost production, lost votes — and never a lose condition. The LOSS_OVERTHROWN status is annotated in the source with the comment "Never happen!".
11No score, no clock
You win or you don't. Nothing is measured.
ROTP computes no end-of-game number of any kind. There is no points total, no ranking of the final empires, no efficiency grade. The ending screen renders a title and some prose chosen from the status enum and two flavour flags, and offers exit or replay.
The two flavour flags are worth knowing because they change the text you get:
- Player strength, derived from your share of the council's votes: below 50% is "minor", above 2/3 is "feared", in between is "major".
- Player role: were you the leading candidate, the challenger, or neither.
The only ranking in the game is journalism. Every 25 years GNN publishes a league table — but it picks one of four categories at random (population, production, average tech level, fleet size) and prints the standings. It affects nothing.
There is likewise no turn limit in the base design. Rotp-Fusion adds an optional one — a chosen end-of-game turn (default 200) and a condition: convene a final council where the threshold is dropped to zero so someone must win, or simply crown the top empire by population, tech-and-industry, or total power. It is a mod-layer bolt-on to a game that was designed to run until it resolves.
12Why this design works
Four properties that make a twenty-line vote carry the whole endgame.
It converts every subsystem into one currency
Population is the vote. Population comes from colonies, which come from planetology and terraforming, which come from the tech tree, which is protected by ships and accelerated by espionage. Relations decide whether that population's votes go to you, and relations are the accumulated ledger of every diplomatic act. The council doesn't add a new game — it prices the one you have been playing.
It is a scheduled crisis, not an ambient state
Because the council sits every 25 turns and not otherwise, the game acquires a rhythm: a long build phase, then a sharp, dated moment where all positions are marked to market. Players plan toward the date. The 0.6 incumbency bonus makes each session mortgage the next. That is a much better dramatic shape than a continuously-visible victory bar.
The loser gets agency
The single best decision in this system is that losing a vote does not lose the game. You are asked. Submitting ends things quietly; defying starts the most lopsided war the engine can construct. It turns a defeat screen into a choice with real, terrible content, and it means an AI landslide never simply deletes your run.
The punishment is proportional and legible
The Final War's design — unity, pooled tech, shared maps, zero espionage on one side, and nothing at all on the other — is exactly what "you defied the galaxy" should feel like mechanically. It is not a difficulty multiplier bolted on; it is the natural consequence of everyone else agreeing, expressed in the same treaty and tech-trade systems the player has used all game.
Because abstention counts against the two-thirds threshold and costs nothing diplomatically, a galaxy of mid-sized empires with lukewarm relations can deadlock indefinitely — session after session with no winner and no consequence beyond a few incidents. The design's escape hatch is that failing to elect anyone is itself pressure: nothing has been resolved, everyone keeps building, and eventually someone grows large enough to clear the bar alone. But it is the one place where the endgame can stall rather than escalate.
13Quirks, dead code, and things that look wrong
Verified against the source, not inferred.
| What | Detail |
|---|---|
| One option, two meanings | counciRequiredPct (sic) sets both the election threshold and the galaxy colonization fraction that forms the council. Lowering the vote requirement makes the council convene earlier. The setting text mentions only the vote. |
| Ties favour the smaller empire | closeConvention()'s if (votes1 > votes2) … else if (votes2 >= minVotes) hands an exact tie to candidate 2. The UI's vote-total panel uses votes1 >= votes2 and highlights candidate 1 in the same situation. |
| A vestigial absolute value | The AI vote does if (random() <= Math.abs(pct)) { if (pct > 0) … }. For any negative pct both branches are no-ops, so the abs() can only burn a random roll. It is the residue of an older version that voted for the other candidate on a negative check. |
| Non-candidates are reported as the leader | playerIsNonCandidate is declared but never assigned anywhere; playerStatus defaults to playerIsLeader (0). A player who was neither candidate and accepted the ruling gets the leader's ending text, and every "as follower" ending title is unreachable. |
| A setter with the wrong enum | loseNewRepublicAsAllied() assigns Status.LOSS_MILITARY rather than LOSS_NEW_REPUBLIC, making its dedicated ending text dead. The call site is itself annotated "Should not happen!". |
LOSS_OVERTHROWN never fires |
Its only call is commented out inside overthrowLeader()'s player guard. GameStatus labels the setter "// Never happen!". |
| Two New Republic wins are unreachable | winNewRepublicAsAllied() and winNewRepublicAsChampion() are both marked "Not reachable yet" at the branch and at the accessor — accepting a ruling as a non-leader already ended the game before they could fire. |
| "Immediate council win" is one-sided | The option is only consulted inside if (leader.isPlayer()). If an AI is elected, the player can still reject and start a final war. Only the "Realms Beyond" option actually removes the reject button. |
| The turn limit outranks "no council" | isForcedEndOfGame() is tested at the very top of nextTurn(), ahead of the noGalacticCouncil(), three-empire and disbanded() guards. Turning the council off does not disable the turn-limited endgame, and the forced path's empires.get(1) can throw when only one empire remains. |
| A turn-limited win by an AI writes no status | On the direct forced-end path the ally and rebel lists are both empty, so end() matches no win or loss branch and falls through loops that iterate over nothing. No ending is recorded and the block re-runs every subsequent turn. |
| Tactical vote-denial was cut | nextVoteWouldElect() is commented out with "Never used not updated for new options!", along with its only caller in all four classic diplomat AIs. No AI ever withholds a vote to block a majority. |
Convention state is transient |
Voters, vote tallies, candidates and the player's vote ratio are all transient, while the leader, rebel and ally lists persist. acceptRuling() and defyRuling() mutate the transient list directly, so a save taken while the accept/defy prompt is open loses the tallies. |