mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Items in the optimization result are now shown in the same order as the wanted list.
This commit is contained in:
parent
583ef859a3
commit
30464680cb
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Editors
|
||||
.vscode
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
@ -16,16 +16,23 @@ export class WantedItem {
|
||||
}
|
||||
}
|
||||
|
||||
export class OptimizationResult {
|
||||
public readonly totalTime: number;
|
||||
export class OptimalResult {
|
||||
constructor(
|
||||
readonly wantedItems: Array<Item>,
|
||||
readonly optimalMethods: Array<OptimalMethod>
|
||||
) { }
|
||||
}
|
||||
|
||||
export class OptimalMethod {
|
||||
readonly totalTime: number;
|
||||
|
||||
constructor(
|
||||
public readonly difficulty: Difficulty,
|
||||
public readonly sectionIds: Array<SectionId>,
|
||||
public readonly methodName: string,
|
||||
public readonly methodTime: number,
|
||||
public readonly runs: number,
|
||||
public readonly itemCounts: Map<Item, number>
|
||||
readonly difficulty: Difficulty,
|
||||
readonly sectionIds: Array<SectionId>,
|
||||
readonly methodName: string,
|
||||
readonly methodTime: number,
|
||||
readonly runs: number,
|
||||
readonly itemCounts: Map<Item, number>
|
||||
) {
|
||||
this.totalTime = runs * methodTime;
|
||||
}
|
||||
@ -39,7 +46,7 @@ export class OptimizationResult {
|
||||
// TODO: boxes.
|
||||
class HuntOptimizerStore {
|
||||
@observable readonly wantedItems: IObservableArray<WantedItem> = observable.array();
|
||||
@observable readonly results: IObservableArray<OptimizationResult> = observable.array();
|
||||
@observable result?: OptimalResult;
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
@ -92,10 +99,14 @@ class HuntOptimizerStore {
|
||||
|
||||
optimize = async () => {
|
||||
if (!this.wantedItems.length) {
|
||||
this.results.splice(0);
|
||||
this.result = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize this set before awaiting data, so user changes don't affect this optimization
|
||||
// run from this point on.
|
||||
const wantedItems = new Set(this.wantedItems.filter(w => w.amount > 0).map(w => w.item));
|
||||
|
||||
const methods = await huntMethodStore.methods.current.promise;
|
||||
const dropTable = await itemDropStore.enemyDrops.current.promise;
|
||||
|
||||
@ -125,8 +136,6 @@ class HuntOptimizerStore {
|
||||
}
|
||||
const variableDetails: Map<string, VariableDetails> = new Map();
|
||||
|
||||
const wantedItems = new Set(this.wantedItems.filter(w => w.amount > 0).map(w => w.item));
|
||||
|
||||
for (const method of methods) {
|
||||
// Counts include rare enemies, so they are fractional.
|
||||
const counts = new Map<NpcType, number>();
|
||||
@ -158,22 +167,25 @@ class HuntOptimizerStore {
|
||||
// migiums and hidooms.
|
||||
const countsList: Array<Map<NpcType, number>> = [counts];
|
||||
const panArmsCount = counts.get(NpcType.PanArms);
|
||||
const panArms2Count = counts.get(NpcType.PanArms2);
|
||||
|
||||
if (panArmsCount || panArms2Count) {
|
||||
if (panArmsCount) {
|
||||
const splitCounts = new Map(counts);
|
||||
|
||||
if (panArmsCount) {
|
||||
splitCounts.delete(NpcType.PanArms);
|
||||
splitCounts.set(NpcType.Migium, panArmsCount);
|
||||
splitCounts.set(NpcType.Hidoom, panArmsCount);
|
||||
}
|
||||
splitCounts.delete(NpcType.PanArms);
|
||||
splitCounts.set(NpcType.Migium, panArmsCount);
|
||||
splitCounts.set(NpcType.Hidoom, panArmsCount);
|
||||
|
||||
if (panArms2Count) {
|
||||
splitCounts.delete(NpcType.PanArms2);
|
||||
splitCounts.set(NpcType.Migium2, panArms2Count);
|
||||
splitCounts.set(NpcType.Hidoom2, panArms2Count);
|
||||
}
|
||||
countsList.push(splitCounts);
|
||||
}
|
||||
|
||||
const panArms2Count = counts.get(NpcType.PanArms2);
|
||||
|
||||
if (panArms2Count) {
|
||||
const splitCounts = new Map(counts);
|
||||
|
||||
splitCounts.delete(NpcType.PanArms2);
|
||||
splitCounts.set(NpcType.Migium2, panArms2Count);
|
||||
splitCounts.set(NpcType.Hidoom2, panArms2Count);
|
||||
|
||||
countsList.push(splitCounts);
|
||||
}
|
||||
@ -184,9 +196,12 @@ class HuntOptimizerStore {
|
||||
|
||||
for (const diff of Difficulties) {
|
||||
for (const sectionId of SectionIds) {
|
||||
// Will contain an entry per wanted item dropped by enemies in this method/
|
||||
// difficulty/section ID combo.
|
||||
const variable: Variable = {
|
||||
time: method.time
|
||||
};
|
||||
// Only add the variable if the method provides at least 1 item we want.
|
||||
let addVariable = false;
|
||||
|
||||
for (const [npcType, count] of counts.entries()) {
|
||||
@ -220,6 +235,9 @@ class HuntOptimizerStore {
|
||||
feasible: boolean,
|
||||
bounded: boolean,
|
||||
result: number,
|
||||
/**
|
||||
* Value will always be a number if result is indexed with an actual method name.
|
||||
*/
|
||||
[method: string]: number | boolean
|
||||
} = solver.Solve({
|
||||
optimize: 'time',
|
||||
@ -228,73 +246,78 @@ class HuntOptimizerStore {
|
||||
variables
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.results.splice(0);
|
||||
if (!result.feasible) {
|
||||
this.result = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.feasible) {
|
||||
return;
|
||||
}
|
||||
const optimalMethods: Array<OptimalMethod> = [];
|
||||
|
||||
for (const [variableName, runsOrOther] of Object.entries(result)) {
|
||||
const details = variableDetails.get(variableName);
|
||||
// Loop over the entries in result, ignore standard properties that aren't variables.
|
||||
for (const [variableName, runsOrOther] of Object.entries(result)) {
|
||||
const details = variableDetails.get(variableName);
|
||||
|
||||
if (details) {
|
||||
const { method, difficulty, sectionId, splitPanArms } = details;
|
||||
const runs = runsOrOther as number;
|
||||
const variable = variables[variableName];
|
||||
if (details) {
|
||||
const { method, difficulty, sectionId, splitPanArms } = details;
|
||||
const runs = runsOrOther as number;
|
||||
const variable = variables[variableName];
|
||||
|
||||
const items = new Map<Item, number>();
|
||||
const items = new Map<Item, number>();
|
||||
|
||||
for (const [itemName, expectedAmount] of Object.entries(variable)) {
|
||||
for (const item of wantedItems) {
|
||||
if (itemName === item.name) {
|
||||
items.set(item, runs * expectedAmount);
|
||||
break;
|
||||
}
|
||||
for (const [itemName, expectedAmount] of Object.entries(variable)) {
|
||||
for (const item of wantedItems) {
|
||||
if (itemName === item.name) {
|
||||
items.set(item, runs * expectedAmount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all section IDs that provide the same items with the same expected amount.
|
||||
// E.g. if you need a spread needle and a bringer's right arm, using either
|
||||
// purplenum or yellowboze will give you the exact same probabilities.
|
||||
const sectionIds: Array<SectionId> = [];
|
||||
// Find all section IDs that provide the same items with the same expected amount.
|
||||
// E.g. if you need a spread needle and a bringer's right arm, using either
|
||||
// purplenum or yellowboze will give you the exact same probabilities.
|
||||
const sectionIds: Array<SectionId> = [];
|
||||
|
||||
for (const sid of SectionIds) {
|
||||
let matchFound = true;
|
||||
for (const sid of SectionIds) {
|
||||
let matchFound = true;
|
||||
|
||||
if (sid !== sectionId) {
|
||||
const v = variables[
|
||||
this.fullMethodName(difficulty, sid, method, splitPanArms)
|
||||
];
|
||||
if (sid !== sectionId) {
|
||||
const v = variables[
|
||||
this.fullMethodName(difficulty, sid, method, splitPanArms)
|
||||
];
|
||||
|
||||
if (!v) {
|
||||
matchFound = false;
|
||||
} else {
|
||||
for (const itemName of Object.keys(variable)) {
|
||||
if (variable[itemName] !== v[itemName]) {
|
||||
matchFound = false;
|
||||
break;
|
||||
}
|
||||
if (!v) {
|
||||
matchFound = false;
|
||||
} else {
|
||||
for (const itemName of Object.keys(variable)) {
|
||||
if (variable[itemName] !== v[itemName]) {
|
||||
matchFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchFound) {
|
||||
sectionIds.push(sid);
|
||||
}
|
||||
}
|
||||
|
||||
this.results.push(new OptimizationResult(
|
||||
difficulty,
|
||||
sectionIds,
|
||||
method.name + (splitPanArms ? ' (Split Pan Arms)' : ''),
|
||||
method.time,
|
||||
runs,
|
||||
items
|
||||
));
|
||||
if (matchFound) {
|
||||
sectionIds.push(sid);
|
||||
}
|
||||
}
|
||||
|
||||
optimalMethods.push(new OptimalMethod(
|
||||
difficulty,
|
||||
sectionIds,
|
||||
method.name + (splitPanArms ? ' (Split Pan Arms)' : ''),
|
||||
method.time,
|
||||
runs,
|
||||
items
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.result = new OptimalResult(
|
||||
[...wantedItems],
|
||||
optimalMethods
|
||||
);
|
||||
}
|
||||
|
||||
private fullMethodName(
|
||||
|
@ -2,26 +2,26 @@ import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { AutoSizer, Index } from "react-virtualized";
|
||||
import { Item } from "../../domain";
|
||||
import { huntOptimizerStore, OptimizationResult } from "../../stores/HuntOptimizerStore";
|
||||
import { huntOptimizerStore, OptimalMethod } from "../../stores/HuntOptimizerStore";
|
||||
import { Column, DataTable } from "../dataTable";
|
||||
import "./OptimizationResultComponent.less";
|
||||
import { hoursToString } from "../time";
|
||||
import "./OptimizationResultComponent.less";
|
||||
|
||||
@observer
|
||||
export class OptimizationResultComponent extends React.Component {
|
||||
@computed private get columns(): Column<OptimizationResult>[] {
|
||||
@computed private get columns(): Column<OptimalMethod>[] {
|
||||
// Standard columns.
|
||||
const results = huntOptimizerStore.results;
|
||||
const result = huntOptimizerStore.result;
|
||||
const optimalMethods = result ? result.optimalMethods : [];
|
||||
let totalRuns = 0;
|
||||
let totalTime = 0;
|
||||
|
||||
for (const result of results) {
|
||||
totalRuns += result.runs;
|
||||
totalTime += result.totalTime;
|
||||
for (const method of optimalMethods) {
|
||||
totalRuns += method.runs;
|
||||
totalTime += method.totalTime;
|
||||
}
|
||||
|
||||
const columns: Column<OptimizationResult>[] = [
|
||||
const columns: Column<OptimalMethod>[] = [
|
||||
{
|
||||
name: 'Difficulty',
|
||||
width: 75,
|
||||
@ -67,47 +67,43 @@ export class OptimizationResultComponent extends React.Component {
|
||||
];
|
||||
|
||||
// Add one column per item.
|
||||
const items = new Set<Item>();
|
||||
if (result) {
|
||||
for (const item of result.wantedItems) {
|
||||
let totalCount = 0;
|
||||
|
||||
for (const r of results) {
|
||||
for (const i of r.itemCounts.keys()) {
|
||||
items.add(i);
|
||||
for (const method of optimalMethods) {
|
||||
totalCount += method.itemCounts.get(item) || 0;
|
||||
}
|
||||
|
||||
columns.push({
|
||||
name: item.name,
|
||||
width: 80,
|
||||
cellRenderer: (result) => {
|
||||
const count = result.itemCounts.get(item);
|
||||
return count ? count.toFixed(2) : '';
|
||||
},
|
||||
tooltip: (result) => {
|
||||
const count = result.itemCounts.get(item);
|
||||
return count ? count.toString() : '';
|
||||
},
|
||||
className: 'number',
|
||||
footerValue: totalCount.toFixed(2),
|
||||
footerTooltip: totalCount.toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const totalCount = results.reduce(
|
||||
(acc, r) => acc + (r.itemCounts.get(item) || 0),
|
||||
0
|
||||
);
|
||||
|
||||
columns.push({
|
||||
name: item.name,
|
||||
width: 80,
|
||||
cellRenderer: (result) => {
|
||||
const count = result.itemCounts.get(item);
|
||||
return count ? count.toFixed(2) : '';
|
||||
},
|
||||
tooltip: (result) => {
|
||||
const count = result.itemCounts.get(item);
|
||||
return count ? count.toString() : '';
|
||||
},
|
||||
className: 'number',
|
||||
footerValue: totalCount.toFixed(2),
|
||||
footerTooltip: totalCount.toString()
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
// Make sure render is called when result changes.
|
||||
@computed private get updateTrigger() {
|
||||
return huntOptimizerStore.results.slice(0, 0);
|
||||
return huntOptimizerStore.result != null;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.updateTrigger; // eslint-disable-line
|
||||
const result = huntOptimizerStore.result;
|
||||
|
||||
return (
|
||||
<section className="ho-OptimizationResultComponent">
|
||||
@ -118,11 +114,11 @@ export class OptimizationResultComponent extends React.Component {
|
||||
<DataTable
|
||||
width={width}
|
||||
height={height}
|
||||
rowCount={huntOptimizerStore.results.length}
|
||||
rowCount={result ? result.optimalMethods.length : 0}
|
||||
columns={this.columns}
|
||||
fixedColumnCount={3}
|
||||
record={this.record}
|
||||
footer={huntOptimizerStore.results.length > 0}
|
||||
footer={result != null}
|
||||
updateTrigger={this.updateTrigger}
|
||||
/>
|
||||
}
|
||||
@ -132,7 +128,7 @@ export class OptimizationResultComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
private record = ({ index }: Index): OptimizationResult => {
|
||||
return huntOptimizerStore.results[index];
|
||||
private record = ({ index }: Index): OptimalMethod => {
|
||||
return huntOptimizerStore.result!.optimalMethods[index];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user