From 3aea2bc5757356796913d79da2b84da5524890b5 Mon Sep 17 00:00:00 2001 From: Daniel Rauf Date: Sun, 4 Feb 2024 21:35:05 +0100 Subject: [PATCH] Extract logic to smaller methods, rename variables --- index.html | 140 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 58 deletions(-) diff --git a/index.html b/index.html index a3f1413..b1b3a0c 100644 --- a/index.html +++ b/index.html @@ -95,8 +95,8 @@

New Game+ Items

// Pre-calculate the cost of each level using the formula from DS3 wiki const MAX_LEVEL = 802; const levelCosts = [0, 0, 673, 689, 706, 723, 740, 757, 775, 793, 811, 829, 847]; - for (let i = 13; i <= MAX_LEVEL; i++) { - levelCosts[i] = Math.floor(0.02 * Math.pow(i, 3) + 3.06 * Math.pow(i, 2) + 105.6 * i - 895); + for (let level = 13; level <= MAX_LEVEL; level++) { + levelCosts[level] = Math.floor(0.02 * Math.pow(level, 3) + 3.06 * Math.pow(level, 2) + 105.6 * level - 895); } // Define the soul items @@ -129,14 +129,82 @@

New Game+ Items

// Generate the inputs for the soul items function generateInputs(items, containerId) { const container = document.getElementById(containerId); - for (const item of items) { - container.innerHTML += '
'; - } + items.forEach(item => { + const div = document.createElement('div'); + const label = document.createElement('label'); + const input = document.createElement('input'); + input.type = 'number'; + input.id = item.id; + input.min = "0"; + input.max = "999"; + input.value = "0"; + label.textContent = item.name + ": "; + label.appendChild(input); + div.appendChild(label); + container.appendChild(div); + }); } generateInputs(newGameItems, "newGameItems"); generateInputs(newGamePlusItems, "newGamePlusItems"); + // https://en.wikipedia.org/wiki/Knapsack_problem + function solveKnapsackProblem(maximum, items) { + const weights = Array.from({length: maximum + 1}, () => 0); + const usedItems = Array.from({length: maximum + 1}, () => []); + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const itemValue = items[itemIndex].value; + for (let remainingValueIndex = maximum; remainingValueIndex >= itemValue; remainingValueIndex--) { + for (let quantityIndex = items[itemIndex].quantity; quantityIndex >= 1; quantityIndex--) { + if (remainingValueIndex < quantityIndex * itemValue) continue; + const newValue = weights[remainingValueIndex - quantityIndex * itemValue] + quantityIndex * itemValue; + if (weights[remainingValueIndex] < newValue) { + weights[remainingValueIndex] = newValue; + usedItems[remainingValueIndex] = [...usedItems[remainingValueIndex - quantityIndex * itemValue], ...Array(quantityIndex).fill(items[itemIndex].id)]; + items[itemIndex].quantity -= quantityIndex; + break; + } + } + } + } + + const finalItemsValue = weights[maximum]; + const finalUsedItems = usedItems[maximum]; + return {finalItemsValue, finalUsedItems}; + } + + function calculateUsedItems(soulsLeftAfterLevellingUp, inventory) { + // use the fact that all item values are multiples of 50 to reduce the problem size + const ITEM_VALUE_MULTIPLIER = 50; + const scaledSoulsLeftAfterLevellingUp = Math.floor(soulsLeftAfterLevellingUp / ITEM_VALUE_MULTIPLIER); + const scaledInventory = JSON.parse(JSON.stringify(inventory)).map(item => { + item.value = Math.floor(item.value / ITEM_VALUE_MULTIPLIER); + return item; + }) + + // solve the knapsack problem, i.e. find the combination of items that goes as close to soulsLeftAfterLevellingUp + // as possible without going over - we look for items we can skip consuming and still reach the same level + const { + finalItemsValue, + finalUsedItems + } = solveKnapsackProblem(scaledSoulsLeftAfterLevellingUp, scaledInventory); + const unusedItemsSoulsValue = finalItemsValue * ITEM_VALUE_MULTIPLIER; + + // because we calculated items that we DO NOT consume, we need to "flip" the result to get the items we DO consume + const unusedQuantities = {}; + finalUsedItems.forEach(id => { + unusedQuantities[id] = (unusedQuantities[id] || 0) + 1; + }); + + const usedItems = inventory.map(item => ({ + ...item, + quantity: item.quantity - (unusedQuantities[item.id] || 0) + })).filter(item => item.quantity > 0); + + return {unusedItemsSoulsValue, usedItems}; + } + function optimizeLevelUp(allItems, levelCosts) { // fetch current level, soft souls & quantity of each item const currLevel = parseInt(document.getElementById("currLevel").value, 10) || 1; @@ -147,20 +215,17 @@

New Game+ Items

