178 lines
5.3 KiB
JavaScript
178 lines
5.3 KiB
JavaScript
|
import { readFileSync } from 'node:fs';
|
||
|
|
||
|
const input = readFileSync('input', 'utf-8');
|
||
|
|
||
|
// Materials
|
||
|
const M = {
|
||
|
ORE: 0,
|
||
|
CLAY: 1,
|
||
|
OBSIDIAN: 2,
|
||
|
GEODE: 3
|
||
|
};
|
||
|
|
||
|
const blueprints = input
|
||
|
.split('\n')
|
||
|
.filter(line => !!line.trim())
|
||
|
.map(line => {
|
||
|
const [details, robotsData] = line.split(/:\s+/);
|
||
|
const blueprintId = parseInt(details.match(/\d+/)[0]);
|
||
|
|
||
|
const robots = robotsData
|
||
|
.split(/\.\s*(?!$)/)
|
||
|
.map(robotData => {
|
||
|
const robotType = robotData.match(/(\w+)\s+robot/)[1];
|
||
|
const index = Object.entries(M).find(([name]) =>
|
||
|
name === robotType.toUpperCase()
|
||
|
)[1];
|
||
|
const costs = [
|
||
|
parseInt(robotData.match(/\d+(?=\s+ore)/)?.[0] ?? 0),
|
||
|
parseInt(robotData.match(/\d+(?=\s+clay)/)?.[0] ?? 0),
|
||
|
parseInt(robotData.match(/\d+(?=\s+obsidian)/)?.[0] ?? 0),
|
||
|
0
|
||
|
];
|
||
|
return [index, costs];
|
||
|
})
|
||
|
.reduce((arr, [index, costs]) => {
|
||
|
arr[index] = costs;
|
||
|
return arr;
|
||
|
}, []);
|
||
|
|
||
|
return {
|
||
|
id: blueprintId,
|
||
|
robots
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// Attempt to build the given robot and apply changes to the inventory. Return
|
||
|
// new robots and inventory arrays, or undefined if unable to build
|
||
|
function build(blueprint, robotType, inventory, robots) {
|
||
|
const newInventory = [];
|
||
|
for (const [material, cost] of blueprint.robots[robotType].entries()) {
|
||
|
const newQuantity = inventory[material] - cost;
|
||
|
if (newQuantity < 0) return;
|
||
|
newInventory.push(newQuantity);
|
||
|
}
|
||
|
const newRobots = [...robots];
|
||
|
newRobots[robotType] += 1;
|
||
|
return [newInventory, newRobots];
|
||
|
}
|
||
|
|
||
|
// Calculate the maximum possible number of geodes that could be cracked within
|
||
|
// the remaining time assuming an infinite inventory
|
||
|
function getMaxPossibleGeodes(state, timeRemaining) {
|
||
|
const geodeRobotCount = state[1][M.GEODE];
|
||
|
const geodeCount = state[0][M.GEODE];
|
||
|
|
||
|
return (
|
||
|
geodeCount +
|
||
|
(timeRemaining * geodeRobotCount) +
|
||
|
((timeRemaining * Math.max(0, timeRemaining - 1)) / 2)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
function getMaximumGeodes(blueprint, totalTime) {
|
||
|
let states = [[[0, 0, 0, 0], [1, 0, 0, 0]]];
|
||
|
|
||
|
// Determine the worst case scenario per-turn requirements for each material
|
||
|
const requirements = Object.keys(M).map(material =>
|
||
|
blueprint.robots.reduce(
|
||
|
(max, r) => Math.max(max, r[M[material]]),
|
||
|
-Infinity
|
||
|
)
|
||
|
);
|
||
|
|
||
|
for (let time = 0; time < totalTime; time++) {
|
||
|
const timeRemaining = totalTime - time;
|
||
|
const newStates = [];
|
||
|
|
||
|
// Prune
|
||
|
states = states.filter((stateA, ia, states) => {
|
||
|
// Remove duplicates
|
||
|
return states.findLastIndex(stateB =>
|
||
|
stateB[0].every((c, i) => c === stateA[0][i]) &&
|
||
|
stateB[1].every((c, i) => c === stateA[1][i])
|
||
|
) === ia;
|
||
|
}).filter((stateA, ia) => {
|
||
|
// Maximum possible number of geodes that could be created from this
|
||
|
// state assuming an infinite inventory
|
||
|
const fantasyA = getMaxPossibleGeodes(stateA, timeRemaining);
|
||
|
|
||
|
// Filter out states where there exists a better state
|
||
|
for (let ib = 0; ib < states.length; ib++) {
|
||
|
const stateB = states[ib];
|
||
|
|
||
|
// If stateB has more geodes than stateA could ever possibly hope to
|
||
|
// gather then discard stateA (minor optimization)
|
||
|
if (stateB[0][M.GEODE] > fantasyA) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Discard stateA if stateB is objectively better (all values larger or
|
||
|
// the same, at least one larger)
|
||
|
if (
|
||
|
ib !== ia &&
|
||
|
stateB[0].every((c, i) => c >= stateA[0][i]) &&
|
||
|
stateB[1].every((c, i) => c >= stateA[1][i]) && (
|
||
|
stateB[0].some((c, i) => c > stateA[0][i]) ||
|
||
|
stateB[1].some((c, i) => c > stateA[1][i])
|
||
|
)
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
});
|
||
|
|
||
|
// Process states
|
||
|
for (const [inventory, robots] of states) {
|
||
|
// Calculate inventory thresholds after which there is no longer any
|
||
|
// incentive to build more of that robot
|
||
|
const thresholds = Object.keys(M).map(material =>
|
||
|
(requirements[M[material]] - robots[M[material]]) * (timeRemaining - 1)
|
||
|
);
|
||
|
const buildOre = inventory[M.ORE] < thresholds[M.ORE];
|
||
|
const buildClay = inventory[M.CLAY] < thresholds[M.CLAY];
|
||
|
const buildObsidian = inventory[M.OBSIDIAN] < thresholds[M.OBSIDIAN];
|
||
|
|
||
|
let choices = [
|
||
|
// do nothing
|
||
|
[[...inventory], [...robots]],
|
||
|
buildOre && build(blueprint, M.ORE, inventory, robots),
|
||
|
buildClay && build(blueprint, M.CLAY, inventory, robots),
|
||
|
buildObsidian && build(blueprint, M.OBSIDIAN, inventory, robots),
|
||
|
build(blueprint, M.GEODE, inventory, robots)
|
||
|
].filter(choice => !!choice);
|
||
|
|
||
|
choices = choices.map(([cInv, cRob]) => [
|
||
|
cInv.map((c, i) => c + robots[i]),
|
||
|
cRob
|
||
|
]);
|
||
|
|
||
|
newStates.push(...choices);
|
||
|
}
|
||
|
states = newStates;
|
||
|
}
|
||
|
return states.reduce((max, state) => Math.max(max, state[0][M.GEODE]), 0);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
let totalQuality = 0;
|
||
|
for (const blueprint of blueprints) {
|
||
|
const geodes = getMaximumGeodes(blueprint, 24);
|
||
|
const quality = blueprint.id * geodes;
|
||
|
totalQuality += quality
|
||
|
}
|
||
|
console.log('The sum of all blueprint quality levels is:', totalQuality);
|
||
|
|
||
|
|
||
|
let totalMultiplied = 1;
|
||
|
for (const blueprint of blueprints.slice(0, 3)) {
|
||
|
totalMultiplied *= getMaximumGeodes(blueprint, 32);
|
||
|
}
|
||
|
console.log(
|
||
|
'Multiplication of max geodes for first three blueprints:',
|
||
|
totalMultiplied
|
||
|
);
|