Unit orders now gather steel and high-tech over the days before training, exactly as buildings do, and clicking a queued order reveals what it is gathering, its reserved budget and the soldiers it will draw. The trade graph cached a producer-less node list under a stamp that already reflected the producers' stores, so every seeded mine stayed invisible to buyers and rare-good prices settled at a huge multiple of base; the storage nodes are now re-read after production. Money gifts draw the giver's central-bank reserves in the denomination first, buy the rest from the issuing bank at the market rate, convert into the receiver's currency, and cannot buy from a bank their nation is at war with. Pruned the finished design briefs (intelligence, stacks, air movement, politics performance, economy balance, training UI and FIXME) and refreshed the index and cross-references.
618 lines
24 KiB
JavaScript
618 lines
24 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState, cityOf } from "./framework/helpers.js";
|
|
import { key } from "../shared/hex.js";
|
|
import { tileImprovementById, RESOURCE_RULES } from "../shared/data.js";
|
|
import { protoUpkeep } from "../shared/resources.js";
|
|
|
|
const BIG_BUDGET = 1.0e15;
|
|
|
|
// A bare owned land tile with no improvement and no work already on it.
|
|
function bareOwnedLand(state, civ = 0) {
|
|
return state.landCells.find((coords) => {
|
|
const k = key(coords.x, coords.y);
|
|
return state.civAt(coords) === civ &&
|
|
!state.roads.has(k) &&
|
|
!state.railways.has(k) &&
|
|
!state.constructionSiteAt(coords);
|
|
});
|
|
}
|
|
|
|
// Makes `id` the only tile improvement in the world, so the works-upkeep math
|
|
// is not disturbed by the producers the world opens with.
|
|
function soleWork(state, coords, id) {
|
|
state.tileImprovements.clear();
|
|
state.tileImprovementOwner.clear();
|
|
state.tileImprovementHp.clear();
|
|
if (state.buildingAgents) state.buildingAgents.clear();
|
|
const k = key(coords.x, coords.y);
|
|
state.tileImprovements.set(k, id);
|
|
state.tileImprovementOwner.set(k, 0);
|
|
return k;
|
|
}
|
|
|
|
// Registers a construction site literal, so the site tick can be exercised
|
|
// without a full build order.
|
|
function makeSite(state, coords, overrides = {}) {
|
|
const k = key(coords.x, coords.y);
|
|
const site = {
|
|
coords: { x: coords.x, y: coords.y },
|
|
id: "fortifications",
|
|
civ: 0,
|
|
kind: "build",
|
|
targetLevel: 0,
|
|
phase: "materials",
|
|
budget: 0,
|
|
prices: state._lockedUnitPrices(),
|
|
needed: { steel: 0, hightech: 0, energy: 0 },
|
|
bought: { steel: 0, hightech: 0, energy: 0 },
|
|
elapsedHours: 0,
|
|
totalHours: 10,
|
|
cityId: null,
|
|
path: [],
|
|
stalled: false,
|
|
budgetRaises: 0,
|
|
lastBuyDay: -1,
|
|
...overrides,
|
|
};
|
|
state.constructionSites.set(k, site);
|
|
return site;
|
|
}
|
|
|
|
// Empties every reachable store, so a site with no stock of its own buys
|
|
// nothing from the market either.
|
|
function clearMaterials(state) {
|
|
for (const city of state.cities) {
|
|
const stock = state.getCityResourceStock(city);
|
|
stock.steel = 0;
|
|
stock.hightech = 0;
|
|
stock.energy = 0;
|
|
}
|
|
if (state.resourceStock) {
|
|
for (const stock of state.resourceStock.values()) {
|
|
stock.steel = 0;
|
|
stock.hightech = 0;
|
|
stock.energy = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ConstructionSiteTest extends TestCase {
|
|
test_an_order_opens_a_site_without_raising_the_improvement() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
for (const city of state.cities) {
|
|
if (city.civ !== 0) continue;
|
|
const stock = state.getCityResourceStock(city);
|
|
stock.steel = 1e9;
|
|
stock.hightech = 1e6;
|
|
stock.energy = 1e9;
|
|
}
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const site = state.constructionSiteAt(coords);
|
|
this.assertNotNull(site, "a site is opened on the tile");
|
|
this.assertEqual(site.civ, 0);
|
|
this.assertEqual(site.id, "road");
|
|
this.assertFalse(state.roads.has(key(coords.x, coords.y)), "nothing raised yet");
|
|
// The locked prices are recorded from the market when the work was decided.
|
|
this.assertApprox(site.prices.steel, state._marketUnitCost("steel"), 1e-9);
|
|
this.assertNotNull(site.cityId, "an allied city was found to haul to");
|
|
}
|
|
|
|
test_a_site_does_not_freeze_a_price_when_decided() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const site = state.constructionSiteAt(coords);
|
|
const before = state._marketUnitCost("steel");
|
|
state.setResourcePrice("steel", state.getResourcePrice("steel") * 5);
|
|
this.assertTrue(state._marketUnitCost("steel") > before, "the market moved");
|
|
// The stored figure is only a running quote; the next gather refreshes it.
|
|
state._gatherSiteMaterials(site);
|
|
this.assertApprox(
|
|
site.prices.steel,
|
|
state._marketUnitCost("steel"),
|
|
1e-6,
|
|
"the site follows the market"
|
|
);
|
|
}
|
|
|
|
test_a_site_buys_from_its_hub_city_then_reaches_the_market() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const stock = state.getCityResourceStock(city);
|
|
stock.steel = 800;
|
|
stock.hightech = 200;
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e9,
|
|
needed: { steel: 1000, hightech: 100, energy: 0 },
|
|
});
|
|
state._gatherSiteMaterials(site);
|
|
this.assertApprox(site.bought.steel, 1000, 1e-6, "all the steel was gathered");
|
|
this.assertApprox(site.bought.hightech, 100, 1e-6, "all the high-tech was gathered");
|
|
this.assertEqual(site.phase, "construction", "gathering is done, building begins");
|
|
this.assertFalse(site.stalled);
|
|
this.assertLess(site.budget, 1e9, "the site's own budget paid for the materials");
|
|
}
|
|
|
|
test_a_site_files_what_it_bought_and_what_it_paid() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
state.getCityResourceStock(city).steel = 1000;
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e9,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
});
|
|
state._gatherSiteMaterials(site);
|
|
const report = state.budgetMonthReports(0).at(-1);
|
|
const tilebuild = report.expenses.find((entry) => entry.id === "tilebuild");
|
|
this.assertNotNull(tilebuild, "the tile-improvement expense is recorded");
|
|
const steel = tilebuild.buys.find((entry) => entry[0] === "steel");
|
|
this.assertNotNull(steel, "the steel it bought is broken out");
|
|
this.assertApprox(steel[3], 100, 1e-6, "the quantity bought travels with the money");
|
|
this.assertGreater(steel[1], 0, "and so does what it paid for it");
|
|
}
|
|
|
|
test_a_day_that_buys_nothing_stalls_the_site() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
state.getCityResourceStock(city).steel = 1000;
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e9,
|
|
needed: { steel: 100000, hightech: 0, energy: 0 },
|
|
});
|
|
// The first day buys what the store held.
|
|
state._gatherSiteMaterials(site);
|
|
this.assertGreater(site.bought.steel, 0, "some steel was bought");
|
|
this.assertFalse(site.stalled, "a purchase was made");
|
|
// The next day the store is empty, so nothing is bought and the site stalls.
|
|
state._gatherSiteMaterials(site);
|
|
this.assertTrue(site.stalled, "a day without a purchase stalls the work");
|
|
this.assertEqual(site.phase, "materials", "it never reached construction");
|
|
}
|
|
|
|
test_a_stalled_site_tops_up_to_the_current_market() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 0,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
// A token of stock in the hub city keeps the material reachable, so the
|
|
// top-up applies; it is nowhere near enough to finish the work.
|
|
state.getCityResourceStock(city).steel = 1;
|
|
const treasury = 1e9;
|
|
state.budgets.set(0, treasury);
|
|
state._raiseSiteBudgetIfStalled(site);
|
|
this.assertApprox(
|
|
site.prices.steel,
|
|
state._marketUnitCost("steel"),
|
|
1e-6,
|
|
"the site prices at the live market, with no daily rise"
|
|
);
|
|
this.assertGreater(site.budget, 0, "the money needed for the rest moved into the site");
|
|
this.assertLess(state.getBudget(0), treasury, "and came from the treasury");
|
|
}
|
|
|
|
test_a_site_buys_at_the_live_market_not_a_stale_price() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 0,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
});
|
|
// A stale, cheap price does not stop the site buying at today's market.
|
|
site.prices.steel = state._marketUnitCost("steel") * 0.001;
|
|
for (const other of state.cities) state.getCityResourceStock(other).steel = 1e9;
|
|
state.budgets.set(0, 1e15);
|
|
state._gatherSiteMaterials(site);
|
|
this.assertFalse(site.stalled, "the site did not hold out for the stale price");
|
|
this.assertEqual(site.phase, "construction", "it bought at the live market");
|
|
this.assertApprox(site.bought.steel, 100, 1e-6, "all the steel was gathered");
|
|
}
|
|
|
|
test_a_site_reports_the_live_market_price() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const site = state.constructionSiteAt(coords);
|
|
const before = site.prices.steel;
|
|
state.setResourcePrice("steel", state.getResourcePrice("steel") * 5);
|
|
state._gatherSiteMaterials(site);
|
|
this.assertApprox(
|
|
site.prices.steel,
|
|
state._marketUnitCost("steel"),
|
|
1e-6,
|
|
"the site tracks the market"
|
|
);
|
|
this.assertGreater(site.prices.steel, before, "and follows it up");
|
|
}
|
|
|
|
test_a_reachable_supplier_is_bought_from_at_any_price() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 0,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
});
|
|
site.prices.steel = state._marketUnitCost("steel") * 0.2;
|
|
for (const other of state.cities) state.getCityResourceStock(other).steel = 1e9;
|
|
const nodes = state._resourceNodes();
|
|
const suppliers = state._nearbySuppliers(city, "steel", nodes);
|
|
this.assertGreater(suppliers.length, 0, "a supplier is reachable");
|
|
for (const other of state.cities) state.getCityResourceStock(other).steel = 0;
|
|
suppliers[0].stock.steel = 1e9;
|
|
state.budgets.set(0, 1e15);
|
|
state._gatherSiteMaterials(site);
|
|
this.assertEqual(site.phase, "construction", "the site bought at the market");
|
|
this.assertApprox(site.bought.steel, 100, 1e-6, "all the steel was gathered");
|
|
}
|
|
|
|
test_an_unreachable_site_does_not_waste_money_on_a_rise() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 0,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
const treasury = 1e9;
|
|
state.budgets.set(0, treasury);
|
|
const locked = site.prices.steel;
|
|
state._raiseSiteBudgetIfStalled(site);
|
|
this.assertEqual(site.stalled, true, "the work stays stalled");
|
|
this.assertApprox(site.budget, 0, 1e-6, "no money was moved");
|
|
this.assertApprox(site.prices.steel, locked, 1e-9, "and no price was raised");
|
|
this.assertApprox(state.getBudget(0), treasury, 1e-6, "the treasury is untouched");
|
|
}
|
|
|
|
test_cancelling_a_site_refunds_nothing() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const site = state.constructionSiteAt(coords);
|
|
this.assertGreater(site.budget, 0, "the site was funded");
|
|
const treasury = state.getBudget(0);
|
|
this.assertTrue(state.requestCancelSite(0, coords));
|
|
this.assertNull(state.constructionSiteAt(coords), "the site is gone");
|
|
this.assertFalse(state.roads.has(key(coords.x, coords.y)), "no improvement was raised");
|
|
this.assertApprox(state.getBudget(0), treasury, 1e-6, "the budget is not returned");
|
|
}
|
|
|
|
test_a_site_can_be_cancelled_once_construction_begins() {
|
|
const state = smallState();
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
budget: 1e6,
|
|
needed: { steel: 10, hightech: 0, energy: 0 },
|
|
bought: { steel: 10, hightech: 0, energy: 0 },
|
|
phase: "construction",
|
|
elapsedHours: 5,
|
|
totalHours: 10,
|
|
});
|
|
this.assertEqual(site.phase, "construction", "the materials are in and building has begun");
|
|
this.assertTrue(state.requestCancelSite(0, coords), "the work can still be called off");
|
|
this.assertNull(state.constructionSiteAt(coords), "the site is gone");
|
|
}
|
|
|
|
test_only_the_owner_may_cancel_a_site() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
this.assertFalse(state.requestCancelSite(1, coords), "a foreign nation cannot cancel it");
|
|
this.assertNotNull(state.constructionSiteAt(coords));
|
|
}
|
|
|
|
test_a_private_site_borrows_instead_of_touching_the_treasury() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
state.getCityResourceStock(city).steel = 1000;
|
|
const coords = state.landCells.find((cell) => {
|
|
const k = key(cell.x, cell.y);
|
|
return state.civAt(cell) === 0 && !state.tileImprovements.has(k)
|
|
&& !state.constructionSiteAt(cell);
|
|
});
|
|
const k = key(coords.x, coords.y);
|
|
const agent = state._ensureBuildingAgent(k);
|
|
agent.cash = 0;
|
|
const treasury = 5e8;
|
|
state.budgets.set(0, treasury);
|
|
const site = makeSite(state, coords, {
|
|
id: "steel_mill",
|
|
cityId: city.id,
|
|
budget: 0,
|
|
private: true,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
});
|
|
state._gatherSiteMaterials(site);
|
|
this.assertApprox(site.bought.steel, 100, 1e-6, "the agent bought the steel");
|
|
this.assertApprox(state.getBudget(0), treasury, 1e-6, "the treasury was not touched");
|
|
this.assertGreater(agent.debt, 0, "the agent borrowed the money");
|
|
const report = state.budgetMonthReports(0).at(-1);
|
|
this.assertEqual(
|
|
report.expenses.find((entry) => entry.id === "tilebuild"),
|
|
undefined,
|
|
"a private work files no public tile-improvement expense"
|
|
);
|
|
}
|
|
|
|
test_ordering_a_producer_spends_no_treasury_and_creates_its_agent() {
|
|
const state = smallState();
|
|
state.tileImprovements.clear();
|
|
state.tileImprovementOwner.clear();
|
|
state.tileImprovementHp.clear();
|
|
if (state.buildingAgents) state.buildingAgents.clear();
|
|
for (const city of state.cities) {
|
|
if (city.civ !== 0) continue;
|
|
const stock = state.getCityResourceStock(city);
|
|
stock.steel = 1e9;
|
|
stock.hightech = 1e9;
|
|
stock.energy = 1e9;
|
|
}
|
|
const treasury = 1e9;
|
|
state.budgets.set(0, treasury);
|
|
const coords = state.landCells.find(
|
|
(cell) => state.civAt(cell) === 0 && !state.cityAt(cell)
|
|
);
|
|
const k = key(coords.x, coords.y);
|
|
this.assertTrue(state.requestBuildTileImprovement(0, coords, "steel_mill"));
|
|
const agent = state.buildingAgents.get(k);
|
|
this.assertNotNull(agent, "the producer's agent opens with the order");
|
|
this.assertApprox(state.getBudget(0), treasury, 1e-6, "the treasury pays nothing");
|
|
this.assertGreater(agent.debt, 0, "the work is funded by a bank loan");
|
|
const report = state.budgetMonthReports(0).at(-1);
|
|
this.assertEqual(
|
|
report.expenses.find((entry) => entry.id === "tilebuild"),
|
|
undefined,
|
|
"nothing is filed as a public tile-improvement expense"
|
|
);
|
|
}
|
|
|
|
test_a_finished_site_keeps_its_leftover_as_improvement_cash() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
state.getCityResourceStock(city).steel = 1000;
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e6,
|
|
needed: { steel: 10, hightech: 0, energy: 0 },
|
|
phase: "construction",
|
|
totalHours: 0,
|
|
});
|
|
// Buy the small material bill first, then let the work finish.
|
|
state._gatherSiteMaterials(site);
|
|
site.phase = "construction";
|
|
site.totalHours = 0;
|
|
state._tickConstructionSites();
|
|
this.assertNull(state.constructionSiteAt(coords), "the work completed");
|
|
this.assertEqual(state.tileImprovements.get(key(coords.x, coords.y)), "fortifications");
|
|
this.assertGreater(state.improvementCashAt(coords), 0, "the leftover stays with the tile");
|
|
}
|
|
|
|
test_a_works_reserve_pays_toward_its_own_upkeep() {
|
|
const state = smallState();
|
|
const coords = bareOwnedLand(state);
|
|
const k = soleWork(state, coords, "fortifications");
|
|
state.improvementCash.set(k, 1e9);
|
|
const proto = tileImprovementById("fortifications");
|
|
const costs = protoUpkeep(proto, proto.buildCost || 0);
|
|
const categories = {
|
|
works: { steel: costs.steel, hightech: costs.hightech },
|
|
transport: { steel: 0, hightech: 0 },
|
|
};
|
|
state._applyWorksMaintenance(0, categories);
|
|
this.assertLess(state.improvementCashAt(coords), 1e9, "the reserve was spent");
|
|
this.assertApprox(categories.works.steel, 0, 1e-6, "the work covered its own steel");
|
|
this.assertEqual(categories.works.hightech, 0, "and its high-tech");
|
|
}
|
|
|
|
test_a_works_reserve_only_covers_what_its_money_reaches() {
|
|
const state = smallState();
|
|
const coords = bareOwnedLand(state);
|
|
const k = soleWork(state, coords, "fortifications");
|
|
const proto = tileImprovementById("fortifications");
|
|
const costs = protoUpkeep(proto, proto.buildCost || 0);
|
|
const unit = state._marketUnitCost("steel");
|
|
// Half the day's steel bill, so only half the wear is covered.
|
|
state.improvementCash.set(k, costs.steel * unit * 0.5);
|
|
const categories = {
|
|
works: { steel: costs.steel, hightech: costs.hightech },
|
|
transport: { steel: 0, hightech: 0 },
|
|
};
|
|
state._applyWorksMaintenance(0, categories);
|
|
this.assertApprox(state.improvementCashAt(coords), 0, 1e-6, "the reserve ran dry");
|
|
this.assertApprox(categories.works.steel, costs.steel * 0.5, 1e-6, "the rest is still owed");
|
|
}
|
|
|
|
test_a_transport_reserve_pays_its_own_steel() {
|
|
const state = smallState();
|
|
const coords = bareOwnedLand(state);
|
|
const k = key(coords.x, coords.y);
|
|
state.roads.add(k);
|
|
state.improvementCash.set(k, 1e9);
|
|
const categories = {
|
|
works: { steel: 0, hightech: 0 },
|
|
transport: { steel: RESOURCE_RULES.transportUpkeepSteel.road, hightech: 0 },
|
|
};
|
|
state._applyWorksMaintenance(0, categories);
|
|
this.assertApprox(categories.transport.steel, 0, 1e-6, "the road covered its own wear");
|
|
this.assertLess(state.improvementCashAt(coords), 1e9, "the reserve was spent");
|
|
}
|
|
|
|
test_a_stalled_site_knows_what_its_remaining_materials_cost() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1000,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
// The shortfall between the remaining materials at the site's own budgeted
|
|
// prices and the money it still holds is what a raise must move in.
|
|
const expected = 100 * site.prices.steel - 1000;
|
|
this.assertApprox(state._siteTopUp(site), expected, 1e-6, "the site's shortfall");
|
|
this.assertApprox(
|
|
state._siteTopUp(site, { ...site.prices, steel: site.prices.steel * 2 }),
|
|
expected + 100 * site.prices.steel,
|
|
1e-6,
|
|
"a higher price asks for more"
|
|
);
|
|
}
|
|
|
|
test_a_site_holding_enough_reports_no_top_up() {
|
|
const state = smallState();
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
budget: 1e12,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
this.assertEqual(state._siteTopUp(site), 0, "a funded site owes nothing");
|
|
}
|
|
|
|
test_a_site_with_no_reachable_supplier_reports_it() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e9,
|
|
needed: { steel: 100, hightech: 40, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
// Nothing within reach holds either material, so no budget can unstall it.
|
|
const unreachable = state._siteUnreachableMaterials(site).slice().sort();
|
|
this.assertEqual(unreachable.join(","), "hightech,steel", "both materials are unreachable");
|
|
const snapshot = state.snapshot(0);
|
|
const entry = (snapshot.constructionSites[0] || []).find(
|
|
(row) => row.coords[0] === coords.x && row.coords[1] === coords.y
|
|
);
|
|
this.assertEqual(
|
|
entry.unreachable.slice().sort().join(","),
|
|
"hightech,steel",
|
|
"the panel is told no supplier is in reach"
|
|
);
|
|
}
|
|
|
|
test_a_reachable_supplier_clears_the_unreachable_flag() {
|
|
const state = smallState();
|
|
clearMaterials(state);
|
|
const city = cityOf(state, 0);
|
|
const coords = bareOwnedLand(state);
|
|
const site = makeSite(state, coords, {
|
|
cityId: city.id,
|
|
budget: 1e9,
|
|
needed: { steel: 100, hightech: 0, energy: 0 },
|
|
stalled: true,
|
|
});
|
|
this.assertEqual(
|
|
state._siteUnreachableMaterials(site).join(","),
|
|
"steel",
|
|
"the empty market leaves steel unreachable"
|
|
);
|
|
state.getCityResourceStock(city).steel = 1000;
|
|
this.assertEqual(
|
|
state._siteUnreachableMaterials(site).length,
|
|
0,
|
|
"the hub city's own store counts as reachable"
|
|
);
|
|
}
|
|
|
|
test_the_snapshot_ships_the_sites_with_their_phase_and_budget() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const snapshot = state.snapshot(0);
|
|
const entry = (snapshot.constructionSites[0] || []).find(
|
|
(site) => site.coords[0] === coords.x && site.coords[1] === coords.y
|
|
);
|
|
this.assertNotNull(entry, "the site is in the snapshot");
|
|
this.assertEqual(entry.id, "road");
|
|
this.assertTrue(entry.phase === "materials" || entry.phase === "construction");
|
|
this.assertNotNull(entry.prices.steel, "the locked prices travel too");
|
|
this.assertNotNull(entry.cityId, "the haul city travels too");
|
|
this.assertEqual(entry.unreachable.length, 0, "a fresh site has no missing supplier");
|
|
this.assertEqual("topUp" in entry, false, "no manual top-up is shipped any more");
|
|
}
|
|
|
|
test_a_unit_order_gathers_its_materials_before_training() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const barracks = state.protoBuildings.findIndex((b) => b.id === "barracks");
|
|
city.buildings[barracks] = 1;
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const infantry = state.protoUnits.findIndex((p) => p.id === "modern_infantry");
|
|
const before = state.units.filter((u) => u.civ === 0 && u.proto === infantry).length;
|
|
// Nothing to buy from anywhere, so the order can only gather, never finish.
|
|
clearMaterials(state);
|
|
this.assertTrue(state.requestTrain(city.id, infantry), "the order is accepted");
|
|
const entry = state.training.get(city.id)[0];
|
|
this.assertEqual(entry.kind, "unit");
|
|
this.assertEqual(entry.phase, "materials", "a unit starts by gathering, not training");
|
|
this.assertGreater(entry.needed.steel, 0, "it names the steel it must gather");
|
|
this.assertEqual(entry.bought.steel, 0, "no steel could be bought");
|
|
this.assertGreater(entry.budget, 0, "a material budget is reserved");
|
|
// A day passes and it is still gathering, with no new unit to show for it.
|
|
for (let i = 0; i < 24; i++) state.advanceHour();
|
|
this.assertEqual(state.training.get(city.id)[0].phase, "materials", "still gathering");
|
|
this.assertEqual(
|
|
state.units.filter((u) => u.civ === 0 && u.proto === infantry).length,
|
|
before,
|
|
"no unit was trained while the materials are missing"
|
|
);
|
|
}
|
|
|
|
test_a_supplied_unit_order_gathers_then_trains() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const barracks = state.protoBuildings.findIndex((b) => b.id === "barracks");
|
|
city.buildings[barracks] = 1;
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
for (const other of state.cities) {
|
|
if (other.civ !== 0) continue;
|
|
const stock = state.getCityResourceStock(other);
|
|
stock.steel = 1e9;
|
|
stock.hightech = 1e6;
|
|
}
|
|
const infantry = state.protoUnits.findIndex((p) => p.id === "modern_infantry");
|
|
const before = state.units.filter((u) => u.civ === 0 && u.proto === infantry).length;
|
|
this.assertTrue(state.requestTrain(city.id, infantry));
|
|
for (let i = 0; i < 200000 && state.training.size > 0; i++) state.advanceHour();
|
|
this.assertFalse(state.training.has(city.id), "the order finished");
|
|
this.assertEqual(
|
|
state.units.filter((u) => u.civ === 0 && u.proto === infantry).length,
|
|
before + 1,
|
|
"the unit was trained once its materials were in"
|
|
);
|
|
}
|
|
}
|