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