-
Notifications
You must be signed in to change notification settings - Fork 0
/
solver.ts
150 lines (130 loc) · 5.32 KB
/
solver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { Data, Item, Stage, Language} from './penguin-stats'
import _, { Dictionary } from 'lodash'
import solver from 'javascript-lp-solver'
export class OptimizeResult {
constructor (result: any, itemMap: Dictionary<Item>, stageMap: Dictionary<Stage>) {
Object.entries(result).forEach(([key, value]) => {
if (key in ['feasible', 'bounded', 'isIntegral', 'depot', 'result']) return
if (key in stageMap) this.farms.set(stageMap[key], Math.ceil(value as number))
if (key in itemMap) this.synthesizes.set(itemMap[key], Math.ceil(value as number))
})
this.sanity = _.sumBy([...this.farms.entries()], ([stage, count]) => stage.cost * count)
}
sanity: number
farms: Map<Stage, number> = new Map()
synthesizes: Map<Item, number> = new Map()
toString (language: Language): string {
const obj = { sanityCost: this.sanity }
this.farms.forEach((value, key) => {
obj[key.codes[language]] = value
})
this.synthesizes.forEach((value, key) => {
obj[key.names[language]] = value
})
return JSON.stringify(obj, null, 2)
}
}
export class OptimizeOption {
// If true, the optimizer will try to find the exact best integer solution for the problem.
//
// For example, A stage drops 3 item with 20 AP cost, B stage drops 1 item with 10 AP cost; we want to get 10 items,
// and the option is set to true. The result will be 3 times A stage and 1 times B stage;
// the total sanity cost is 3 * 20 + 1 * 10 = 70. However, this is not the long-term optimal solution,
// because the 3 drops stage is always better than the 1 drops stage.
exactInt: boolean = false
}
export function solve (
needs: Map<string, number>,
haves: Map<string, number>,
data: Data,
option: OptimizeOption = new OptimizeOption()
): OptimizeResult {
// to solve the problem, we have to find the minimum sanity cost of the following variables and constraints:
//
// variables:
// each variable name is a stage id or synthesis outcome id;
// consumed items are represented by a negative number;
// produced items are represented by a positive number;
// the sanity cost is represented by a positive number;
// synthesis steps have extremely low sanity cost, which prevents synthesizing unused items.
//
// special variable: 'depot_var', costs exactly one special item 'depot_con', and produce all the items in the depot.
// it represents the items in the depot.
//
// constraints:
// the sanity cost must be non-negative;
// all the items must be non-negative; we can't loan in the terra world :D
// all the needed items must be produced (minimum = item needed count).
//
// special constraint: 'depot_con' must be exactly one, which represents the items in the depot (see above).
const variables = {}
const ints = {}
// special depot; see above
const depot = { depot_count: 1 }
// add a function to simulate the items in depot, which we had already
haves.forEach((haveItemCount, haveItemID) => {depot[haveItemID] = haveItemCount})
variables['depot'] = depot
// the linear functions for stage drops;
// costs the corresponded sanity and produces the estimated drop items
for (const [stageID, drops] of Object.entries(data.drops)) {
const variable = { 'cost': data.stages[stageID].cost }
drops.forEach(drop => {variable[drop.item.id] = drop.dropCount / drop.sampleCount})
if (option.exactInt) {
// we can only farm a stage integer times
// see exactInt comment for more information
ints[stageID] = 1
}
variables[stageID] = variable
}
// add the linear functions for synthesis outcomes;
// costs the corresponded materials and produces the outcome item
for (const [outcomeItemID, synthesize] of Object.entries(data.synthesizes)) {
const variable = { 'cost': synthesize.cost }
// negative for consumed material items
synthesize.materials.forEach(material => {variable[material.item.id] = -material.count})
// positive for produced outcome items
variable[outcomeItemID] = +1
// we can only synthesize an item integer times
ints[outcomeItemID] = 1
variables[outcomeItemID] = variable
}
const constraints = {
// special depot_count constraint; see above
'depot_count': { 'equal': 1 },
// sanity cost must be non-negative
'cost': { 'min': 0 }
}
// all the items must be non-negative
for (const [itemID,] of Object.entries(data.items)) {
constraints[itemID] = { 'min': 0 }
}
// all the needed items must be produced (minimum = item needed count)
needs.forEach((needItemCount, needItemID) => {
constraints[needItemID] = { 'min': needItemCount }
})
const model = {
optimize: 'cost',
opType: 'min',
constraints: constraints,
variables: variables,
ints: ints
}
const result = solver.Solve(model)
return new OptimizeResult(result, data.items, data.stages)
}
export function importFrom (json: string) {
const raw = JSON.parse(json)
const needs = {}
const haves = {}
switch (raw['@type']) {
case '@penguin-statistics/planner/config':
raw.items.forEach(item => {
if (item.need) needs[item.id] = item.need
if (item.have) haves[item.id] = item.have
})
break
default:
throw new Error(`invalid config: expected @type to be @penguin-statistics/planner/config, got '${raw['@type']}'`)
}
return { needs, haves }
}