Files
Battle-for-Tismo/tests/treaty_test.js

307 lines
14 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
import {
TREATY_PEACE,
TREATY_ALLIANCE,
TREATY_FREE_TRADE,
TREATY_GIVE_CITY,
TREATY_GIVE_MILITARY,
TREATY_GIVE_RESOURCES,
TREATY_GIVE_MONEY,
TREATY_JOINT_RESEARCH,
TREATY_SIGNED,
TREATY_REJECTED,
} from "../shared/data/treaties.js";
import { NEWS_TREATY } from "../shared/data/relations.js";
// Signs a treaty directly between two nations, the way a UI flow would.
function sign(state, from, to, type, payload = {}) {
const before = state.treatyProposals.length;
if (!state.requestProposeTreaty(from, to, type, payload)) return false;
const proposal = state.treatyProposals[before];
return state.requestRespondTreaty(to, proposal.id, true);
}
export class TreatyTest extends TestCase {
test_a_proposal_waits_for_the_other_side_and_makes_news() {
const state = smallState();
this.assertTrue(state.requestProposeTreaty(0, 1, TREATY_PEACE));
this.assertSize(state.treatyProposals, 1, "the offer waits for an answer");
this.assertEmpty(state.treaties, "nothing is signed yet");
const notice = state.news.filter((entry) => entry.type === NEWS_TREATY).at(-1);
this.assertEqual(notice.stage, "proposed");
// The same offer is not repeated while it is pending.
this.assertFalse(state.requestProposeTreaty(0, 1, TREATY_PEACE));
}
test_only_the_named_receiver_may_answer_and_rejection_is_recorded() {
const state = smallState(["france", "britain", "poland"]);
state.requestProposeTreaty(0, 1, TREATY_PEACE);
const proposal = state.treatyProposals[0];
this.assertFalse(state.requestRespondTreaty(2, proposal.id, true), "a third party cannot sign");
this.assertFalse(state.requestRespondTreaty(0, proposal.id, true), "nor the proposer");
this.assertTrue(state.requestRespondTreaty(1, proposal.id, false), "the receiver rejects");
this.assertEmpty(state.treatyProposals);
this.assertEmpty(state.treaties);
const notice = state.news.filter((entry) => entry.type === NEWS_TREATY).at(-1);
this.assertEqual(notice.stage, TREATY_REJECTED);
}
test_signing_records_an_active_treaty_and_a_signed_notice() {
const state = smallState();
this.assertTrue(sign(state, 0, 1, TREATY_ALLIANCE));
this.assertTrue(state.areAllied(0, 1));
this.assertTrue(state.hasTreaty(1, 0, TREATY_ALLIANCE), "symmetric lookup");
this.assertEqual(state.treaties[0].a, 0);
this.assertEqual(state.treaties[0].b, 1);
const notice = state.news.filter((entry) => entry.type === NEWS_TREATY).at(-1);
this.assertEqual(notice.stage, TREATY_SIGNED);
}
test_peace_ends_the_war_and_sends_troops_home() {
const state = smallState();
this.assertTrue(state.requestDeclareWar(0, 1));
// Put an invader deep on the enemy's soil.
const enemyTile = state._territoryByCiv.get(1)[0];
const unit = state.units.find((u) => u.civ === 0);
state._moveUnitTo(unit, { x: enemyTile.x, y: enemyTile.y });
this.assertTrue(sign(state, 0, 1, TREATY_PEACE));
this.assertFalse(state.isAtWar(0, 1), "the war is over");
const city = cityOf(state, 0);
this.assertEqual(unit.coords.x, city.coords.x, "the invader is back at a home city");
this.assertEqual(unit.coords.y, city.coords.y);
}
test_an_alliance_drags_allies_into_the_war() {
const state = smallState(["france", "britain", "poland"]);
this.assertTrue(sign(state, 1, 2, TREATY_ALLIANCE), "Britain and Poland ally");
this.assertTrue(state.requestDeclareWar(0, 1), "France attacks Britain");
this.assertTrue(state.isAtWar(0, 1), "the target is at war");
this.assertTrue(state.isAtWar(0, 2), "Poland is drawn in");
this.assertTrue(state.areAllied(1, 2), "the defenders' alliance holds");
}
test_going_to_war_with_an_ally_breaks_the_alliance() {
const state = smallState();
this.assertTrue(sign(state, 0, 1, TREATY_ALLIANCE));
this.assertTrue(state.areAllied(0, 1));
this.assertTrue(state.requestDeclareWar(0, 1));
this.assertFalse(state.areAllied(0, 1), "the alliance cannot survive the war");
this.assertTrue(state.isAtWar(0, 1));
}
test_marching_across_an_ally_does_not_declare_war() {
const state = smallState(["france", "britain", "poland"]);
this.assertTrue(sign(state, 0, 2, TREATY_ALLIANCE));
const unit = state.units.find((u) => u.civ === 0);
const goal = state.topology
.neighbours(unit.coords.x, unit.coords.y)
.find((n) => state._isLand(n) && !state.cityAt(n) && !state.unitAt(n));
this.assertNotNull(goal, "the fixture has an empty land neighbour");
state.territory.set(`${goal.x},${goal.y}`, 2);
this.assertTrue(state.requestMove(unit.id, goal), "the march into allied land is accepted");
this.assertFalse(state.isAtWar(0, 2), "crossing an ally declares no war");
this.assertTrue(state.areAllied(0, 2), "and leaves the alliance intact");
}
test_renewed_war_breaks_a_peace_treaty() {
const state = smallState();
this.assertTrue(state.requestDeclareWar(0, 1));
this.assertTrue(sign(state, 0, 1, TREATY_PEACE));
this.assertFalse(state.isAtWar(0, 1), "the peace holds");
this.assertTrue(state.hasTreaty(0, 1, TREATY_PEACE));
this.assertTrue(state.requestDeclareWar(0, 1), "going back to war");
this.assertFalse(state.hasTreaty(0, 1, TREATY_PEACE), "the peace treaty is broken");
this.assertTrue(state.isAtWar(0, 1));
}
test_free_trade_partners_are_exempt_from_tariffs() {
const state = smallState();
this.assertTrue(sign(state, 0, 1, TREATY_FREE_TRADE));
this.assertTrue(state.freeTradePartners(0).has(1));
this.assertTrue(state._tariffExempt(0, 1));
this.assertTrue(state._tariffExempt(1, 0));
this.assertFalse(state._tariffExempt(0, 2));
}
test_giving_money_moves_the_sum_between_treasuries() {
const state = smallState();
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 400_000 }));
this.assertApprox(state.getBudget(0), 600_000, 1e-6);
this.assertApprox(state.getBudget(1), 400_000, 1e-6);
this.assertFalse(
state.requestProposeTreaty(0, 1, TREATY_GIVE_MONEY, { amount: 0 }),
"a zero gift is refused"
);
}
test_a_gift_is_converted_into_the_receivers_currency_at_the_market_rate() {
const state = smallState();
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
// The receiver's currency is worth half, so its money is twice as dear.
state.setCurrencyValue(1, 0.5);
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 400_000 }));
// 400,000 of the receiver's currency is 200,000 of global value: the giver
// pays that in its own (par) money and the receiver is credited it back as
// 400,000 of its dearer currency.
this.assertApprox(state.getBudget(0), 800_000, 1e-6);
this.assertApprox(state.getBudget(1), 400_000, 1e-6);
}
test_a_gift_spends_the_banks_reserves_before_buying_the_currency() {
const state = smallState();
const code = state.currencyOf(1).code;
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
state._addReserve(0, code, 300);
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 1_000 }));
this.assertApprox(
state.getCentralBankReserves(0).get(code), 0, 1e-6,
"the bank's reserves are spent first"
);
this.assertApprox(state.getBudget(1), 1_000, 1e-6, "the receiver gets the full gift");
this.assertApprox(state.getBudget(0), 999_000, 1e-6, "the giver pays the full value");
}
test_a_gift_may_be_denominated_in_a_third_currency() {
const state = smallState(["france", "britain", "poland"]);
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
const code = state.currencyOf(2).code;
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 500_000, currency: code }));
this.assertApprox(state.getBudget(1), 500_000, 1e-6);
this.assertFalse(
state.requestProposeTreaty(0, 1, TREATY_GIVE_MONEY, { amount: 1, currency: "ZZZ" }),
"an unknown currency is refused"
);
}
test_a_war_with_the_issuer_blocks_buying_and_limits_the_gift() {
const state = smallState(["france", "britain", "poland"]);
const code = state.currencyOf(2).code;
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
state.requestDeclareWar(0, 2);
// At war with the issuing bank and holding none of its money, nothing can be
// bought, so the gift moves nothing at all.
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 1_000, currency: code }));
this.assertApprox(state.getBudget(0), 1_000_000, 1e-6, "the giver paid nothing");
this.assertApprox(state.getBudget(1), 0, 1e-6, "the receiver got nothing");
}
test_a_blocked_gift_still_moves_the_reserves_already_held() {
const state = smallState(["france", "britain", "poland"]);
const code = state.currencyOf(2).code;
state.budgets.set(0, 1_000_000);
state.budgets.set(1, 0);
state.requestDeclareWar(0, 2);
state._addReserve(0, code, 250);
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MONEY, { amount: 1_000, currency: code }));
this.assertApprox(
state.getCentralBankReserves(0).get(code), 0, 1e-6,
"the reserves in hand are still handed over"
);
this.assertApprox(state.getBudget(1), 250, 1e-6, "the receiver gets only what was held");
}
test_giving_military_transfers_units_but_never_infantry() {
const state = smallState();
const infantry = state.units.find((u) => u.civ === 0 && state.unitProto(u).infantry);
this.assertNotNull(infantry, "the fixture has an infantry unit");
this.assertFalse(
state.requestProposeTreaty(0, 1, TREATY_GIVE_MILITARY, { units: [infantry.id] }),
"infantry cannot be gifted"
);
const mobile = state.units.find((u) => u.civ === 0 && !state.unitProto(u).infantry);
// The fixture standard army is all infantry, so raise a gun to gift.
if (!mobile) {
const artillery = state.protoUnits.findIndex((p) => p.id === "artillery");
state._spawnUnit(cityOf(state, 0).coords, 0, state.protoUnits[artillery]);
}
const gift = state.units.find((u) => u.civ === 0 && !state.unitProto(u).infantry);
this.assertNotNull(gift, "a non-infantry unit is available");
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_MILITARY, { units: [gift.id] }));
this.assertEqual(state.findUnit(gift.id).civ, 1, "the unit changed hands");
}
test_giving_a_city_transfers_it_and_its_land() {
const state = smallState();
const city = cityOf(state, 0);
const cityKey = key(city.coords.x, city.coords.y);
this.assertFalse(
state.requestProposeTreaty(0, 1, TREATY_GIVE_CITY, { city: 99999 }),
"an unknown city is refused"
);
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_CITY, { city: city.id }));
this.assertEqual(state.findCity(city.id).civ, 1, "the city changed hands");
this.assertEqual(state.territory.get(cityKey), 1, "its tile went with it");
}
test_giving_resources_moves_the_basket_to_the_receiver() {
const state = smallState();
const giver = cityOf(state, 0);
const receiver = cityOf(state, 1);
const giverStore = state.getCityResourceStock(giver);
giverStore.steel = 1000;
giverStore.food = 500;
const receiverStore = state.getCityResourceStock(receiver);
const receiverSteel = receiverStore.steel || 0;
const receiverFood = receiverStore.food || 0;
this.assertTrue(sign(state, 0, 1, TREATY_GIVE_RESOURCES, {
resources: { steel: 300, food: 200 },
}));
this.assertApprox(giverStore.steel, 700, 1e-6, "the steel left the giver");
this.assertApprox(giverStore.food, 300, 1e-6, "and so did the food");
this.assertApprox(receiverStore.steel, receiverSteel + 300, 1e-6, "the receiver got the steel");
this.assertApprox(receiverStore.food, receiverFood + 200, 1e-6, "and the food");
}
test_joint_research_shares_points_but_not_military_ones() {
const state = smallState();
this.assertTrue(sign(state, 0, 1, TREATY_JOINT_RESEARCH));
const civil = state.technologies.findIndex((t) => t.id === "renewable_energy");
const military = state.technologies.findIndex((t) => t.id === "body_armour");
state.focusTechnology.set(0, civil);
state.focusTechnology.set(1, civil);
state._addResearch(0, 5);
this.assertApprox(state.getTechnologyProgress(1, civil), 5, 1e-6, "the partner shares the points");
// A military technology is not shared.
state.focusTechnology.set(0, military);
state.focusTechnology.set(1, civil);
const before = state.getTechnologyProgress(1, civil);
state._addResearch(0, 7);
this.assertApprox(
state.getTechnologyProgress(1, civil),
before,
1e-6,
"military research stays private"
);
}
test_ending_a_treaty_moves_it_to_the_history() {
const state = smallState();
this.assertTrue(sign(state, 0, 1, TREATY_ALLIANCE));
const treaty = state.treaties[0];
this.assertTrue(state.requestCancelTreaty(1, treaty.id), "either party may end it");
this.assertEmpty(state.treaties);
this.assertSize(state.treatyHistory, 1);
this.assertEqual(state.treatyHistory[0].reason, "broken");
this.assertFalse(state.areAllied(0, 1));
}
test_the_snapshot_ships_treaties_proposals_and_history() {
const state = smallState();
state.requestProposeTreaty(0, 1, TREATY_PEACE);
const snapshot = state.snapshot(0);
this.assertSize(snapshot.treatyProposals, 1);
this.assertEqual(snapshot.treatyProposals[0].from, 0);
this.assertEqual(snapshot.treatyProposals[0].type, TREATY_PEACE);
this.assertEmpty(snapshot.treaties);
this.assertEmpty(snapshot.treatyHistory);
}
}