Merge pull request #142 from hppeng-wynn/builder-performance-testing

Builder performance improvements
This commit is contained in:
hppeng-wynn 2022-07-17 23:34:06 -07:00 committed by GitHub
commit 8c050437fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 288 additions and 194 deletions

View file

@ -30,7 +30,6 @@ class Build{
this.level = level; this.level = level;
} else if (typeof level === "string") { } else if (typeof level === "string") {
this.level = level; this.level = level;
errors.push(new IncorrectInput(level, "a number", "level-choice"));
} else { } else {
errors.push("Level is not a string or number."); errors.push("Level is not a string or number.");
} }

View file

@ -407,7 +407,10 @@ class BuildAssembleNode extends ComputeNode {
input_map.get('guildTome1-input') input_map.get('guildTome1-input')
]; ];
let weapon = input_map.get('weapon-input'); let weapon = input_map.get('weapon-input');
let level = input_map.get('level-input'); let level = parseInt(input_map.get('level-input'));
if (isNaN(level)) {
level = 106;
}
let all_none = weapon.statMap.has('NONE'); let all_none = weapon.statMap.has('NONE');
for (const item of equipments) { for (const item of equipments) {

View file

@ -9,13 +9,13 @@ let iload_complete = false;
let ings; let ings;
let recipes; let recipes;
let ingMap; let ingMap = new Map();
let ingList = []; let ingList = [];
let recipeMap; let recipeMap;
let recipeList = []; let recipeList = [];
let ingIDMap; let ingIDMap = new Map();
let recipeIDMap; let recipeIDMap;
/* /*
@ -163,39 +163,41 @@ async function load_ing_init() {
} }
function init_ing_maps() { function init_ing_maps() {
ingMap = new Map();
recipeMap = new Map(); recipeMap = new Map();
ingIDMap = new Map();
recipeIDMap = new Map(); recipeIDMap = new Map();
let ing = Object(); let ing = {
ing.name = "No Ingredient"; name: "No Ingredient",
ing.displayName = "No Ingredient"; displayName: "No Ingredient",
ing.tier = 0; tier: 0,
ing.lvl = 0; lvl: 0,
ing.skills = ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING", "COOKING", "ALCHEMISM", "SCRIBING"]; skills: ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING", "COOKING", "ALCHEMISM", "SCRIBING"],
ing.ids= {}; ids: {},
ing.itemIDs = {"dura": 0, "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,}; itemIDs: {"dura": 0, "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,},
ing.consumableIDs = {"dura": 0, "charges": 0}; consumableIDs: {"dura": 0, "charges": 0},
ing.posMods = {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}; posMods: {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0},
ing.id = 4000; id: 4000
ingMap.set(ing["displayName"], ing); };
ingList.push(ing["displayName"]); ingMap.set(ing.displayName, ing);
ingIDMap.set(ing["id"], ing["displayName"]); ingList.push(ing.displayName);
ingIDMap.set(ing.id, ing.displayName);
let numerals = new Map([[1, "I"], [2, "II"], [3, "III"], [4, "IV"], [5, "V"], [6, "VI"]]); let numerals = new Map([[1, "I"], [2, "II"], [3, "III"], [4, "IV"], [5, "V"], [6, "VI"]]);
for (let i = 0; i < 5; i ++) { for (let i = 0; i < 5; i ++) {
for (const powderIng of powderIngreds) { for (const powderIng of powderIngreds) {
let ing = Object(); let ing = {
ing.name = "" + damageClasses[i+1] + " Powder " + numerals.get(powderIngreds.indexOf(powderIng) + 1); name: "" + damageClasses[i+1] + " Powder " + numerals.get(powderIngreds.indexOf(powderIng) + 1),
ing.displayName = ing.name tier: 0,
ing.tier = 0; lvl: 0,
ing.lvl = 0; skills: ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING"],
ing.skills = ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING"]; ids: {},
ing.ids = {}; isPowder: true,
ing.isPowder = true; pid: 6*i + powderIngreds.indexOf(powderIng),
ing.pid = 6*i + powderIngreds.indexOf(powderIng); itemIDs: {"dura": powderIng["durability"], "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0},
consumableIDs: {"dura": 0, "charges": 0},
posMods: {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}
};
ing.id = 4001 + ing.pid; ing.id = 4001 + ing.pid;
ing.itemIDs = {"dura": powderIng["durability"], "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,}; ing.diplayName = ing.name;
switch(i) { switch(i) {
case 0: case 0:
ing.itemIDs["strReq"] = powderIng["skpReq"]; ing.itemIDs["strReq"] = powderIng["skpReq"];
@ -213,24 +215,21 @@ function init_ing_maps() {
ing.itemIDs["agiReq"] = powderIng["skpReq"]; ing.itemIDs["agiReq"] = powderIng["skpReq"];
break; break;
} }
ing.consumableIDs = {"dura": 0, "charges": 0}; ingMap.set(ing.displayName, ing);
ing.posMods = {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}; ingList.push(ing.displayName);
ingMap.set(ing["displayName"],ing); ingIDMap.set(ing.id, ing.displayName);
ingList.push(ing["displayName"]);
ingIDMap.set(ing["id"], ing["displayName"]);
} }
} }
for (const ing of ings) { for (const ing of ings) {
ingMap.set(ing["displayName"], ing); ingMap.set(ing.displayName, ing);
ingList.push(ing["displayName"]); ingList.push(ing.displayName);
ingIDMap.set(ing["id"], ing["displayName"]); ingIDMap.set(ing.id, ing.displayName);
} }
for (const recipe of recipes) { for (const recipe of recipes) {
recipeMap.set(recipe["name"], recipe); recipeMap.set(recipe.name, recipe);
recipeList.push(recipe["name"]); recipeList.push(recipe.name);
recipeIDMap.set(recipe["id"],recipe["name"]); recipeIDMap.set(recipe.id, recipe.name);
} }
console.log(ingMap);
} }

View file

@ -1,32 +1,14 @@
function calculate_skillpoints(equipment, weapon) { /**
// Calculate equipment equipping order and required skillpoints. * Apply skillpoint bonuses from an item.
// Return value: [equip_order, best_skillpoints, final_skillpoints, best_total]; * Also applies set deltas.
let fixed = []; * Modifies the skillpoints array.
let consider = []; */
let noboost = []; function apply_skillpoints(skillpoints, item, activeSetCounts) {
let crafted = [];
for (const item of equipment) {
if (item.get("crafted")) {
crafted.push(item);
}
else if (item.get("reqs").every(x => x === 0) && item.get("skillpoints").every(x => x >= 0)) {
// All reqless item without -skillpoints.
fixed.push(item);
}
// TODO hack: We will treat ALL set items as unsafe :(
else if (item.get("skillpoints").every(x => x === 0) && item.get("set") === null) {
noboost.push(item);
}
else {
consider.push(item);
}
}
function apply_skillpoints(skillpoints, item, activeSetCounts) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
skillpoints[i] += item.get("skillpoints")[i]; skillpoints[i] += item.skillpoints[i];
} }
const setName = item.get("set"); const setName = item.set;
if (setName) { // undefined/null means no set. if (setName) { // undefined/null means no set.
let setCount = activeSetCounts.get(setName); let setCount = activeSetCounts.get(setName);
let old_bonus = {}; let old_bonus = {};
@ -45,32 +27,35 @@ function calculate_skillpoints(equipment, weapon) {
skillpoints[i] += delta; skillpoints[i] += delta;
} }
} }
} }
function apply_to_fit(skillpoints, item, skillpoint_min, activeSetCounts) { /**
* Apply skillpoints until this item can be worn.
* Also applies set deltas.
* Confusingly, does not modify the skillpoints array.
* Instead, return an array of deltas.
*/
function apply_to_fit(skillpoints, item, skillpoint_min, activeSetCounts) {
let applied = [0, 0, 0, 0, 0]; let applied = [0, 0, 0, 0, 0];
let total = 0;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (item.get("skillpoints")[i] < 0 && skillpoint_min[i]) { if (item.skillpoints[i] < 0 && skillpoint_min[i]) {
const unadjusted = skillpoints[i] + item.get("skillpoints")[i]; const unadjusted = skillpoints[i] + item.skillpoints[i];
const delta = skillpoint_min[i] - unadjusted; const delta = skillpoint_min[i] - unadjusted;
if (delta > 0) { if (delta > 0) {
applied[i] += delta; applied[i] += delta;
total += delta;
} }
} }
if (item.get("reqs")[i] == 0) continue; if (item.reqs[i] == 0) continue;
skillpoint_min[i] = Math.max(skillpoint_min[i], item.get("reqs")[i] + item.get("skillpoints")[i]); skillpoint_min[i] = Math.max(skillpoint_min[i], item.reqs[i] + item.skillpoints[i]);
const req = item.get("reqs")[i]; const req = item.reqs[i];
const cur = skillpoints[i]; const cur = skillpoints[i];
if (req > cur) { if (req > cur) {
const diff = req - cur; const diff = req - cur;
applied[i] += diff; applied[i] += diff;
total += diff;
} }
} }
const setName = item.get("set"); const setName = item.set;
if (setName) { // undefined/null means no set. if (setName) { // undefined/null means no set.
const setCount = activeSetCounts.get(setName); const setCount = activeSetCounts.get(setName);
if (setCount) { if (setCount) {
@ -84,15 +69,48 @@ function calculate_skillpoints(equipment, weapon) {
const delta = skillpoint_min[i] - unadjusted; const delta = skillpoint_min[i] - unadjusted;
if (delta > 0) { if (delta > 0) {
applied[i] += delta; applied[i] += delta;
total += delta;
} }
} }
} }
} }
} }
return applied;
}
function calculate_skillpoints(equipment, weapon) {
// const start = performance.now();
// Calculate equipment equipping order and required skillpoints.
// Return value: [equip_order, best_skillpoints, final_skillpoints, best_total];
let fixed = [];
let consider = [];
let noboost = [];
let crafted = [];
weapon.skillpoints = weapon.get('skillpoints');
weapon.reqs = weapon.get('reqs');
weapon.set = weapon.get('set');
for (const item of equipment) {
item.skillpoints = item.get('skillpoints');
item.reqs = item.get('reqs');
item.set = item.get('set');
if (item.get("crafted")) {
crafted.push(item);
}
// TODO hack: We will treat ALL set items as unsafe :(
else if (item.set !== null) {
consider.push(item);
}
else if (item.get("reqs").every(x => x === 0) && item.skillpoints.every(x => x >= 0)) {
// All reqless item without -skillpoints.
fixed.push(item);
}
else if (item.skillpoints.every(x => x <= 0)) {
noboost.push(item);
}
else {
consider.push(item);
}
}
return [applied, total];
}
// Separate out the no req items and add them to the static skillpoint base. // Separate out the no req items and add them to the static skillpoint base.
let static_skillpoints_base = [0, 0, 0, 0, 0] let static_skillpoints_base = [0, 0, 0, 0, 0]
@ -101,7 +119,7 @@ function calculate_skillpoints(equipment, weapon) {
apply_skillpoints(static_skillpoints_base, item, static_activeSetCounts); apply_skillpoints(static_skillpoints_base, item, static_activeSetCounts);
} }
let best = consider.concat(noboost); let best = consider;
let final_skillpoints = static_skillpoints_base.slice(); let final_skillpoints = static_skillpoints_base.slice();
let best_skillpoints = [0, 0, 0, 0, 0]; let best_skillpoints = [0, 0, 0, 0, 0];
let best_total = Infinity; let best_total = Infinity;
@ -110,100 +128,175 @@ function calculate_skillpoints(equipment, weapon) {
let allFalse = [0, 0, 0, 0, 0]; let allFalse = [0, 0, 0, 0, 0];
if (consider.length > 0 || noboost.length > 0 || crafted.length > 0) { if (consider.length > 0 || noboost.length > 0 || crafted.length > 0) {
// Try every combination and pick the best one. // Try every combination and pick the best one.
for (let permutation of perm(consider)) { const [root, terminal, sccs] = construct_scc_graph(consider);
let activeSetCounts = new Map(static_activeSetCounts); const end_checks = crafted.concat(noboost);
end_checks.push(weapon);
let has_skillpoint = allFalse.slice(); function check_end(skillpoints_applied, skillpoints, activeSetCounts, total_applied) {
// Crafted skillpoint does not count initially.
permutation = permutation.concat(noboost); for (const item of end_checks) {
const needed_skillpoints = apply_to_fit(skillpoints, item,
let skillpoints_applied = [0, 0, 0, 0, 0]; [false, false, false, false, false], activeSetCounts);
// Complete slice is a shallow copy.
let skillpoints = static_skillpoints_base.slice();
let total_applied = 0;
let result;
let needed_skillpoints;
let total_diff;
for (const item of permutation) {
result = apply_to_fit(skillpoints, item, has_skillpoint, activeSetCounts);
needed_skillpoints = result[0];
total_diff = result[1];
for (let i = 0; i < 5; ++i) { for (let i = 0; i < 5; ++i) {
skillpoints_applied[i] += needed_skillpoints[i]; const skp = needed_skillpoints[i]
skillpoints[i] += needed_skillpoints[i]; skillpoints_applied[i] += skp;
skillpoints[i] += skp;
total_applied += skp;
}
if (best_total < total_applied) { return -1; }
}
return total_applied;
}
function permute_check(idx, _applied, _skillpoints, _sets, _has, _total_applied, order) {
const {nodes, children} = sccs[idx];
if (nodes[0] === terminal) {
const total = check_end(_applied, _skillpoints, _sets, _total_applied);
if (total !== -1 && total < best_total) {
final_skillpoints = _skillpoints;
best_skillpoints = _applied;
best_total = total;
best_activeSetCounts = _sets;
best = order;
}
return;
}
for (let permutation of perm(nodes)) {
const skillpoints_applied = _applied.slice();
const skillpoints = _skillpoints.slice();
const activeSetCounts = new Map(_sets);
const has_skillpoint = _has.slice();
let total_applied = _total_applied;
let short_circuit = false;
for (const {item} of permutation) {
needed_skillpoints = apply_to_fit(skillpoints, item, has_skillpoint, activeSetCounts);
for (let i = 0; i < 5; ++i) {
skp = needed_skillpoints[i];
skillpoints_applied[i] += skp;
skillpoints[i] += skp;
total_applied += skp;
}
if (total_applied >= best_total) {
short_circuit = true;
break; // short circuit failure
} }
apply_skillpoints(skillpoints, item, activeSetCounts); apply_skillpoints(skillpoints, item, activeSetCounts);
total_applied += total_diff; }
if (total_applied >= best_total) { if (short_circuit) { continue; }
break; permute_check(idx+1, skillpoints_applied, skillpoints, activeSetCounts, has_skillpoint, total_applied, order.concat(permutation.map(x => x.item)));
} }
} }
// skip root.
permute_check(1, best_skillpoints, final_skillpoints, best_activeSetCounts, allFalse.slice(), 0, []);
// Crafted skillpoint does not count initially. // add extra sp bonus
for (const item of crafted) { apply_skillpoints(final_skillpoints, weapon, best_activeSetCounts);
//console.log(item)
result = apply_to_fit(skillpoints, item, allFalse.slice(), activeSetCounts);
//console.log(result)
needed_skillpoints = result[0];
total_diff = result[1];
for (let i = 0; i < 5; ++i) {
skillpoints_applied[i] += needed_skillpoints[i];
skillpoints[i] += needed_skillpoints[i];
}
total_applied += total_diff;
}
if (total_applied >= best_total) {
continue;
}
let pre = skillpoints.slice();
result = apply_to_fit(skillpoints, weapon, allFalse.slice(), activeSetCounts);
needed_skillpoints = result[0];
total_diff = result[1];
for (let i = 0; i < 5; ++i) {
skillpoints_applied[i] += needed_skillpoints[i];
skillpoints[i] += needed_skillpoints[i];
}
apply_skillpoints(skillpoints, weapon, activeSetCounts);
total_applied += total_diff;
// Applying crafted item skill points last. // Applying crafted item skill points last.
for (const item of crafted) { for (const item of crafted) {
apply_skillpoints(skillpoints, item, activeSetCounts); apply_skillpoints(final_skillpoints, item, best_activeSetCounts);
//total_applied += total_diff;
} }
if (total_applied < best_total) {
best = permutation;
final_skillpoints = skillpoints;
best_skillpoints = skillpoints_applied;
best_total = total_applied;
best_activeSetCounts = activeSetCounts;
}
}
} }
else { else {
best_total = 0; best_total = 0;
result = apply_to_fit(final_skillpoints, weapon, allFalse.slice(), best_activeSetCounts); needed_skillpoints = apply_to_fit(final_skillpoints, weapon, allFalse.slice(), best_activeSetCounts);
needed_skillpoints = result[0];
total_diff = result[1];
for (let i = 0; i < 5; ++i) { for (let i = 0; i < 5; ++i) {
best_skillpoints[i] += needed_skillpoints[i]; const skp = needed_skillpoints[i];
final_skillpoints[i] += needed_skillpoints[i]; best_skillpoints[i] += skp;
final_skillpoints[i] += skp;
best_skillpoints += skp;
} }
apply_skillpoints(final_skillpoints, weapon, best_activeSetCounts); apply_skillpoints(final_skillpoints, weapon, best_activeSetCounts);
best_total += total_diff;
} }
let equip_order = fixed.concat(best).concat(crafted); let equip_order = fixed.concat(best).concat(noboost).concat(crafted);
// best_skillpoints: manually assigned (before any gear) // best_skillpoints: manually assigned (before any gear)
// final_skillpoints: final totals (5 individ) // final_skillpoints: final totals (5 individ)
// best_total: total skillpoints assigned (number) // best_total: total skillpoints assigned (number)
// const end = performance.now();
// const output_msg = `skillpoint calculation took ${(end-start)/ 1000} seconds.`;
// console.log(output_msg);
return [equip_order, best_skillpoints, final_skillpoints, best_total, best_activeSetCounts]; return [equip_order, best_skillpoints, final_skillpoints, best_total, best_activeSetCounts];
} }
function construct_scc_graph(items_to_consider) {
let nodes = [];
let terminal_node = {
item: null,
children: [],
parents: nodes,
visited: false,
assigned: false,
scc: null
};
let root_node = {
item: null,
children: nodes,
parents: [],
visited: false,
assigned: false,
scc: null
};
for (const item of items_to_consider) {
nodes.push({item: item, children: [terminal_node], parents: [root_node], visited: false, assigned: false, scc: null});
}
// Dependency graph construction.
for (const node_a of nodes) {
const {item: a, children: a_children} = node_a;
for (const node_b of nodes) {
const {item: b, parents: b_parents} = node_b;
for (let i = 0; i < 5; ++i) {
if (a.skillpoints[i] > 0 && (a.reqs[i] < b.reqs[i] || b.skillpoints[i] < 0)) {
a_children.push(node_b);
b_parents.push(node_a);
break;
}
}
}
}
const res = []
/*
* SCC graph construction.
* https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm
*/
function visit(u, res) {
if (u.visited) { return; }
u.visited = true;
for (const child of u.children) {
if (!child.visited) { visit(child, res); }
}
res.push(u);
}
visit(root_node, res);
res.reverse();
const sccs = [];
function assign(node, cur_scc) {
if (node.assigned) { return; }
cur_scc.nodes.push(node);
node.scc = cur_scc;
node.assigned = true;
for (const parent of node.parents) {
assign(parent, cur_scc);
}
}
for (const node of res) {
if (node.assigned) { continue; }
const cur_scc = {
nodes: [],
children: new Set(),
parents: new Set()
};
assign(node, cur_scc);
sccs.push(cur_scc);
}
for (const scc of sccs) {
for (const node of scc.nodes) {
for (const child of node.children) {
scc.children.add(child.scc);
}
for (const parent of node.parents) {
scc.parents.add(parent.scc);
}
}
}
return [root_node, terminal_node, sccs];
}