ソースを参照

gov: calculate daily cost

raylu 1 週間 前
コミット
c160603c21
3 ファイル変更105 行追加27 行削除
  1. 1 1
      ts/buy.ts
  2. 100 26
      ts/gov.ts
  3. 4 0
      www/style.css

+ 1 - 1
ts/buy.ts

@@ -169,7 +169,7 @@ async function calcForCX(username: string, apiKey: string, supplyForDays: number
 }
 }
 
 
 async function getPrices(cx: string) {
 async function getPrices(cx: string) {
-	const rawPrices= await cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json');
+	const rawPrices = await cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json');
 	const prices = new Map<string, RawPrice>();
 	const prices = new Map<string, RawPrice>();
 	for (const p of rawPrices)
 	for (const p of rawPrices)
 		if (p.ExchangeCode === cx)
 		if (p.ExchangeCode === cx)

+ 100 - 26
ts/gov.ts

@@ -51,10 +51,11 @@ async function render(planetName: string, pop: Pop) {
 
 
 async function _render(planetName: string, pop: Pop) {
 async function _render(planetName: string, pop: Pop) {
 	const encodedPlanetName = encodeURIComponent(planetName);
 	const encodedPlanetName = encodeURIComponent(planetName);
-	const [planet, siteCounts, infras]: [Planet, SiteCount[], Infrastructure[]] = await Promise.all([
+	const [planet, siteCounts, infras, allPrices]: [Planet, SiteCount[], Infrastructure[], Price[]] = await Promise.all([
 		cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
 		cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
 		cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
 		cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
-		cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`)
+		cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`),
+		cachedFetchJSON('https://api.prunplanner.org/data/exchanges'),
 	]);
 	]);
 
 
 	let lastPOPR = null;
 	let lastPOPR = null;
@@ -86,7 +87,10 @@ async function _render(planetName: string, pop: Pop) {
 			currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
 			currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
 	}
 	}
 
 
-	// gen
+	const prices: Map<string, number> = new Map();
+	for (const price of allPrices)
+		if (price.exchange_code === 'IC1')
+			prices.set(price.ticker, price.vwap_30d);
 
 
 	renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
 	renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
 	<br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
 	<br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
@@ -164,11 +168,12 @@ async function _render(planetName: string, pop: Pop) {
 
 
 	<h2>options</h2>
 	<h2>options</h2>
 	current projected ${pop} happiness: ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled))}
 	current projected ${pop} happiness: ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled))}
-	<table>
-	${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled).map((result) => {
+	<table class="options">
+	${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, prices).map((result) => {
 		return `<tr>
 		return `<tr>
 			<td>${[...result.config.entries()].map(([building, fill]) => `${building}: ${fill.numMats}`).join(', ')}</td>
 			<td>${[...result.config.entries()].map(([building, fill]) => `${building}: ${fill.numMats}`).join(', ')}</td>
 			<td>${formatPct(result.happiness)}</td>
 			<td>${formatPct(result.happiness)}</td>
+			<td>${formatNum(result.cost)}/day</td>
 		</tr>`;
 		</tr>`;
 	}).join('')}
 	}).join('')}
 	</table>
 	</table>
@@ -224,12 +229,12 @@ function calcPOPIFilled(infra: Infrastructure): number | null {
 	return filled;
 	return filled;
 }
 }
 
 
