Files
Battle-for-Tismo/tests/game_state_technology_test.js
T

229 lines
10 KiB
JavaScript

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