/
summary.go
263 lines (234 loc) · 6.78 KB
/
summary.go
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
package randomizer
import (
"fmt"
"os"
"sort"
"strings"
"time"
)
// returns a channel that will write strings to a text file with CRLF line
// endings. the function will send on the int channel when finished printing.
func getSummaryChannel(filename string) (chan string, chan int) {
c, done := make(chan string), make(chan int)
go func() {
logFile, err := os.Create(filename)
if err != nil {
panic(err)
}
defer logFile.Close()
for line := range c {
fmt.Fprintf(logFile, "%s\r\n", line)
}
done <- 1
}()
// header
c <- fmt.Sprintf("oracles randomizer %s", version)
c <- fmt.Sprintf("generated %s", time.Now().Format(time.RFC3339))
return c, done
}
// separates a map of checks into progression checks and junk checks.
func filterJunk(g graph, checks map[*node]*node, treasures map[string]*treasure,
keysAreProgression bool, resetFunc func()) (prog, junk map[*node]*node) {
prog, junk = make(map[*node]*node), make(map[*node]*node)
// get all required items. if multiple instances of the same class exist
// and any is skippable but some are required, the first instances are
// considered required and the rest are considered unrequired.
spheres, _ := getSpheres(g, checks, keysAreProgression, resetFunc)
for _, class := range getAllItemClasses(checks) {
// skip known inert items
if class != "rupees" && itemIsInert(treasures, class) {
continue
}
// start by removing all instances
removed := make(map[*node]*node)
for slot, item := range checks {
if item.name == class ||
(class == "rupees" && strings.HasPrefix(item.name, "rupees")) {
removed[slot] = item
item.removeParent(slot)
}
}
// add instances back one at a time in sphere order
resetFunc()
g.reset()
g["start"].explore()
for !g["done"].reached {
outerLoop:
for _, sphere := range spheres {
for _, node := range sphere {
if item := removed[node]; item != nil {
delete(removed, node)
prog[node] = item
item.addParent(node)
break outerLoop
}
}
}
resetFunc()
g.reset()
g["start"].explore()
}
// add all other instances back
for slot, item := range removed {
item.addParent(slot)
}
}
// remove denominations of rupees that were added but are actually too
// small to matter.
junkRupees := make(map[*node]*node)
for slot, item := range checks {
if strings.HasPrefix(item.name, "rupees") && prog[slot] == nil {
item.removeParent(slot)
junkRupees[slot] = item
}
}
trivialRupees := make([]*node, 0, 10)
for slot, item := range prog {
if strings.HasPrefix(item.name, "rupees") {
item.removeParent(slot)
resetFunc()
g.reset()
g["start"].explore()
if g["done"].reached {
trivialRupees = append(trivialRupees, slot)
}
item.addParent(slot)
}
}
for _, slot := range trivialRupees {
delete(prog, slot)
}
for slot, item := range junkRupees {
item.addParent(slot)
}
// the remainder is junk.
for slot, item := range checks {
if prog[slot] == nil {
junk[slot] = item
}
}
return
}
// return an ordered slice of names of different item classes. all rupees are
// considered a single class.
func getAllItemClasses(checks map[*node]*node) []string {
allClasses := make(map[string]bool)
for _, item := range checks {
if strings.HasPrefix(item.name, "rupees") {
allClasses["rupees"] = true
} else {
allClasses[item.name] = true
}
}
return orderedKeys(allClasses)
}
// returns a sorted textual representation of the slots in each sphere (except
// for the slot `except`), for easier comparison.
func spheresToText(spheres [][]*node, checks map[*node]*node, except *node) string {
b := new(strings.Builder)
for _, sphere := range spheres {
sort.Slice(sphere, func(i, j int) bool {
return sphere[i].name < sphere[j].name
})
for _, n := range sphere {
if checks[n] != nil && n != except {
b.WriteString(n.name + "\n")
}
}
}
return b.String()
}
// write a "spoiler log" to a file.
func writeSummary(path string, checksum []byte, ropts randomizerOptions,
rom *romState, ri *routeInfo, checks map[*node]*node, spheres [][]*node,
extra []*node, g graph, resetFunc func(), treasures map[string]*treasure,
owlHints map[string]string) {
summary, summaryDone := getSummaryChannel(path)
// header
summary <- fmt.Sprintf("seed: %08x", ri.seed)
summary <- fmt.Sprintf("sha-1 sum: %x", checksum)
summary <- fmt.Sprintf("difficulty: %s",
ternary(ropts.hard, "hard", "normal"))
// items
progChecks := make(map[*node]*node)
for slot, item := range checks {
if ropts.keysanity || !keyRegexp.MatchString(item.name) {
progChecks[slot] = item
}
}
prog, junk := filterJunk(g, progChecks, treasures, ropts.keysanity, resetFunc)
sendSectionHeader(summary, "progression items")
logSpheres(summary, prog, spheres, extra, rom.game, nil)
if !ropts.keysanity {
sendSectionHeader(summary, "small keys and boss keys")
logSpheres(summary, checks, spheres, extra, rom.game, keyRegexp.MatchString)
}
sendSectionHeader(summary, "other items")
logSpheres(summary, junk, spheres, extra, rom.game, nil)
// warps
if ropts.dungeons {
sendSectionHeader(summary, "dungeon entrances")
sendSorted(summary, func(c chan string) {
for entrance, dungeon := range ri.entrances {
c <- fmt.Sprintf("%s entrance <- %s",
"D"+entrance[1:], "D"+dungeon[1:])
}
close(c)
})
}
if ropts.portals {
sendSectionHeader(summary, "subrosia portals")
sendSorted(summary, func(c chan string) {
for in, out := range ri.portals {
c <- fmt.Sprintf("%-20s <- %s",
getNiceName(in, rom.game), getNiceName(out, rom.game))
}
close(c)
})
}
// default seasons (oos only)
if rom.game == gameSeasons {
sendSectionHeader(summary, "default seasons")
sendSorted(summary, func(c chan string) {
for area, id := range ri.seasons {
c <- fmt.Sprintf("%-15s <- %s", area, seasonsById[id])
}
close(c)
})
}
// owl hints
if owlHints != nil {
sendSectionHeader(summary, "hints")
sendSorted(summary, func(c chan string) {
for owlName, hint := range owlHints {
oneLineHint := strings.ReplaceAll(hint, "\n", " ")
oneLineHint = strings.ReplaceAll(oneLineHint, " ", " ")
c <- fmt.Sprintf("%-20s <- \"%s\"", owlName, oneLineHint)
}
close(c)
})
}
close(summary)
<-summaryDone
}
// get the output of a function that sends strings to a given channel, sort
// those strings, and send them to the `out` channel.
func sendSorted(out chan string, generate func(chan string)) {
in := make(chan string)
lines := make([]string, 0, 20) // should be enough capacity for most cases
go generate(in)
for s := range in {
lines = append(lines, s)
}
sort.Strings(lines)
for _, line := range lines {
out <- line
}
}
// sends a section delimiter to the channel.
func sendSectionHeader(c chan string, name string) {
c <- ""
c <- ""
c <- fmt.Sprintf("-- %s --", name)
c <- ""
}