-function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill):
-		{config: POPIFill, happiness: number, cost: number}[] {
+function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
+		prices: Map<string, number>): {config: POPIFill, happiness: number, cost: number}[] {
 	const results: {config: POPIFill, happiness: number, cost: number}[] = [];
 	const results: {config: POPIFill, happiness: number, cost: number}[] = [];
 	for (const config of popiFillCombinations(currentPOPIFilled)) {
 	for (const config of popiFillCombinations(currentPOPIFilled)) {
 		const happiness = projectedHappiness(pop, totalNeeds, siteCount, config);
 		const happiness = projectedHappiness(pop, totalNeeds, siteCount, config);
-		let cost = config.values().reduce((sum, fill) => sum + fill.numMats * fill.tier, 0); // TODO
+		let cost = calcCost(config, prices);
 		// is any result better than this one?
 		// is any result better than this one?
 		if (results.some((result) => (result.happiness >= happiness && result.cost < cost) ||
 		if (results.some((result) => (result.happiness >= happiness && result.cost < cost) ||
 				(result.happiness > happiness && result.cost <= cost)))
 				(result.happiness > happiness && result.cost <= cost)))
@@ -253,7 +258,8 @@ function* popiFillCombinations(currentPOPIFilled: POPIFill): Generator<POPIFill>
 		}
 		}
 		const building = entries[index];
 		const building = entries[index];
 		const tier = currentPOPIFilled.get(building)!.tier;
 		const tier = currentPOPIFilled.get(building)!.tier;
-		for (let i = 0; i <= POPI[building].max; i++) {
+		const maxBuildingMats = Object.keys(POPI[building].mats).length;
+		for (let i = 0; i <= maxBuildingMats; i++) {
 			current.set(building, {tier, numMats: i});
 			current.set(building, {tier, numMats: i});
 			yield* helper(index + 1, current);
 			yield* helper(index + 1, current);
 		}
 		}
@@ -265,9 +271,10 @@ function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCoun
 	let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
 	let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
 		'comfort': 0, 'culture': 0, 'education': 0};
 		'comfort': 0, 'culture': 0, 'education': 0};
 	for (const [building, fill] of popiFilled.entries()) {
 	for (const [building, fill] of popiFilled.entries()) {
-		const {max, needs} = POPI[building];
+		const {needs} = POPI[building];
+		const maxBuildingMats = Object.keys(POPI[building].mats).length;
 		for (const {need, supplied} of needs) {
 		for (const {need, supplied} of needs) {
-			const provided = fill.numMats / max * fill.tier * supplied;
+			const provided = fill.numMats / maxBuildingMats * fill.tier * supplied;
 			totalProvided[need] += provided;
 			totalProvided[need] += provided;
 		}
 		}
 	}
 	}
@@ -293,6 +300,25 @@ function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCoun
 	return happiness;
 	return happiness;
 }
 }
 
 
