A House Divided A House DividedDocumentation
Changelog
Game Design/Government & Executive

UK Cabinet System

Last updated 2026-08-21
Source files

Overview#

The UK Cabinet System manages cabinet appointments for the United Kingdom government. The Prime Minister appoints and dismisses eligible player-controlled MPs. Cabinet eligibility is not restricted to the governing party or coalition.

Location: src/lib/uk/

Key files:

Cabinet Positions#

Cabinet positions are defined in UK_CABINET_POSITIONS (), 18 entries in file order (agriculture_secretary and environment_secretary share order: 12 because their eras are disjoint: agriculture retires in 2001 and hands off to environment via succeededBy):

Order Position ID Name (base) Year Enabled
0 deputy_prime_minister Deputy Prime Minister 1775
1 first_secretary_of_state First Secretary of State 1962
2 chancellor Chancellor of the Exchequer 1775
3 foreign_secretary Foreign Secretary 1775
4 home_secretary Home Secretary 1775
5 defence_secretary Secretary of State for Defence 1775
6 justice_secretary Lord Chancellor & Secretary of State for Justice 1775
7 health_secretary Secretary of State for Health and Social Care 1775
8 education_secretary Secretary of State for Education 1775
9 business_secretary Secretary of State for Business, Energy and Industrial Strategy 1775
10 levelling_secretary Secretary of State for Housing, Communities and Local Government 1775
11 transport_secretary Secretary of State for Transport 1775
12 agriculture_secretary Minister of Agriculture, Fisheries and Food (retires 2001, succeeded by environment_secretary) 1775
12 environment_secretary Secretary of State for Environment, Food and Rural Affairs 2001
13 work_secretary Secretary of State for Work and Pensions 1775
14 northern_ireland Secretary of State for Northern Ireland 1972
15 scotland Secretary of State for Scotland 1775
16 wales Secretary of State for Wales 1964

Each seat's display name can also change over time within its era (namesByYear, resolved against the live game year, e.g. defence_secretary reads "Secretary of State for War" before 1964). The base names above are the seat's identity, not necessarily the label shown at every year.

Eligibility Rules#

Prime Minister Authorization#

Only the current Prime Minister can appoint or dismiss cabinet ministers. This check is enforced by requireCurrentPrimeMinister in . Despite its location, the guard is country-agnostic and is used by other parliamentary cabinets as well.

Minister Eligibility#

To be eligible for cabinet appointment:

  1. Must be a player-controlled character holding a Commons seat. Lords and NPPs are not eligible through this flow.
  2. Cannot already hold a cabinet position. A character holds at most one cabinet seat.
  3. Cannot be the Prime Minister. The PM cannot appoint themselves to an additional position.
  4. Party is not a gate. The PM may appoint any otherwise eligible Commons player, including an opposition MP.

Appointment Flow#

1. View Eligible Characters#

GET /api/country/uk/executive/cabinet/characters

Response:
{
  success: true,
  characters: Character[]  // Eligible for appointment
}

2. Appoint Minister#

POST /api/country/uk/executive/cabinet/appoint
Body: { positionId: string, characterId: string }

Authorization: requireAuth() + requireCurrentPrimeMinister(db, countryId, userId)

Checks:
1. Position exists in UK_CABINET_POSITIONS
2. Character exists and is player character
3. Character holds Commons seat
4. Character is not PM
5. Position is not already filled
6. Position is not on cooldown
7. Character doesn't already hold cabinet position

Effects:
1. Insert record into `cabinetMembers` (shared collection, `countryId: "UK"`)
2. Add career history entry to character
3. Reset advocacy toggle for territorial secretaries (NI, Scotland, Wales)

3. Dismiss Minister#

POST /api/country/uk/executive/cabinet/fire
Body: { positionId: string }

Authorization: requireAuth() + requireCurrentPrimeMinister(db, countryId, userId)

Checks:
1. Position exists
2. Position is currently filled

Effects:
1. Delete record from `cabinetMembers` (shared collection)
2. The appointment-time seat lock remains in `ukCabinetCooldowns`

Cooldown System#

Each appointment starts a 24-turn seat lock (COOLDOWN_TURNS in cabinetApi.ts). The lock survives a firing, so the PM cannot immediately refill a seat by cycling ministers. Firing itself is unrestricted.

const COOLDOWN_TURNS = 24;
// Locked for COOLDOWN_TURNS turns from this appointment/firing, keyed to turn
// length so it tracks game time regardless of wall-clock drift.
const cooldownUntilTurn = appointTurn + COOLDOWN_TURNS;
const cooldownUntil = new Date(
  now.getTime() + COOLDOWN_TURNS * turnLengthMinutes * 60_000,
);

await getUKCabinetCooldownsCollection(db).updateOne(
  { countryId: "UK", positionId },
  {
    $set: {
      countryId: "UK",
      positionId,
      appointedCharacterId: character._id,
      appointedByPmCharacterId: pmCharacter._id,
      appointedAt: now,
      cooldownUntil,
    },
  },
  { upsert: true },
);

Purpose: Prevents rapid hire/fire/re-hire cycles within the same cabinet seat.

Advocacy Toggle#

Territorial secretaries (Northern Ireland, Scotland, Wales) have an advocacy toggle that can be activated to advocate for their region. This toggle is automatically reset when a new minister is appointed:

if (["northern_ireland", "scotland", "wales"].includes(positionId)) {
  await getCabinetSettingsCollection(db).updateOne(
    { _id: `UK_${positionId}` },
    { $set: { advocacyActive: false, updatedAt: new Date() } },
  );
}

Data Model#

Collections#

Collection Purpose
cabinetMembers Current cabinet ministers (shared across all countries, keyed by countryId)
ukCabinetCooldowns Appointment-time seat locks
cabinetSettings Per-position settings (advocacy toggle)
characters Career history updates
electedOfficials Commons seat verification

Document Types#

// Shared cabinetMembers doc shape, filtered by countryId: "UK" for this system.
interface CabinetMember {
  _id: ObjectId;
  countryId: string;
  positionId: string;
  characterId: ObjectId;
  characterName: string;
  party: string;
  appointedByPmCharacterId: ObjectId;
  appointedAt: Date;
  createdAt: Date;
  updatedAt: Date;
}

interface UKCabinetCooldown {
  _id: ObjectId;
  countryId: "UK";
  positionId: string;
  appointedCharacterId: ObjectId;
  appointedByPmCharacterId: ObjectId;
  appointedAt: Date;
  cooldownUntil: Date;
  cooldownUntilTurn: number;
  createdAt: Date;
  updatedAt: Date;
}

interface CabinetSettings {
  _id: string; // "UK_{positionId}"
  advocacyActive: boolean;
  updatedAt: Date;
}

Career History#

Cabinet appointments add entries to character career history:

const cabinetOffice: OfficeType = { type: "ukCabinet", positionId };
const careerEvent: CareerEvent = {
  type: "appointed",
  office: cabinetOffice,
  officeLabel: getOfficeLabel(cabinetOffice, "UK"),
  party: commonsOfficial.party ?? targetChar.party,
  date: now,
};
await db
  .collection<Character>("characters")
  .updateOne(
    { _id: targetChar._id },
    { $push: { careerHistory: careerEvent } },
  );

Error Responses#

Status Condition
401 Not authenticated
403 Not PM, ineligible character, already holds position, not in Commons
404 Character or position not found
409 Position already filled, on cooldown