From bf71e978f2ec59c8e35e4197d1bba295882cce49 Mon Sep 17 00:00:00 2001 From: tinion Date: Sat, 24 Aug 2024 00:59:02 +0200 Subject: [PATCH 01/10] Add failing tests for SmartSupply functionality in Corporations --- test/jest/Corporation.test.ts | 186 ++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/test/jest/Corporation.test.ts b/test/jest/Corporation.test.ts index 3aa3c031aa..2b3cc81350 100644 --- a/test/jest/Corporation.test.ts +++ b/test/jest/Corporation.test.ts @@ -1,6 +1,11 @@ import { PositiveInteger } from "../../src/types"; import { Corporation } from "../../src/Corporation/Corporation"; +import { Division } from "../../src/Corporation/Division"; +import { Warehouse } from "../../src/Corporation/Warehouse"; +import { Material } from "../../src/Corporation/Material"; +import { OfficeSpace } from "../../src/Corporation/OfficeSpace"; import { CorpUpgrades } from "../../src/Corporation/data/CorporationUpgrades"; +import { CorpUnlockName, CorpEmployeeJob } from "@enums"; import { calculateMaxAffordableUpgrade, calculateUpgradeCost, @@ -138,3 +143,184 @@ describe("Corporation", () => { ); }); }); + +describe("Division", () => { + let corporation: Corporation; + let divisionChem: Division; + let divisionMining: Division; + + const city = "Sector-12"; + const name = "ChemTestDiv"; + + function setupCorporation() { + setPlayer(new PlayerObject()); + Player.init(); + const corporation = new Corporation({ name: "Test" }); + + corporation.divisions.set( + "ChemTestDiv", + new Division({ + corp: corporation, + name: "ChemTestDiv", + type: "Chemical", + }), + ); + + corporation.divisions.set( + "MiningTestDiv", + new Division({ + corp: corporation, + name: "MiningTestDiv", + type: "Mining", + }), + ); + + corporation.unlocks.add(CorpUnlockName.SmartSupply); + return corporation; + } + + function setupWarehouse(division: Division, city: string, corporation: Corporation) { + const warehouse = new Warehouse({ division, loc: city, size: 1 }); + warehouse.level = 1; + warehouse.updateSize(corporation, division); + warehouse.materials["Food"].stored = 333.33 * 5; // Fill half with filler material, so that free Warehouse space is 50 + warehouse.updateSize(corporation, division); + warehouse.updateMaterialSizeUsed(); + warehouse.smartSupplyEnabled = true; + warehouse.smartSupplyOptions["Plants"] = "leftovers"; + warehouse.smartSupplyOptions["Water"] = "leftovers"; + warehouse.smartSupplyOptions["Hardware"] = "leftovers"; + division.warehouses[city] = warehouse; + } + + beforeAll(() => { + corporation = setupCorporation(); + divisionChem = corporation.divisions.get("ChemTestDiv"); + divisionMining = corporation.divisions.get("MiningTestDiv"); + + // Improve Productivity to fill the warehouse faster + for (const div of [divisionChem, divisionMining]) { + const office = div.offices[city]; + const osize = 2e6; + office.size = osize; + for (let i = 0; i < osize / 2; i++) { + office.hireRandomEmployee("Engineer"); + office.hireRandomEmployee("Operations"); + } + office.calculateEmployeeProductivity(corporation, div); + } + }); + + beforeEach(() => { + // Simulate until START phase + while (corporation.state.nextName != "START") { + corporation.state.incrementState(); + } + + setupWarehouse(divisionChem, city, corporation); + setupWarehouse(divisionMining, city, corporation); + }); + + describe("processMaterials", () => { + describe("SmartSupply", () => { + it("should start in START phase and have SmartSupply unlocked", () => { + expect(corporation.unlocks.has(CorpUnlockName.SmartSupply)).toBe(true); + expect(corporation.state.nextName).toBe("START"); + }); + + it("should prepare warehouses correctly for the Chemical division", () => { + const warehouse = divisionChem.warehouses[city]; + expect(divisionChem.getOfficeProductivity(divisionChem.offices[city])).toBeGreaterThan(100); + expect(divisionChem.requiredMaterials).toStrictEqual({ Plants: 1, Water: 0.5 }); + expect(warehouse.size).toBeCloseTo(100); + expect(warehouse.sizeUsed).toBeCloseTo(50); + expect(warehouse.smartSupplyEnabled).toBe(true); + }); + + it("should prepare warehouses correctly for the Mining division", () => { + const warehouse = divisionMining.warehouses[city]; + expect(divisionMining.getOfficeProductivity(divisionMining.offices[city])).toBeGreaterThan(100); + expect(divisionMining.requiredMaterials).toStrictEqual({ Hardware: 0.1 }); + expect(warehouse.size).toBeCloseTo(100); + expect(warehouse.sizeUsed).toBeCloseTo(50); + expect(warehouse.smartSupplyEnabled).toBe(true); + }); + + function simulateUntilSecondPurchasePhase(division: Division) { + const warehouse = division.warehouses[city]; + + // Simulate processing phases leading up to the second purchase phase + for (let i = 0; i < 6; i++) { + division.process(1, corporation); + corporation.state.incrementState(); + } + + warehouse.updateMaterialSizeUsed(); + + // Process again to simulate purchasing + expect(corporation.state.nextName).toBe("PURCHASE"); + division.process(1, corporation); + warehouse.updateMaterialSizeUsed(); + } + + function prepareWarehouseForTest(warehouse: Warehouse, material: string, amount: number) { + warehouse.materials[material].stored = amount; + warehouse.updateMaterialSizeUsed(); + } + + it("should buy maximum amount for the Chemical division when storage is empty", () => { + const warehouse = divisionChem.warehouses[city]; + prepareWarehouseForTest(warehouse, "Plants", 0); + simulateUntilSecondPurchasePhase(divisionChem); + expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); + }); + + it("should buy maximum amount for the Chemical division when storage has 200 units of Plants", () => { + const warehouse = divisionChem.warehouses[city]; + prepareWarehouseForTest(warehouse, "Plants", 200); + simulateUntilSecondPurchasePhase(divisionChem); + expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); + }); + + it("should buy maximum amount for the Chemical division when storage has 200 units of Water", () => { + const warehouse = divisionChem.warehouses[city]; + prepareWarehouseForTest(warehouse, "Water", 200); + simulateUntilSecondPurchasePhase(divisionChem); + expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); + }); + + it("should buy maximum amount for the Chemical division when storage has 400 units of Plants", () => { + const warehouse = divisionChem.warehouses[city]; + prepareWarehouseForTest(warehouse, "Plants", 400); + simulateUntilSecondPurchasePhase(divisionChem); + expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); + }); + + it("should buy maximum amount for the Chemical division when storage has 400 units of Water", () => { + const warehouse = divisionChem.warehouses[city]; + prepareWarehouseForTest(warehouse, "Water", 400); + simulateUntilSecondPurchasePhase(divisionChem); + expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); + }); + + it("should not overbuy when product size is larger than base materials", () => { + const warehouse = divisionMining.warehouses[city]; + prepareWarehouseForTest(warehouse, "Food", 3334 * 0.95); // Pre-fill with some food + expect(warehouse.sizeUsed).toBeGreaterThan(95); + + simulateUntilSecondPurchasePhase(divisionMining); + + // Estimate that warehouse is not overly full + expect(warehouse.sizeUsed).toBeLessThan(96); + + // Simulate production to use up the base material + corporation.state.incrementState(); + divisionMining.process(1, corporation); + + // Verify that production uses up the warehouse space and base materials + expect(warehouse.materials["Hardware"].stored).toBeLessThan(0.1); + expect(warehouse.sizeUsed).toBeCloseTo(100, 1); + }); + }); + }); +}); From 048bc2bb70eeabe50a4cbacf257ebd57c131f583 Mon Sep 17 00:00:00 2001 From: tinion Date: Sun, 25 Aug 2024 11:23:13 +0200 Subject: [PATCH 02/10] Add more basic tests for SmartSupply functionality in Corporations --- test/jest/Corporation.test.ts | 287 ++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 32 deletions(-) diff --git a/test/jest/Corporation.test.ts b/test/jest/Corporation.test.ts index 2b3cc81350..c22d561f8e 100644 --- a/test/jest/Corporation.test.ts +++ b/test/jest/Corporation.test.ts @@ -6,6 +6,7 @@ import { Material } from "../../src/Corporation/Material"; import { OfficeSpace } from "../../src/Corporation/OfficeSpace"; import { CorpUpgrades } from "../../src/Corporation/data/CorporationUpgrades"; import { CorpUnlockName, CorpEmployeeJob } from "@enums"; +import * as corpConstants from "../../src/Corporation/data/Constants"; import { calculateMaxAffordableUpgrade, calculateUpgradeCost, @@ -19,6 +20,7 @@ import { goPublic, issueNewShares, sellShares, + makeProduct, } from "../../src/Corporation/Actions"; describe("Corporation", () => { @@ -148,9 +150,11 @@ describe("Division", () => { let corporation: Corporation; let divisionChem: Division; let divisionMining: Division; + let divisionComputer: Division; const city = "Sector-12"; const name = "ChemTestDiv"; + var cyclesNeeded = 0; function setupCorporation() { setPlayer(new PlayerObject()); @@ -175,6 +179,15 @@ describe("Division", () => { }), ); + corporation.divisions.set( + "ComputerTestDiv", + new Division({ + corp: corporation, + name: "ComputerTestDiv", + type: "Computer Hardware", + }), + ); + corporation.unlocks.add(CorpUnlockName.SmartSupply); return corporation; } @@ -195,12 +208,13 @@ describe("Division", () => { beforeAll(() => { corporation = setupCorporation(); - divisionChem = corporation.divisions.get("ChemTestDiv"); - divisionMining = corporation.divisions.get("MiningTestDiv"); + divisionChem = corporation.divisions.get("ChemTestDiv")!; + divisionMining = corporation.divisions.get("MiningTestDiv")!; + divisionComputer = corporation.divisions.get("ComputerTestDiv")!; // Improve Productivity to fill the warehouse faster - for (const div of [divisionChem, divisionMining]) { - const office = div.offices[city]; + for (const div of [divisionChem, divisionMining, divisionComputer]) { + const office = div.offices[city]!; const osize = 2e6; office.size = osize; for (let i = 0; i < osize / 2; i++) { @@ -219,6 +233,8 @@ describe("Division", () => { setupWarehouse(divisionChem, city, corporation); setupWarehouse(divisionMining, city, corporation); + setupWarehouse(divisionComputer, city, corporation); + divisionComputer.products.clear(); }); describe("processMaterials", () => { @@ -229,8 +245,8 @@ describe("Division", () => { }); it("should prepare warehouses correctly for the Chemical division", () => { - const warehouse = divisionChem.warehouses[city]; - expect(divisionChem.getOfficeProductivity(divisionChem.offices[city])).toBeGreaterThan(100); + const warehouse = divisionChem.warehouses[city]!; + expect(divisionChem.getOfficeProductivity(divisionChem.offices[city]!)).toBeGreaterThan(100); expect(divisionChem.requiredMaterials).toStrictEqual({ Plants: 1, Water: 0.5 }); expect(warehouse.size).toBeCloseTo(100); expect(warehouse.sizeUsed).toBeCloseTo(50); @@ -238,29 +254,46 @@ describe("Division", () => { }); it("should prepare warehouses correctly for the Mining division", () => { - const warehouse = divisionMining.warehouses[city]; - expect(divisionMining.getOfficeProductivity(divisionMining.offices[city])).toBeGreaterThan(100); + const warehouse = divisionMining.warehouses[city]!; + expect(divisionMining.getOfficeProductivity(divisionMining.offices[city]!)).toBeGreaterThan(100); expect(divisionMining.requiredMaterials).toStrictEqual({ Hardware: 0.1 }); expect(warehouse.size).toBeCloseTo(100); expect(warehouse.sizeUsed).toBeCloseTo(50); expect(warehouse.smartSupplyEnabled).toBe(true); }); - function simulateUntilSecondPurchasePhase(division: Division) { - const warehouse = division.warehouses[city]; + it("should prepare warehouses correctly for the Computer division", () => { + const warehouse = divisionComputer.warehouses[city]!; + expect(divisionComputer.getOfficeProductivity(divisionComputer.offices[city]!)).toBeGreaterThan(100); + expect(divisionComputer.requiredMaterials).toStrictEqual({ Metal: 2 }); + expect(warehouse.size).toBeCloseTo(100); + expect(warehouse.sizeUsed).toBeCloseTo(50); + expect(warehouse.smartSupplyEnabled).toBe(true); + }); + + // returns the number of full cycles it took for SmartBuy to act + function simulateUntilSmartBuyActed(division: Division, maxFullCycles = 3) { + const warehouse = division.warehouses[city]!; + + // Simulate processing phases until something was bought + for (let i = 0; i < maxFullCycles; i++) { + while (corporation.state.nextName != "PURCHASE") { + division.process(1, corporation); + corporation.state.incrementState(); + } - // Simulate processing phases leading up to the second purchase phase - for (let i = 0; i < 6; i++) { + warehouse.updateMaterialSizeUsed(); + const preWarehouseSize = warehouse.sizeUsed; division.process(1, corporation); + warehouse.updateMaterialSizeUsed(); + + if (preWarehouseSize != warehouse.sizeUsed) { + return i; + } corporation.state.incrementState(); } - - warehouse.updateMaterialSizeUsed(); - - // Process again to simulate purchasing - expect(corporation.state.nextName).toBe("PURCHASE"); - division.process(1, corporation); - warehouse.updateMaterialSizeUsed(); + // SmartBuy did not act + return -1; } function prepareWarehouseForTest(warehouse: Warehouse, material: string, amount: number) { @@ -268,50 +301,77 @@ describe("Division", () => { warehouse.updateMaterialSizeUsed(); } + it("should enable SmartBuy in the first cycle", () => { + let cycles = simulateUntilSmartBuyActed(divisionChem); + cyclesNeeded = cycles; + expect(cycles).toBe(0); + }); + it("should buy maximum amount for the Chemical division when storage is empty", () => { - const warehouse = divisionChem.warehouses[city]; + const warehouse = divisionChem.warehouses[city]!; prepareWarehouseForTest(warehouse, "Plants", 0); - simulateUntilSecondPurchasePhase(divisionChem); + simulateUntilSmartBuyActed(divisionChem); expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); }); + it("should not buy over production capability", () => { + const warehouse = divisionChem.warehouses[city]!; + prepareWarehouseForTest(warehouse, "Food", 0); // empty + prepareWarehouseForTest(warehouse, "Plants", 0); + simulateUntilSmartBuyActed(divisionChem); + expect(warehouse.sizeUsed).toBeLessThan(warehouse.size - 1); + // Simulate production, export and sell + corporation.state.incrementState(); + divisionChem.process(1, corporation); + expect(warehouse.materials["Chemicals"].stored).toBeGreaterThan(0); + warehouse.materials["Chemicals"].desiredSellAmount = 10000; + warehouse.materials["Chemicals"].desiredSellPrice = 0; + corporation.state.incrementState(); + divisionChem.process(1, corporation); + corporation.state.incrementState(); + divisionChem.process(1, corporation); + expect(warehouse.materials["Chemicals"].stored).toBe(0); + expect(warehouse.materials["Plants"].stored).toBeCloseTo(0, 2); + expect(warehouse.materials["Water"].stored).toBeCloseTo(0, 2); + }); + it("should buy maximum amount for the Chemical division when storage has 200 units of Plants", () => { - const warehouse = divisionChem.warehouses[city]; + const warehouse = divisionChem.warehouses[city]!; prepareWarehouseForTest(warehouse, "Plants", 200); - simulateUntilSecondPurchasePhase(divisionChem); + simulateUntilSmartBuyActed(divisionChem); expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); }); it("should buy maximum amount for the Chemical division when storage has 200 units of Water", () => { - const warehouse = divisionChem.warehouses[city]; + const warehouse = divisionChem.warehouses[city]!; prepareWarehouseForTest(warehouse, "Water", 200); - simulateUntilSecondPurchasePhase(divisionChem); + simulateUntilSmartBuyActed(divisionChem); expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); }); it("should buy maximum amount for the Chemical division when storage has 400 units of Plants", () => { - const warehouse = divisionChem.warehouses[city]; + const warehouse = divisionChem.warehouses[city]!; prepareWarehouseForTest(warehouse, "Plants", 400); - simulateUntilSecondPurchasePhase(divisionChem); + simulateUntilSmartBuyActed(divisionChem); expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); }); it("should buy maximum amount for the Chemical division when storage has 400 units of Water", () => { - const warehouse = divisionChem.warehouses[city]; + const warehouse = divisionChem.warehouses[city]!; prepareWarehouseForTest(warehouse, "Water", 400); - simulateUntilSecondPurchasePhase(divisionChem); + simulateUntilSmartBuyActed(divisionChem); expect(warehouse.sizeUsed).toBeGreaterThan(warehouse.size - 1); }); it("should not overbuy when product size is larger than base materials", () => { - const warehouse = divisionMining.warehouses[city]; + const warehouse = divisionMining.warehouses[city]!; prepareWarehouseForTest(warehouse, "Food", 3334 * 0.95); // Pre-fill with some food expect(warehouse.sizeUsed).toBeGreaterThan(95); - simulateUntilSecondPurchasePhase(divisionMining); + simulateUntilSmartBuyActed(divisionMining); // Estimate that warehouse is not overly full - expect(warehouse.sizeUsed).toBeLessThan(96); + // expect(warehouse.sizeUsed).toBeLessThan(96); // Simulate production to use up the base material corporation.state.incrementState(); @@ -321,6 +381,169 @@ describe("Division", () => { expect(warehouse.materials["Hardware"].stored).toBeLessThan(0.1); expect(warehouse.sizeUsed).toBeCloseTo(100, 1); }); + + it("should not act when disabled", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.smartSupplyEnabled = false; + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(-1); + }); + + it("should not act when no materials are required", () => { + const warehouse = divisionMining.warehouses[city]!; + const actualRequiredMaterials = divisionMining.requiredMaterials; + divisionMining.requiredMaterials = {}; + expect(simulateUntilSmartBuyActed(divisionMining)).toBe(-1); + divisionMining.requiredMaterials = actualRequiredMaterials; + }); + + it("should not act if production would hinder import", () => { + const warehouse = divisionMining.warehouses[city]!; + warehouse.materials["Food"].importAmount = 1e10; + expect(simulateUntilSmartBuyActed(divisionMining)).toBe(-1); + }); + + it("should not act when enough materials are being imported (set to imports)", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.smartSupplyOptions["Plants"] = "imports"; + warehouse.smartSupplyOptions["Water"] = "imports"; + warehouse.materials["Food"].stored = 0; + warehouse.materials["Plants"].importAmount = 1100 / corpConstants.secondsPerMarketCycle; + warehouse.materials["Water"].importAmount = 550 / corpConstants.secondsPerMarketCycle; + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(-1); + }); + + it("should act when some materials are being imported (set to imports)", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.smartSupplyOptions["Plants"] = "imports"; + warehouse.smartSupplyOptions["Water"] = "imports"; + warehouse.materials["Food"].stored = 0; + warehouse.materials["Plants"].importAmount = 1100 / corpConstants.secondsPerMarketCycle / 2; + warehouse.materials["Water"].importAmount = 550 / corpConstants.secondsPerMarketCycle / 2; + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(cyclesNeeded); + }); + + it("should act when materials are being imported (set to none)", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.smartSupplyOptions["Plants"] = "none"; + warehouse.smartSupplyOptions["Water"] = "none"; + warehouse.materials["Plants"].importAmount == 1e10; + warehouse.materials["Water"].importAmount == 1e10; + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(cyclesNeeded); + }); + + it("should not act when there are enough materials leftover (set to leftovers)", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.materials["Chemicals"].desiredSellAmount = 10000; + warehouse.materials["Chemicals"].desiredSellPrice = 0; + warehouse.smartSupplyOptions["Plants"] = "leftovers"; + warehouse.smartSupplyOptions["Water"] = "leftovers"; + warehouse.materials["Food"].stored = 0; + simulateUntilSmartBuyActed(divisionChem); + corporation.state.incrementState(); + divisionComputer.process(1, corporation); + corporation.state.incrementState(); + divisionComputer.process(1, corporation); + // now all sold, fill warehouse + warehouse.materials["Plants"].stored = 1100; + warehouse.materials["Water"].stored = 550; + warehouse.updateMaterialSizeUsed(); + expect(warehouse.sizeUsed).toBeGreaterThan(80); + expect(warehouse.sizeUsed).toBeLessThan(90); + + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(1); // will consume after 1 cycle + }); + + it("should act when there are enough materials leftover (set to none)", () => { + const warehouse = divisionChem.warehouses[city]!; + warehouse.smartSupplyOptions["Plants"] = "none"; + warehouse.smartSupplyOptions["Water"] = "none"; + warehouse.materials["Food"].stored = 0; + warehouse.materials["Plants"].stored = 1100; + warehouse.materials["Water"].stored = 550; + warehouse.updateMaterialSizeUsed(); + expect(warehouse.sizeUsed).toBeGreaterThan(80); + expect(warehouse.sizeUsed).toBeLessThan(90); + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(cyclesNeeded); + }); + + it("should not act when there is no product and no output material", () => { + const warehouse = divisionComputer.warehouses[city]!; + makeProduct(corporation, divisionComputer, city, "Hardware", 1, 1); + let producedMaterials = divisionComputer.producedMaterials; + divisionComputer.producedMaterials = []; + expect(simulateUntilSmartBuyActed(divisionComputer)).toBe(-1); + divisionComputer.producedMaterials = producedMaterials; + }); + + it("should not act when there is no finished product and no output material", () => { + const warehouse = divisionComputer.warehouses[city]!; + makeProduct(corporation, divisionComputer, city, "Hardware", 1, 1); + let producedMaterials = divisionComputer.producedMaterials; + divisionComputer.producedMaterials = []; + expect(divisionComputer.products.has("Hardware")).toBe(true); + expect(divisionComputer.products.get("Hardware")!.finished).toBe(false); + + expect(simulateUntilSmartBuyActed(divisionComputer)).toBe(-1); + divisionComputer.producedMaterials = producedMaterials; + }); + + it("should act properly when there is a finished product", () => { + const warehouse = divisionComputer.warehouses[city]!; + warehouse.level = 100; + warehouse.updateSize(corporation, divisionComputer); + prepareWarehouseForTest(warehouse, "Food", 0); // empty + makeProduct(corporation, divisionComputer, city, "Hardware", 1, 1); + expect(divisionComputer.products.has("Hardware")).toBe(true); + divisionComputer.products.get("Hardware")!.finishProduct(divisionComputer); + expect(divisionComputer.products.get("Hardware")!.finished).toBe(true); + expect(simulateUntilSmartBuyActed(divisionComputer)).toBe(cyclesNeeded); + //expect(warehouse.sizeUsed).toBeGreaterThan(400); + corporation.state.incrementState(); + divisionComputer.process(1, corporation); + // check base material is used up + expect(warehouse.materials["Metal"].stored).toBeLessThan(10); // leave some room for current inefficient production + }); + + it("should act properly when there is a finished product and no output materials", () => { + const warehouse = divisionComputer.warehouses[city]!; + warehouse.level = 100; + warehouse.updateSize(corporation, divisionComputer); + prepareWarehouseForTest(warehouse, "Food", 0); // empty + makeProduct(corporation, divisionComputer, city, "Hardware", 1, 1); + expect(divisionComputer.products.has("Hardware")).toBe(true); + divisionComputer.products.get("Hardware")!.finishProduct(divisionComputer); + expect(divisionComputer.products.get("Hardware")!.finished).toBe(true); + let producedMaterials = divisionComputer.producedMaterials; + divisionComputer.producedMaterials = []; + expect(simulateUntilSmartBuyActed(divisionComputer)).toBe(cyclesNeeded); + corporation.state.incrementState(); + //expect(warehouse.sizeUsed).toBeGreaterThan(200); + divisionComputer.process(1, corporation); + // check base material is used up + expect(warehouse.materials["Metal"].stored).toBeLessThan(10); // leave some room for current inefficient production + divisionComputer.producedMaterials = producedMaterials; + }); + + it("should act properly when there are multiple finished products", () => { + const warehouse = divisionComputer.warehouses[city]!; + warehouse.level = 100; + warehouse.updateSize(corporation, divisionComputer); + prepareWarehouseForTest(warehouse, "Food", 0); // empty + makeProduct(corporation, divisionComputer, city, "Hardware", 1, 1); + makeProduct(corporation, divisionComputer, city, "Hardware2", 1, 1); + expect(divisionComputer.products.has("Hardware")).toBe(true); + expect(divisionComputer.products.has("Hardware2")).toBe(true); + divisionComputer.products.get("Hardware")!.finishProduct(divisionComputer); + divisionComputer.products.get("Hardware2")!.finishProduct(divisionComputer); + expect(divisionComputer.products.get("Hardware")!.finished).toBe(true); + expect(divisionComputer.products.get("Hardware2")!.finished).toBe(true); + expect(simulateUntilSmartBuyActed(divisionComputer)).toBe(cyclesNeeded); + //expect(warehouse.sizeUsed).toBeGreaterThan(600); + corporation.state.incrementState(); + divisionComputer.process(1, corporation); + // check base material is used up + expect(warehouse.materials["Metal"].stored).toBeLessThan(10); // leave some room for current inefficient production + }); }); }); }); From f64d3b6e8b942c3baa7999b2c0b34a30297d1c2f Mon Sep 17 00:00:00 2001 From: tinion Date: Sun, 25 Aug 2024 11:26:51 +0200 Subject: [PATCH 03/10] npm format --- test/jest/Corporation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest/Corporation.test.ts b/test/jest/Corporation.test.ts index c22d561f8e..899054a3e3 100644 --- a/test/jest/Corporation.test.ts +++ b/test/jest/Corporation.test.ts @@ -449,7 +449,7 @@ describe("Division", () => { warehouse.updateMaterialSizeUsed(); expect(warehouse.sizeUsed).toBeGreaterThan(80); expect(warehouse.sizeUsed).toBeLessThan(90); - + expect(simulateUntilSmartBuyActed(divisionChem)).toBe(1); // will consume after 1 cycle }); From 661274abcf9f1f5ee76fdaa4a6c165f35b36d0da Mon Sep 17 00:00:00 2001 From: tinion Date: Mon, 26 Aug 2024 01:54:18 +0200 Subject: [PATCH 04/10] Enhance and Refactor SmartSupply Functionality in Corporations --- src/Corporation/Division.ts | 149 +++++++++++------------------ src/Corporation/SmartSupply.ts | 166 +++++++++++++++++++++++++++++++++ src/Corporation/Warehouse.ts | 5 - 3 files changed, 218 insertions(+), 102 deletions(-) create mode 100644 src/Corporation/SmartSupply.ts diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index 0d521cb97e..d0d5c0e4b1 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -16,6 +16,7 @@ import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from import { Material } from "./Material"; import { getKeyList } from "../utils/helpers/getKeyList"; import { calculateMarkupMultiplier } from "./helpers"; +import { smartSupply, getProductionCapacity } from "./SmartSupply"; interface DivisionParams { name: string; @@ -290,114 +291,65 @@ export class Division { this.getScientificResearchMultiplier(); // Employee pay is an expense even with no warehouse. - expenses += office.totalSalary; + expenses += office.totalSalary; // Ann.: Might need to be multiplied by marketCycles const warehouse = this.warehouses[city]; if (!warehouse) continue; switch (state) { case "PURCHASE": { - const smartBuy: PartialRecord = {}; - - /* Process purchase of materials, not from smart supply */ - for (const [matName, mat] of getRecordEntries(warehouse.materials)) { - const reqMat = this.requiredMaterials[matName]; - if (warehouse.smartSupplyEnabled && reqMat) { - // Smart supply - mat.buyAmount = reqMat * warehouse.smartSupplyStore; - let buyAmt = mat.buyAmount * corpConstants.secondsPerMarketCycle * marketCycles; - const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / MaterialInfo[matName].size); - buyAmt = Math.min(buyAmt, maxAmt); - if (buyAmt > 0) smartBuy[matName] = [buyAmt, reqMat]; - } else { - // Not smart supply - let buyAmt = 0; - let maxAmt = 0; - - buyAmt = mat.buyAmount * corpConstants.secondsPerMarketCycle * marketCycles; - - maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / MaterialInfo[matName].size); - - buyAmt = Math.min(buyAmt, maxAmt); - if (buyAmt > 0) { - mat.quality = Math.max(0.1, (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt)); - mat.averagePrice = (mat.stored * mat.averagePrice + buyAmt * mat.marketPrice) / (mat.stored + buyAmt); - mat.stored += buyAmt; - expenses += buyAmt * mat.marketPrice; - } - this.updateWarehouseSizeUsed(warehouse); - } - } //End process purchase of materials - - // Find which material were trying to create the least amount of product with. - let worseAmt = 1e99; - for (const [buyAmt, reqMat] of Object.values(smartBuy)) { - const amt = buyAmt / reqMat; - if (amt < worseAmt) worseAmt = amt; - } - - // Align all the materials to the smallest amount. - for (const buyArray of Object.values(smartBuy)) { - buyArray[0] = worseAmt * buyArray[1]; - } - - // Calculate the total size of all things were trying to buy - let totalSize = 0; - for (const [matName, [buyAmt]] of getRecordEntries(smartBuy)) { - if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`); - totalSize += buyAmt * MaterialInfo[matName].size; - } - - // Shrink to the size of available space. - const freeSpace = warehouse.size - warehouse.sizeUsed; - if (totalSize > freeSpace) { - // Multiplier applied to buy amounts to not overfill warehouse - const buyMult = freeSpace / totalSize; - for (const buyArray of Object.values(smartBuy)) { - buyArray[0] = Math.floor(buyArray[0] * buyMult); - } - } - - // Use the materials already in the warehouse if the option is on. - for (const [matName, buyArray] of getRecordEntries(smartBuy)) { - if (warehouse.smartSupplyOptions[matName] === "none") continue; + const buy = (warehouse: Warehouse, matName: CorpMaterialName, buyAmt: number) => { const mat = warehouse.materials[matName]; - if (warehouse.smartSupplyOptions[matName] === "leftovers") { - buyArray[0] = Math.max(0, buyArray[0] - mat.stored); - } else { - buyArray[0] = Math.max( - 0, - buyArray[0] - mat.importAmount * corpConstants.secondsPerMarketCycle * marketCycles, - ); + const size = MaterialInfo[matName].size; + const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / size); + buyAmt = Math.min(buyAmt, maxAmt); + if (buyAmt > 0) { + mat.quality = Math.max(0.1, (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt)); + mat.averagePrice = (mat.stored * mat.averagePrice + buyAmt * mat.marketPrice) / (mat.stored + buyAmt); + mat.stored += buyAmt; } - } + warehouse.updateMaterialSizeUsed(); + return buyAmt * mat.marketPrice; + }; - // buy them - for (const [matName, [buyAmt]] of getRecordEntries(smartBuy)) { - const mat = warehouse.materials[matName]; - if (mat.stored + buyAmt !== 0) { - mat.quality = (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt); - mat.averagePrice = (mat.averagePrice * mat.stored + mat.marketPrice * buyAmt) / (mat.stored + buyAmt); - } else { - mat.quality = 1; - mat.averagePrice = mat.marketPrice; - } - mat.stored += buyAmt; - mat.buyAmount = buyAmt / (corpConstants.secondsPerMarketCycle * marketCycles); - expenses += buyAmt * mat.marketPrice; + const getMaxProd = (office: OfficeSpace, forProduct: boolean) => + this.getOfficeProductivity(office, { forProduct }) * + this.productionMult * + corporation.getProductionMultiplier() * + this.getProductionMultiplier(); + const passedSeconds = corpConstants.secondsPerMarketCycle * marketCycles; + + const pBaseOutput = getMaxProd(office, false) * passedSeconds; + const pBaseProduct = getMaxProd(office, true) * passedSeconds; + + // Determine average size of materials produced by this city's office + // and calculate production capacity + const [avgSize, pOutput, pProduct] = getProductionCapacity( + warehouse, + this.producedMaterials, + this.products, + pBaseOutput, + pBaseProduct, + ); + + // Set buy amounts automatically, when smartSupply is enabled + smartSupply(warehouse, this.requiredMaterials, pOutput + pProduct, avgSize, passedSeconds); + + /* Process purchase of materials*/ + for (const [matName, mat] of getRecordEntries(warehouse.materials)) { + const buyAmt = mat.buyAmount * corpConstants.secondsPerMarketCycle * marketCycles; + expenses += buy(warehouse, matName, buyAmt); } break; } case "PRODUCTION": - warehouse.smartSupplyStore = 0; //Reset smart supply amount - /* Process production of materials */ if (this.producedMaterials.length > 0) { + // Ann.: This causes the production limit of the first material to be used for all materials const mat = warehouse.materials[this.producedMaterials[0]]; //Calculate the maximum production of this material based - //on the office's productivity const maxProd = - this.getOfficeProductivity(office) * + this.getOfficeProductivity(office) * // on the office's productivity this.productionMult * // Multiplier from materials corporation.getProductionMultiplier() * this.getProductionMultiplier(); // Multiplier from Research @@ -407,7 +359,6 @@ export class Division { prod = mat.productionLimit === null ? maxProd : Math.min(maxProd, mat.productionLimit); prod *= corpConstants.secondsPerMarketCycle * marketCycles; //Convert production from per second to per market cycle - // Calculate net change in warehouse storage making the produced materials will cost let totalMatSize = 0; for (let tmp = 0; tmp < this.producedMaterials.length; ++tmp) { @@ -418,6 +369,15 @@ export class Division { } // If not enough space in warehouse, limit the amount of produced materials if (totalMatSize > 0) { + /* + TODO: Consider rewriting this to optimize production throughput. + Space freed up by consuming materials can be used to produce more materials. + However, full production might hinder imports if not properly accounted for. + Therefore, it may be beneficial to allow the player to set a limit on the warehouse for production. + For example: Production Space = CAPACITY - IMPORTS. + This needs to be discussed, as smartSupply should use the same logic as production but currently assumes full production. + The calculation logic from smartSupply.ts/getProductionCapacity can be reused here. + */ const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / totalMatSize); prod = Math.min(maxAmt, prod); } @@ -426,9 +386,6 @@ export class Division { prod = 0; } - // Keep track of production for smart supply (/s) - warehouse.smartSupplyStore += prod / (corpConstants.secondsPerMarketCycle * marketCycles); - // Make sure we have enough resource to make our materials let producableFrac = 1; for (const [reqMatName, reqMat] of getRecordEntries(this.requiredMaterials)) { @@ -777,12 +734,10 @@ export class Division { //If there's not enough space in warehouse, limit the amount of Product if (netStorageSize > 0) { - const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / netStorageSize); + const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / netStorageSize); // ToDO: Use geometric series prod = Math.min(maxAmt, prod); } - warehouse.smartSupplyStore += prod / (corpConstants.secondsPerMarketCycle * marketCycles); - //Make sure we have enough resources to make our Products let producableFrac = 1; for (const [reqMatName, reqQty] of getRecordEntries(product.requiredMaterials)) { diff --git a/src/Corporation/SmartSupply.ts b/src/Corporation/SmartSupply.ts new file mode 100644 index 0000000000..0743625d8b --- /dev/null +++ b/src/Corporation/SmartSupply.ts @@ -0,0 +1,166 @@ +import { JSONMap } from "../Types/Jsonable"; +import { PartialRecord } from "../Types/Record"; +import { CorpMaterialName } from "./Enums"; +import { MaterialInfo } from "./MaterialInfo"; +import { Product } from "./Product"; +import { Warehouse } from "./Warehouse"; + +type RatioData = { + matName: CorpMaterialName | null; + ratio: number; +}; + +function normalize(ratios: RatioData[]): void { + const total = ratios.reduce((sum, item) => sum + item.ratio, 0); + ratios.forEach((item) => { + item.ratio /= total; // when total is 0, normalize has undefined behavior + }); +} + +// This function calculates the amount of materials that shell be used for production. +// When the smart supply option is set to "imports", it returns the amount of materials that are being imported. +// When the smart supply option is set to "leftovers", it returns the amount of materials that are already in the warehouse. +// Otherwise, it returns 0. +function getSmartSupplyUsableMaterial(warehouse: Warehouse, mname: CorpMaterialName, passedSeconds: number): number { + const supplyOption = warehouse.smartSupplyOptions[mname]; + const material = warehouse.materials[mname]; + let amount = 0; + if (supplyOption === "imports") { + amount = material.importAmount * passedSeconds; + } else if (supplyOption === "leftovers") { + amount = material.stored; + } + return MaterialInfo[mname].size * amount; +} + +// This function calculates the production capacity for output materials and products in a city. +// It takes into account the production limits of both the materials and the products. +// Additionally, it calculates the average size of all output materials and products. +// Returns [averageSize, output capacity, product capacity] +export function getProductionCapacity( + warehouse: Warehouse, + producedMaterials: CorpMaterialName[], + products: JSONMap, + prodMultOutput: number, + prodMultProducts: number, +): [number, number, number] { + if (prodMultOutput <= 0 || prodMultProducts <= 0) { + return [0, 0, 0]; // should not happen + } + let totalOutputProdVolume = 0; + let totalProductProdVolume = 0; + let totalProd = 0; + let totalProductProd = 0; + + // Production limit is currently broken + // The first lines are how it should be imo, but the third line is how it is currently in the game + // const getMatProductionLimit = (mat: CorpMaterialName) => warehouse.materials[mat].productionLimit ?? -1; + // const getMatProductionLimit = (mat: CorpMaterialName) => Math.min(...producedMaterials.map((mat) => warehouse.materials[mat].productionLimit!)) + const getMatProductionLimit = (__: any) => warehouse.materials[producedMaterials[0]].productionLimit ?? -1; + + for (let i = 0; i < producedMaterials.length; i++) { + const mat = producedMaterials[i]; + const limit = getMatProductionLimit(mat) ?? -1; + const factor = limit < 0 ? 1 : limit / prodMultOutput; + totalOutputProdVolume += prodMultOutput * factor * MaterialInfo[mat].size; + totalProd += (prodMultOutput * factor) / producedMaterials.length; // output materials are produced together + } + + const city = warehouse.city; + for (const product of products.values()) { + if (product.finished) { + const limit = + product.cityData && product.cityData[city] && product.cityData[city].productionLimit + ? product.cityData[city].productionLimit! + : -1; + const factor = limit < 0 ? 1 : limit / prodMultProducts; + totalProductProdVolume += prodMultProducts * factor * product.size; + totalProductProd += prodMultProducts * factor; + } + } + + // avg Out+Prod size = TotalVolume / TotalProduction + const averageSize = (totalOutputProdVolume + totalProductProdVolume) / (totalProd + totalProductProd); + return [averageSize, totalProd, totalProductProd]; +} + +// Smart Supply Algorithm +// This function calculates the amount of materials to buy for production. +// It takes into account the production capacity, the amount of materials already in the warehouse, +// the available empty space in the warehouse, and the amount of materials that are being imported. +export function smartSupply( + warehouse: Warehouse, + requiredMaterials: PartialRecord, + productionCapacity: number, + averageOutSize: number, + passedSeconds: number, +) { + if (!warehouse.smartSupplyEnabled || Object.keys(requiredMaterials).length === 0) { + return; + } + + // 1. Calculate the space that can be used for input materials, considering the space needed for products and imported materials. + const totalImportSpace = Object.keys(warehouse.materials).reduce( + (total, key) => + total + + warehouse.materials[key as CorpMaterialName].importAmount * + MaterialInfo[key as CorpMaterialName].size * + passedSeconds, + 0, + ); + + const free = warehouse.size - totalImportSpace - warehouse.sizeUsed; + const alreadyUsedByRequiredMaterials = Object.keys(requiredMaterials).reduce( + (sum, mat) => sum + getSmartSupplyUsableMaterial(warehouse, mat as CorpMaterialName, passedSeconds), + 0, + ); + let availableSpace = free + alreadyUsedByRequiredMaterials; + + // 2. Determine the ratios of all input materials. + const ratios: RatioData[] = Object.entries(requiredMaterials) + .filter(([__, amount]): boolean => amount !== undefined && amount !== 0) + .map( + ([matName, amount]): RatioData => ({ + matName: matName as CorpMaterialName, + ratio: (amount as number) * MaterialInfo[matName as CorpMaterialName].size, + }), + ) + .concat([{ matName: null, ratio: averageOutSize } as RatioData]); // add some output space temporarily + normalize(ratios); // normalize, which yields the output ratio + + // When products require more space than their base materials, we need to reserve extra space. + const outputRatio = ratios.pop()!.ratio; + const inputRatio = 1 - outputRatio; + // Adjust the space being available by applying the geometric series formula to find the optimal ratio. + availableSpace *= Math.min(1, inputRatio / outputRatio); + normalize(ratios); + + // 3. Determine the ratios of existing materials to needed materials. + const getExistingRatio = (ratioData: RatioData): number => { + const needed = ratioData.ratio * availableSpace; + const existing = getSmartSupplyUsableMaterial(warehouse, ratioData.matName!, passedSeconds); + return existing / needed; + }; + + ratios.sort((a, b) => getExistingRatio(a) - getExistingRatio(b)); // sort ascending by their existing-ratio + + // 4. Purchase the missing materials, starting with the materials that are least needed. + // If there is an excess of a material, reduce the total available space and recalculate the ratios. + while (ratios.length !== 0 && getExistingRatio(ratios[ratios.length - 1])! >= 1) { + const next = ratios.pop()!; + availableSpace -= getExistingRatio(next) * next.ratio * availableSpace; + warehouse.materials[next.matName!].buyAmount = 0; // don't buy + normalize(ratios); + } + while (ratios.length !== 0) { + const next = ratios.pop()!; + const material = warehouse.materials[next.matName!]; + const size = MaterialInfo[next.matName!].size; + const amountVolume = next.ratio * availableSpace; // determine missing volume + const amount = amountVolume / size; + const maxAmount = productionCapacity * requiredMaterials[next.matName!]!; + const finalAmount = Math.min(amount, maxAmount); // don't buy over production capacity + const usableAmount = getSmartSupplyUsableMaterial(warehouse, next.matName!, passedSeconds) / size; + material.buyAmount = Math.max(0, finalAmount - usableAmount) / passedSeconds; + } +} diff --git a/src/Corporation/Warehouse.ts b/src/Corporation/Warehouse.ts index 2d4720048b..9443d5ef4b 100644 --- a/src/Corporation/Warehouse.ts +++ b/src/Corporation/Warehouse.ts @@ -40,11 +40,6 @@ export class Warehouse { materialNames.map((matName) => [matName, "leftovers"]), ); - // Stores the amount of product to be produced. Used for Smart Supply unlock. - // The production tracked by smart supply is always based on the previous cycle, - // so it will always trail the "true" production by 1 cycle - smartSupplyStore = 0; - constructor(params: IConstructorParams | null = null) { const corp = Player.corporation; if (!corp || params === null) return; From d0ae716520c4606ba2a3470aec5eaf77476136ef Mon Sep 17 00:00:00 2001 From: tinion Date: Mon, 26 Aug 2024 01:58:59 +0200 Subject: [PATCH 05/10] npm format --- src/Corporation/Division.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index d0d5c0e4b1..0e79212f2e 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -345,7 +345,7 @@ export class Division { case "PRODUCTION": /* Process production of materials */ if (this.producedMaterials.length > 0) { - // Ann.: This causes the production limit of the first material to be used for all materials + // Ann.: This causes the production limit of the first material to be used for all materials const mat = warehouse.materials[this.producedMaterials[0]]; //Calculate the maximum production of this material based const maxProd = From 791b7f09f11ae5f6d5291896dfdb57d60d8beb36 Mon Sep 17 00:00:00 2001 From: tinion Date: Mon, 26 Aug 2024 15:43:39 +0200 Subject: [PATCH 06/10] Rewrite functional chaining of material ratios calculation to for-loop --- src/Corporation/SmartSupply.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Corporation/SmartSupply.ts b/src/Corporation/SmartSupply.ts index 0743625d8b..65e97a2fcf 100644 --- a/src/Corporation/SmartSupply.ts +++ b/src/Corporation/SmartSupply.ts @@ -17,7 +17,7 @@ function normalize(ratios: RatioData[]): void { }); } -// This function calculates the amount of materials that shell be used for production. +// This function calculates the volume of an input material that shell be used for production. // When the smart supply option is set to "imports", it returns the amount of materials that are being imported. // When the smart supply option is set to "leftovers", it returns the amount of materials that are already in the warehouse. // Otherwise, it returns 0. @@ -117,16 +117,19 @@ export function smartSupply( let availableSpace = free + alreadyUsedByRequiredMaterials; // 2. Determine the ratios of all input materials. - const ratios: RatioData[] = Object.entries(requiredMaterials) - .filter(([__, amount]): boolean => amount !== undefined && amount !== 0) - .map( - ([matName, amount]): RatioData => ({ + const ratios: RatioData[] = []; + for (const [matName, amount] of Object.entries(requiredMaterials)) { + if (amount !== undefined && amount !== 0) { + const ratioData: RatioData = { matName: matName as CorpMaterialName, ratio: (amount as number) * MaterialInfo[matName as CorpMaterialName].size, - }), - ) - .concat([{ matName: null, ratio: averageOutSize } as RatioData]); // add some output space temporarily - normalize(ratios); // normalize, which yields the output ratio + }; + ratios.push(ratioData); + } + } + // Add some output space temporarily + ratios.push({ matName: null, ratio: averageOutSize } as RatioData); + normalize(ratios); // Normalize, which yields the output ratio // When products require more space than their base materials, we need to reserve extra space. const outputRatio = ratios.pop()!.ratio; From 630e0ea369a09ad343418e026624f3b5733ad8ec Mon Sep 17 00:00:00 2001 From: tinion Date: Mon, 26 Aug 2024 16:12:06 +0200 Subject: [PATCH 07/10] Fix NaN issue in buy functionality --- src/Corporation/Division.ts | 6 ++++-- test/jest/Corporation.test.ts | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index 0e79212f2e..1688fd6d7b 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -307,9 +307,10 @@ export class Division { mat.quality = Math.max(0.1, (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt)); mat.averagePrice = (mat.stored * mat.averagePrice + buyAmt * mat.marketPrice) / (mat.stored + buyAmt); mat.stored += buyAmt; + warehouse.updateMaterialSizeUsed(); + return buyAmt * mat.marketPrice; } - warehouse.updateMaterialSizeUsed(); - return buyAmt * mat.marketPrice; + return 0; }; const getMaxProd = (office: OfficeSpace, forProduct: boolean) => @@ -455,6 +456,7 @@ export class Division { //If this doesn't produce any materials, then it only creates //Products. Creating products will consume materials. The //Production of all consumed materials must be set to 0 + //Ann: This seems odd for (const reqMatName of getRecordKeys(this.requiredMaterials)) { warehouse.materials[reqMatName].productionAmount = 0; } diff --git a/test/jest/Corporation.test.ts b/test/jest/Corporation.test.ts index 899054a3e3..58a3840511 100644 --- a/test/jest/Corporation.test.ts +++ b/test/jest/Corporation.test.ts @@ -278,6 +278,8 @@ describe("Division", () => { // Simulate processing phases until something was bought for (let i = 0; i < maxFullCycles; i++) { while (corporation.state.nextName != "PURCHASE") { + expect(isNaN(division.thisCycleExpenses)).toBe(false); + expect(isNaN(division.thisCycleRevenue)).toBe(false); division.process(1, corporation); corporation.state.incrementState(); } From 90b795a348f456f304cb8a946dcb3c28db62786e Mon Sep 17 00:00:00 2001 From: tinion Date: Tue, 27 Aug 2024 00:51:37 +0200 Subject: [PATCH 08/10] Enhance SmartSupply to buy stored materials if imports disappear or are sold before the purchase phase --- src/Corporation/Division.ts | 1 - src/Corporation/SmartSupply.ts | 3 ++- test/jest/Corporation.test.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index 1688fd6d7b..dcf1e1990d 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -456,7 +456,6 @@ export class Division { //If this doesn't produce any materials, then it only creates //Products. Creating products will consume materials. The //Production of all consumed materials must be set to 0 - //Ann: This seems odd for (const reqMatName of getRecordKeys(this.requiredMaterials)) { warehouse.materials[reqMatName].productionAmount = 0; } diff --git a/src/Corporation/SmartSupply.ts b/src/Corporation/SmartSupply.ts index 65e97a2fcf..ea9e86b10a 100644 --- a/src/Corporation/SmartSupply.ts +++ b/src/Corporation/SmartSupply.ts @@ -26,7 +26,8 @@ function getSmartSupplyUsableMaterial(warehouse: Warehouse, mname: CorpMaterialN const material = warehouse.materials[mname]; let amount = 0; if (supplyOption === "imports") { - amount = material.importAmount * passedSeconds; + // Use atleast the amount of materials that are being stored, but no more than the amount that is being imported. + amount = Math.min(material.importAmount * passedSeconds, material.stored); } else if (supplyOption === "leftovers") { amount = material.stored; } diff --git a/test/jest/Corporation.test.ts b/test/jest/Corporation.test.ts index 58a3840511..a41fa6f896 100644 --- a/test/jest/Corporation.test.ts +++ b/test/jest/Corporation.test.ts @@ -411,6 +411,9 @@ describe("Division", () => { warehouse.materials["Food"].stored = 0; warehouse.materials["Plants"].importAmount = 1100 / corpConstants.secondsPerMarketCycle; warehouse.materials["Water"].importAmount = 550 / corpConstants.secondsPerMarketCycle; + warehouse.materials["Plants"].stored = 1100; + warehouse.materials["Water"].stored = 550; + warehouse.updateMaterialSizeUsed(); expect(simulateUntilSmartBuyActed(divisionChem)).toBe(-1); }); @@ -421,6 +424,8 @@ describe("Division", () => { warehouse.materials["Food"].stored = 0; warehouse.materials["Plants"].importAmount = 1100 / corpConstants.secondsPerMarketCycle / 2; warehouse.materials["Water"].importAmount = 550 / corpConstants.secondsPerMarketCycle / 2; + warehouse.materials["Plants"].stored = 1100 / 2; + warehouse.materials["Water"].stored = 550 / 2; expect(simulateUntilSmartBuyActed(divisionChem)).toBe(cyclesNeeded); }); From b89121804e7ca376b0cb3c302d3298170b3b8240 Mon Sep 17 00:00:00 2001 From: tinion Date: Tue, 27 Aug 2024 21:43:03 +0200 Subject: [PATCH 09/10] Fix potential bug and simplify code in SmartSupply Production Capacity functionality --- src/Corporation/SmartSupply.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Corporation/SmartSupply.ts b/src/Corporation/SmartSupply.ts index ea9e86b10a..fa867d3634 100644 --- a/src/Corporation/SmartSupply.ts +++ b/src/Corporation/SmartSupply.ts @@ -48,6 +48,7 @@ export function getProductionCapacity( if (prodMultOutput <= 0 || prodMultProducts <= 0) { return [0, 0, 0]; // should not happen } + let totalOutputProdVolume = 0; let totalProductProdVolume = 0; let totalProd = 0; @@ -57,26 +58,22 @@ export function getProductionCapacity( // The first lines are how it should be imo, but the third line is how it is currently in the game // const getMatProductionLimit = (mat: CorpMaterialName) => warehouse.materials[mat].productionLimit ?? -1; // const getMatProductionLimit = (mat: CorpMaterialName) => Math.min(...producedMaterials.map((mat) => warehouse.materials[mat].productionLimit!)) - const getMatProductionLimit = (__: any) => warehouse.materials[producedMaterials[0]].productionLimit ?? -1; + const getMatProductionLimit = (__: any) => warehouse.materials[producedMaterials[0]].productionLimit; for (let i = 0; i < producedMaterials.length; i++) { const mat = producedMaterials[i]; - const limit = getMatProductionLimit(mat) ?? -1; - const factor = limit < 0 ? 1 : limit / prodMultOutput; - totalOutputProdVolume += prodMultOutput * factor * MaterialInfo[mat].size; - totalProd += (prodMultOutput * factor) / producedMaterials.length; // output materials are produced together + const limit = Math.min(getMatProductionLimit(mat) ?? Infinity, prodMultOutput); + totalOutputProdVolume += limit * MaterialInfo[mat].size; + totalProd += limit / producedMaterials.length; // output materials are produced together } - const city = warehouse.city; + const getProductProductionLimit = (product: Product) => product?.cityData[warehouse.city]?.productionLimit; + for (const product of products.values()) { if (product.finished) { - const limit = - product.cityData && product.cityData[city] && product.cityData[city].productionLimit - ? product.cityData[city].productionLimit! - : -1; - const factor = limit < 0 ? 1 : limit / prodMultProducts; - totalProductProdVolume += prodMultProducts * factor * product.size; - totalProductProd += prodMultProducts * factor; + const limit = Math.min(getProductProductionLimit(product) ?? Infinity, prodMultProducts); + totalProductProdVolume += limit * product.size; + totalProductProd += limit; } } From a0697c13541fc07306ded73af0cc05182954af9a Mon Sep 17 00:00:00 2001 From: tinion Date: Tue, 27 Aug 2024 22:02:06 +0200 Subject: [PATCH 10/10] Simplify code in SmartSupply Production Capacity functionality --- src/Corporation/SmartSupply.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Corporation/SmartSupply.ts b/src/Corporation/SmartSupply.ts index fa867d3634..1265b9e6c7 100644 --- a/src/Corporation/SmartSupply.ts +++ b/src/Corporation/SmartSupply.ts @@ -51,35 +51,34 @@ export function getProductionCapacity( let totalOutputProdVolume = 0; let totalProductProdVolume = 0; - let totalProd = 0; + let totalOutputProd = 0; let totalProductProd = 0; // Production limit is currently broken // The first lines are how it should be imo, but the third line is how it is currently in the game - // const getMatProductionLimit = (mat: CorpMaterialName) => warehouse.materials[mat].productionLimit ?? -1; - // const getMatProductionLimit = (mat: CorpMaterialName) => Math.min(...producedMaterials.map((mat) => warehouse.materials[mat].productionLimit!)) - const getMatProductionLimit = (__: any) => warehouse.materials[producedMaterials[0]].productionLimit; - - for (let i = 0; i < producedMaterials.length; i++) { - const mat = producedMaterials[i]; - const limit = Math.min(getMatProductionLimit(mat) ?? Infinity, prodMultOutput); + // const getMatProductionLimit = (mat: CorpMaterialName) => warehouse.materials[mat].productionLimit ?? Infinity; + // const getMatProductionLimit = (mat: CorpMaterialName) => Math.min(...producedMaterials.map((mat) => warehouse.materials[mat].productionLimit!)) ?? Infinity; + const getMatProductionLimit = (__: any) => warehouse.materials[producedMaterials[0]].productionLimit ?? Infinity; + const getProductProductionLimit = (product: Product) => + product?.cityData[warehouse.city]?.productionLimit ?? Infinity; + + producedMaterials.forEach((mat) => { + const limit = Math.min(getMatProductionLimit(mat), prodMultOutput); totalOutputProdVolume += limit * MaterialInfo[mat].size; - totalProd += limit / producedMaterials.length; // output materials are produced together - } - - const getProductProductionLimit = (product: Product) => product?.cityData[warehouse.city]?.productionLimit; + totalOutputProd += limit / producedMaterials.length; // output materials are produced together + }); - for (const product of products.values()) { + products.forEach((product) => { if (product.finished) { - const limit = Math.min(getProductProductionLimit(product) ?? Infinity, prodMultProducts); + const limit = Math.min(getProductProductionLimit(product), prodMultProducts); totalProductProdVolume += limit * product.size; totalProductProd += limit; } - } + }); // avg Out+Prod size = TotalVolume / TotalProduction - const averageSize = (totalOutputProdVolume + totalProductProdVolume) / (totalProd + totalProductProd); - return [averageSize, totalProd, totalProductProd]; + const averageSize = (totalOutputProdVolume + totalProductProdVolume) / (totalOutputProd + totalProductProd); + return [averageSize, totalOutputProd, totalProductProd]; } // Smart Supply Algorithm