+function calcCost(config: POPIFill, prices: Map<string, number>): number {
+	let cost = 0;
+	for (const [building, fill] of config.entries()) {
+		const {mats} = POPI[building];
+		const matPrices: {ticker: string, costPerDay: number}[] = [];
+		for (const [mat, amount] of Object.entries(mats)) {
+			const matCost = prices.get(mat)!;
+			if (matCost === undefined)
+				throw new Error('no price for ' + mat);
+			matPrices.push({ticker: mat, costPerDay: matCost * amount});
+		}
+		matPrices.sort((a, b) => a.costPerDay - b.costPerDay);
+
+		for (let i = 0; i < fill.numMats; i++)
+			cost += matPrices[i].costPerDay * fill.tier;
+	}
+	return cost;
+}
+
 const NEEDS: Record<Pop, Record<Need, number>> = {
 const NEEDS: Record<Pop, Record<Need, number>> = {
 	'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
 	'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
 	'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
 	'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
@@ -300,21 +326,63 @@ const NEEDS: Record<Pop, Record<Need, number>> = {
 	'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
 	'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
 	'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
 	'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
 }
 }
-const POPI: Record<POPIBuilding, {max: number, needs: {need: Need, supplied: number}[]}> = {
-	'SAFETY_STATION': {max: 3, needs: [{need: 'safety', supplied: 2500}]},
-	'SECURITY_DRONE_POST': {max: 4, needs: [{need: 'safety', supplied: 5000}]},
-	'EMERGENCY_CENTER': {max: 5, needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}]},
-	'INFIRMARY': {max: 3, needs: [{need: 'health', supplied: 2500}]},
-	'HOSPITAL': {max: 6, needs: [{need: 'health', supplied: 5000}]},
-	'WELLNESS_CENTER': {max: 6, needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}]},
-	'WILDLIFE_PARK': {max: 5, needs: [{need: 'comfort', supplied: 2500}]},
-	'ARCADES': {max: 6, needs: [{need: 'comfort', supplied: 5000}]},
-	'ART_CAFE': {max: 6, needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}]},
-	'ART_GALLERY': {max: 4, needs: [{need: 'culture', supplied: 2500}]},
-	'THEATER': {max: 6, needs: [{need: 'culture', supplied: 5000}]},
-	'PLANETARY_BROADCASTING_HUB': {max: 6, needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}]},
-	'LIBRARY': {max: 5, needs: [{need: 'education', supplied: 2500}]},
-	'UNIVERSITY': {max: 6, needs: [{need: 'education', supplied: 5000}]},
+const POPI: Record<POPIBuilding, {needs: {need: Need, supplied: number}[], mats: Record<string, number>}> = {
+	'SAFETY_STATION': {
+		needs: [{need: 'safety', supplied: 2500}],
+		mats: {'DW': 10, 'OFF': 10, 'SUN': 2},
+	},
+	'SECURITY_DRONE_POST': {
+		needs: [{need: 'safety', supplied: 5000}],
+		mats: {'POW': 1, 'RAD': 0.47, 'CCD': 0.07, 'SUD': 0.07},
+	},
+	'EMERGENCY_CENTER': {
+		needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}],
+		mats: {'PK': 2, 'POW': 0.4, 'BND': 4, 'RED': 0.07, 'BSC': 0.07},
+	},
+	'INFIRMARY': {
+		needs: [{need: 'health', supplied: 2500}],
+		mats: {'OFF': 10, 'TUB': 6.67, 'STR': 0.67},
+	},
+	'HOSPITAL': {
+		needs: [{need: 'health', supplied: 5000}],
+		mats: {'PK': 2, 'SEQ': 0.4, 'BND': 4, 'SDR': 0.07, 'RED': 0.07, 'BSC': 0.13},
+	},
+	'WELLNESS_CENTER': {
+		needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}],
+		mats: {'KOM': 4, 'OLF': 2, 'DW': 6, 'DEC': 0.67, 'PFE': 2.67, 'SOI': 6.67},
+	},
+	'WILDLIFE_PARK': {
+		needs: [{need: 'comfort', supplied: 2500}],
+		mats: {'DW': 10, 'FOD': 6, 'PFE': 2, 'SOI': 3.33, 'DEC': 0.33},
+	},
+	'ARCADES': {
+		needs: [{need: 'comfort', supplied: 5000}],
+		mats: {'POW': 2, 'MHP': 2, 'OLF': 4, 'BID': 0.2, 'HOG': 0.2, 'EDC': 0.2},
+	},
+	'ART_CAFE': {
+		needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}],
+		mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
+	},
+	'ART_GALLERY': {
+		needs: [{need: 'culture', supplied: 2500}],
+		mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
+	},
+	'THEATER': {
+		needs: [{need: 'culture', supplied: 5000}],
+		mats: {'POW': 1.4, 'MHP': 2, 'HOG': 1.4, 'OLF': 4, 'BID': 0.33, 'DEC': 0.67},
+	},
+	'PLANETARY_BROADCASTING_HUB': {
+		needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}],
+		mats: {'OFF': 10, 'MHP': 1, 'SP': 1.33, 'AAR': 0.67, 'EDC': 0.27, 'IDC': 0.13},
+	},
+	'LIBRARY': {
+		needs: [{need: 'education', supplied: 2500}],
+		mats: {'MHP': 1, 'HOG': 1, 'CD': 0.33, 'DIS': 0.33, 'BID': 0.2},
+	},
+	'UNIVERSITY': {
+		needs: [{need: 'education', supplied: 5000}],
+		mats: {'COF': 10, 'REA': 10, 'TUB': 10, 'BID': 0.33, 'HD': 0.67, 'IDC': 0.2},
+	},
 };
 };
 
 
 type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
 type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
@@ -367,3 +435,9 @@ interface Infrastructure {
 	CurrentLevel: number;
 	CurrentLevel: number;
 	Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
 	Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
 }
 }
+
+interface Price {
+	ticker: string
+	exchange_code: string
+	vwap_30d: number
+}

+ 4 - 0
www/style.css

@@ -111,6 +111,10 @@ main.gov {
 	}
 	}
 	div#gov {
 	div#gov {
 		margin-top: 2em;
 		margin-top: 2em;
+
+		table.options td:first-child {
+			text-align: initial;
+		}
 	}
 	}
 	.positive {
 	.positive {
 		color: #0aa;
 		color: #0aa;