import { TestCase } from "./framework/test_case.js"; import { smallState, stripPorts } from "./framework/helpers.js"; import { TECHNOLOGIES, EFFECT_INFANTRY_ATTACK, EFFECT_CONSTRUCTION_SPEED, EFFECT_PRODUCTION, EFFECT_RESEARCH, } from "../shared/data.js"; import { NEWS_TECHNOLOGY } from "../shared/data/relations.js"; export class GameStateTechnologyTest extends TestCase { index(state, id) { return state.technologies.findIndex((t) => t.id === id); } // Focuses `id` and pours exactly its cost of points into it, the way a nation // would over time; returns the index. complete(state, id, civ = 0) { const index = this.index(state, id); this.assertTrue(state.requestFocusTechnology(civ, index), `focusing ${id}`); state._addResearch(civ, state.getTechnologyCost(civ, index)); return index; } test_catalogue_links_prerequisites() { const root = TECHNOLOGIES.find((t) => t.id === "industrial_automation"); this.assertEmpty(root.prerequisites, "the lowest tier needs nothing"); this.assertGreater(root.cost, 0); const space = TECHNOLOGIES.find((t) => t.id === "space_program"); this.assertEqual(space.prerequisites, ["industrial_automation", "quantum_computing"]); } test_every_civilisation_starts_with_nothing_researched() { const state = smallState(); this.assertSize(state.technologies, TECHNOLOGIES.length); this.assertFalse(state.isResearched(0, 0)); this.assertEmpty(state.getResearchedIndices(0)); } test_research_points_fill_the_focused_technology() { const state = smallState(); const index = this.index(state, "quantum_computing"); this.assertTrue(state.requestFocusTechnology(0, index), "the focus is accepted"); const cost = state.getTechnologyCost(0, index); // Points flow in as they are created; just short of the cost leaves it in // progress rather than in a bank. state._addResearch(0, cost - 1); this.assertFalse(state.isResearched(0, index), "still researching"); this.assertApprox(state.getTechnologyProgress(0), cost - 1, 1e-6); state._addResearch(0, 1); this.assertTrue(state.isResearched(0, index), "the technology is discovered"); this.assertEqual(state.getFocusedTechnology(0), -1, "the focus clears"); } test_research_is_never_gated_on_the_treasury() { const state = smallState(); // A treasury deep in the red does not stop a nation focusing or accruing // research: the shortfall is central-bank debt, not a spending limit. state.budgets.set(0, -1_000_000); const index = this.index(state, "quantum_computing"); this.assertTrue(state.requestFocusTechnology(0, index), "the focus is accepted in debt"); state._addResearch(0, 5); this.assertApprox(state.getTechnologyProgress(0), 5, 1e-9, "points still accrue"); this.assertLess(state.getBudget(0), 0, "the treasury stayed in debt"); } test_switching_focus_keeps_committed_progress() { const state = smallState(); const weapons = this.index(state, "infantry_weapons"); const armour = this.index(state, "body_armour"); this.assertTrue(state.requestFocusTechnology(0, weapons)); state._addResearch(0, 7); this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9); // Switch focus: the weapons points stay committed. this.assertTrue(state.requestFocusTechnology(0, armour)); this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9, "weapons kept its work"); this.assertApprox(state.getTechnologyProgress(0, armour), 0, 1e-9); state._addResearch(0, 5); this.assertApprox(state.getTechnologyProgress(0, armour), 5, 1e-9); // Switching back resumes from the seven points already committed. this.assertTrue(state.requestFocusTechnology(0, weapons)); this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9); this.assertEqual(state.getTechnologyProgress(0), 7, "the focus reports its own progress"); this.assertApprox(state.snapshot(0).techProgress[0][weapons], 7, 1e-9, "progress is shipped"); } test_nothing_is_researched_without_a_focus() { const state = smallState(); const index = this.index(state, "quantum_computing"); // Points created with nothing focused are simply not stored anywhere. state._addResearch(0, state.getTechnologyCost(0, index) * 10); this.assertFalse(state.isResearched(0, index)); this.assertApprox(state.getTechnologyProgress(0), 0, 1e-9); } test_focus_rejects_unknown_technology_and_civilisation() { const state = smallState(); this.assertFalse(state.requestFocusTechnology(0, 999), "unknown technology"); this.assertFalse(state.requestFocusTechnology(999, 0), "unknown civilisation"); } test_focus_rejects_when_prerequisites_are_missing() { const state = smallState(); const space = this.index(state, "space_program"); this.assertFalse(state.canResearchTechnology(0, space)); this.assertFalse(state.requestFocusTechnology(0, space)); } test_prerequisites_unlock_the_tree() { const state = smallState(); const space = this.index(state, "space_program"); this.assertFalse(state.requestFocusTechnology(0, space), "locked before its prerequisites"); const automation = this.index(state, "industrial_automation"); this.complete(state, "industrial_automation"); this.assertTrue(state.isResearched(0, automation)); this.assertFalse(state.canResearchTechnology(0, space), "one prerequisite is not enough"); this.complete(state, "quantum_computing"); this.assertTrue(state.requestFocusTechnology(0, space), "both prerequisites met"); } test_technology_effects_are_folded_into_gdp() { const state = smallState(); stripPorts(state, 0); const index = this.index(state, "renewable_energy"); const added = TECHNOLOGIES[index].effects.find((e) => e.stat === EFFECT_PRODUCTION).baseValue; const base = state.getNationalModifiers(0)[EFFECT_PRODUCTION] || 0; const before = state.getPlayerGdp(0); this.complete(state, "renewable_energy"); this.assertApprox(state.getPlayerGdp(0), (before * (1 + base + added)) / (1 + base), 0.01); } test_researched_technology_adds_its_research_rate() { const withTech = smallState(); const without = smallState(); this.complete(withTech, "quantum_computing"); this.assertApprox( withTech.viewerStats(0).researchRate - without.viewerStats(0).researchRate, 4.0, 1e-9 ); } test_snapshot_exposes_technologies_and_progress() { const state = smallState(); const index = this.complete(state, "quantum_computing"); const snapshot = state.snapshot(0); this.assertSize(snapshot.technologies, state.technologies.length); this.assertSize(snapshot.researched, state.civilisations.length); this.assertTrue(snapshot.researched[0].includes(index)); this.assertEmpty(snapshot.researched[1]); } test_repeatable_research_costs_two_percent_more_each_level() { const state = smallState(); const index = this.index(state, "infantry_weapons"); const base = state.getTechnologyCost(0, index); this.assertTrue(state.requestFocusTechnology(0, index), "first level"); state._addResearch(0, base); this.assertEqual(state.getTechnologyRepeatCount(0, index), 1); this.assertApprox( state.getTechnologyCost(0, index), base * 1.02, 1e-6, "the next level costs 2% more" ); this.assertTrue(state.requestFocusTechnology(0, index), "second level"); state._addResearch(0, state.getTechnologyCost(0, index)); this.assertEqual(state.getTechnologyRepeatCount(0, index), 2); this.assertApprox(state.getTechnologyCost(0, index), base * 1.02 * 1.02, 1e-6); } test_repeatable_effects_stack_with_each_level() { const state = smallState(); const weapons = this.index(state, "infantry_weapons"); const construction = this.index(state, "construction_technology"); for (let level = 0; level < 2; level++) { state.requestFocusTechnology(0, weapons); state._addResearch(0, state.getTechnologyCost(0, weapons)); } state.requestFocusTechnology(0, construction); state._addResearch(0, state.getTechnologyCost(0, construction)); const modifiers = state.getCivModifiers(0); this.assertApprox(modifiers[EFFECT_INFANTRY_ATTACK], 0.02, 1e-9, "two levels stack"); this.assertApprox(modifiers[EFFECT_CONSTRUCTION_SPEED], 0.01, 1e-9); this.assertApprox(state.constructionSpeedMultiplier(0), 1 / 1.01, 1e-9); } test_repeatable_research_notifies_without_news() { const state = smallState(); this.complete(state, "quantum_computing"); this.complete(state, "body_armour"); const notices = state.news.filter((entry) => entry.type === NEWS_TECHNOLOGY); const normalNotice = notices.find((entry) => entry.technology === "quantum_computing"); const repeatNotice = notices.find((entry) => entry.technology === "body_armour"); this.assertNotNull(normalNotice); this.assertFalse(!!normalNotice.repeatable, "a discovery is a real news event"); this.assertNotNull(repeatNotice); this.assertTrue(repeatNotice.repeatable, "a repeat level is only a notice"); } test_repeatable_technologies_have_a_theme() { for (const proto of TECHNOLOGIES) { this.assertTrue( typeof proto.theme === "string" && proto.theme.length > 0, `${proto.id} is filed under a theme` ); } } test_completing_a_repeatable_keeps_it_focused() { const state = smallState(); const index = this.complete(state, "infantry_weapons"); this.assertEqual(state.getTechnologyRepeatCount(0, index), 1, "one level done"); this.assertEqual(state.getFocusedTechnology(0), index, "it stays focused"); // Fresh points keep flowing into the next level without another order. state._addResearch(0, state.getTechnologyCost(0, index)); this.assertEqual(state.getTechnologyRepeatCount(0, index), 2, "the next level completes"); this.assertApprox(state.getTechnologyProgress(0, index), 0, 1e-9, "reset for the next level"); } test_research_costs_a_hundred_times_the_catalogue_price() { const state = smallState(); const index = this.index(state, "quantum_computing"); const proto = TECHNOLOGIES[index]; this.assertEqual(state.getTechnologyCost(0, index), proto.cost * 100); } }