quantity: parseInt(document.getElementById(item.id).value, 10) || 0 } }).filter(item => item.quantity > 0); - console.debug("Inventory", inventory); // calculate total souls available const totalSoulsAvailable = softSouls + inventory.reduce((sum, item) => sum + (item.value * item.quantity), 0); - console.debug("Total souls available", totalSoulsAvailable); + // calculate the highest level that can be reached with the available souls let soulsLeftAfterLevellingUp = totalSoulsAvailable; let maxLevel = currLevel; while (levelCosts[maxLevel + 1] <= soulsLeftAfterLevellingUp) { soulsLeftAfterLevellingUp -= levelCosts[maxLevel + 1]; maxLevel++; } - console.debug("Souls left after levelling up", soulsLeftAfterLevellingUp); - console.debug("Max level", maxLevel); // if maxLevel is already at the cap, return early to not waste time on the knapsack problem if (maxLevel >= MAX_LEVEL) { @@ -184,67 +249,26 @@

New Game+ Items

usedItems: [] }; } + const {unusedItemsSoulsValue, usedItems} = calculateUsedItems(soulsLeftAfterLevellingUp, inventory); - // solve the knapsack problem for soulsLeftAfterLevelUp, using the fact that all values are multiples of 50 - // i.e. we look for the combination of items that goes as close to soulsLeftAfterLevelUp as possible - const soulsLeftAfterLevelUp = Math.floor(soulsLeftAfterLevellingUp / 50); - let dp = Array.from({length: soulsLeftAfterLevelUp + 1}, () => 0); - let unusedItems = Array.from({length: soulsLeftAfterLevelUp + 1}, () => []); - - // a deep copy of the inventory is needed because we will modify the quantity of items - const items = JSON.parse(JSON.stringify(inventory)); - for (let i = 0; i < items.length; i++) { - const itemValue = items[i].value / 50; - for (let j = soulsLeftAfterLevelUp; j >= itemValue; j--) { - for (let k = items[i].quantity; k >= 1; k--) { - if (j < k * itemValue) continue; - const newValue = dp[j - k * itemValue] + k * itemValue; - if (dp[j] < newValue) { - dp[j] = newValue; - unusedItems[j] = [...unusedItems[j - k * itemValue], ...Array(k).fill(items[i].id)]; - items[i].quantity -= k; - break; - } - } - } - } - console.debug("Unused items", unusedItems); - - // track the quantities of unused items (or: items used in the knapsack algorithm) - let unusedQuantities = {}; - unusedItems[soulsLeftAfterLevelUp].forEach(id => { - unusedQuantities[id] = (unusedQuantities[id] || 0) + 1; - }); - console.debug("Unused quantities", unusedQuantities); - - // subtract the quantities of unused items from the original inventory to see what was actually used - let usedItems = inventory.map(item => ({ - ...item, - quantity: item.quantity - (unusedQuantities[item.id] || 0) - })).filter(item => item.quantity > 0); - console.debug("Used items", usedItems); - - // calculate remaining souls - const unusedItemSoulsValue = dp[soulsLeftAfterLevelUp] * 50; - const usedSouls = Math.min(totalSoulsAvailable - unusedItemSoulsValue, levelCosts.slice(currLevel + 1, maxLevel + 1).reduce((a, b) => a + b, 0)); - const remainingSoftSouls = Math.max(0, totalSoulsAvailable - usedSouls - unusedItemSoulsValue); - + const usedSouls = Math.min(totalSoulsAvailable - unusedItemsSoulsValue, levelCosts.slice(currLevel + 1, maxLevel + 1).reduce((a, b) => a + b, 0)); + const remainingSoftSouls = Math.max(0, totalSoulsAvailable - usedSouls - unusedItemsSoulsValue); return { highestLevel: maxLevel, nextLevelCost: levelCosts[maxLevel + 1], - soulsUsed: usedSouls ? usedSouls : 0, - remainingSoftSouls: remainingSoftSouls ? remainingSoftSouls : 0, - remainingItemsSouls: unusedItemSoulsValue, + soulsUsed: usedSouls, + remainingSoftSouls: remainingSoftSouls, + remainingItemsSouls: unusedItemsSoulsValue, usedItems: usedItems }; } function calculate() { const result = optimizeLevelUp(newGameItems.concat(newGamePlusItems), levelCosts); - console.debug("Calculation result", result); const levelInfo = `

Highest Level: ${result.highestLevel}
Souls to Next Level: ${result.nextLevelCost}

`; - const soulsInfo = `

Total souls used: ${result.soulsUsed}
Leftover soft souls: ${result.remainingSoftSouls}
Leftover souls in consumables: ${result.remainingItemsSouls}

`; + const leftoverSouls = result.remainingSoftSouls + result.remainingItemsSouls; + const soulsInfo = `

Total souls used: ${result.soulsUsed}
Total leftover souls: ${leftoverSouls}
Leftover soft souls: ${result.remainingSoftSouls}
Leftover souls in consumables: ${result.remainingItemsSouls}

`; const usedItemsList = result.usedItems.length === 0 ? '
  • None
  • ' : `${result.usedItems.map(item => `
  • ${item.quantity}x ${item.name}
  • `).join("")}`; const usedItemsInfo = `

    Consumables used:

    `;