forked from Stewmath/oracles-randomizer-ng
/
hints.go
221 lines (188 loc) · 5.49 KB
/
hints.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
package randomizer
import (
"fmt"
"math/rand"
"sort"
"strings"
"gopkg.in/yaml.v2"
)
// returns a map of owl names to text indexes for the given game.
func getOwlIds(game int) map[string]byte {
owls := make(map[string]map[string]byte)
if err := yaml.Unmarshal(
FSMustByte(false, "/romdata/owls.yaml"), owls); err != nil {
panic(err)
}
return owls[gameNames[game]]
}
// updates the owl statue text data based on the given hints. does not mutate
// anything.
func (rom *romState) setOwlData(owlHints map[string]string) {
table := rom.codeMutables["owlTextOffsets"]
text := rom.codeMutables["owlText"]
builder := new(strings.Builder)
addr := text.addr.offset
owlTextIds := getOwlIds(rom.game)
for _, owlName := range orderedKeys(owlTextIds) {
hint := owlHints[owlName]
textId := owlTextIds[owlName]
str := "\x0c\x00" + strings.ReplaceAll(hint, "\n", "\x01") + "\x00"
table.new[textId*2] = byte(addr)
table.new[textId*2+1] = byte(addr >> 8)
addr += uint16(len(str))
builder.WriteString(str)
}
text.new = []byte(builder.String())
rom.codeMutables["owlTextOffsets"] = table
rom.codeMutables["owlText"] = text
}
type hinter struct {
areas map[string]string
items map[string]string
}
// returns a new hinter initialized for the given game.
func newHinter(game int) *hinter {
h := &hinter{
areas: make(map[string]string),
items: make(map[string]string),
}
// load item names
itemFiles := []string{
"/hints/common_items.yaml",
fmt.Sprintf("/hints/%s_items.yaml", gameNames[game]),
}
for _, filename := range itemFiles {
if err := yaml.Unmarshal(
FSMustByte(false, filename), h.items); err != nil {
panic(err)
}
}
// load area names
rawAreas := make(map[string][]string)
areasFilename := fmt.Sprintf("/hints/%s_areas.yaml", gameNames[game])
if err := yaml.Unmarshal(
FSMustByte(false, areasFilename), rawAreas); err != nil {
panic(err)
}
// transform the areas map from: {final: [internal 1, internal 2]}
// to: {internal 1: final, internal 2: final}
for k, a := range rawAreas {
for _, v := range a {
h.areas[v] = k
}
}
return h
}
// returns a randomly generated map of owl names to owl messages.
func (h *hinter) generate(src *rand.Rand, g graph, checks map[*node]*node,
owlNames []string) map[string]string {
// function body starts here lol
hints := make(map[string]string)
slots := getShuffledHintSlots(src, checks)
i := 0
// keep track of which slots have been hinted at in order to avoid
// duplicates. in practice the implementation of the hint loop makes this
// very unlikely in the first place.
hintedSlots := make(map[*node]bool)
for _, owlName := range owlNames {
// sometimes owls are just unreachable, so anything goes, i guess
g.reset()
g["start"].explore()
owlUnreachable := !g[owlName].reached
// if we're in plando mode, there could be no slots.
if len(slots) == 0 || len(hintedSlots) >= len(slots) {
hints[owlName] = h.format("...")
continue
}
for {
slot, item := slots[i], checks[slots[i]]
i = (i + 1) % len(slots)
if hintedSlots[slot] {
continue
}
// don't give hints about checks that are required to reach the owl
// in the first place, as dictated by the logic of the seed.
item.removeParent(slot)
g.reset()
g["start"].explore()
required := !g[owlName].reached
item.addParent(slot)
if !required || owlUnreachable {
hints[owlName] = h.format(fmt.Sprintf("%s holds %s.",
h.areas[slot.name], h.items[item.name]))
hintedSlots[slot] = true
break
}
}
}
return hints
}
// formats a string for a text box. text box. this doesn't include control
// characters, except for newlines.
func (h *hinter) format(s string) string {
// split message into words to be wrapped
words := strings.Split(s, " ")
// build message line by line
msg := new(strings.Builder)
line := ""
for _, word := range words {
if len(line) == 0 {
line += word
} else if len(line)+len(word) <= 15 {
line += " " + word
} else {
msg.WriteString(line + "\n")
line = word
}
}
msg.WriteString(line)
return msg.String()
}
// implement sort.Interface for []*node
type nodeSlice []*node
func (ns nodeSlice) Len() int {
return len(ns)
}
func (ns nodeSlice) Less(i, j int) bool {
return ns[i].name < ns[j].name
}
func (ns nodeSlice) Swap(i, j int) {
ns[i], ns[j] = ns[j], ns[i]
}
// getShuffledHintSlots returns a randomly ordered slice of slot nodes.
func getShuffledHintSlots(src *rand.Rand, checks map[*node]*node) []*node {
// make slice of check names
slots, i := make([]*node, len(checks)), 0
for slot, item := range checks {
// don't include dungeon items, since dungeon item hints would be
// useless ("Level 7 holds a Boss Key")
if getDungeonName(item.name) != "" {
continue
}
// and don't include these checks, since they're dummy slots that
// aren't actually randomized, or seed trees that the player is
// guaranteed to know about if they're using seeds.
switch slot.name {
case "shop, 20 rupees", "shop, 30 rupees",
"horon village tree", "south lynna tree":
continue
}
slots[i], i = slot, i+1
}
slots = slots[:i] // trim to the number of actually hintable checks
// sort the slots before shuffling to get "deterministic" results
sort.Sort(nodeSlice(slots))
src.Shuffle(len(slots), func(i, j int) {
slots[i], slots[j] = slots[j], slots[i]
})
return slots
}
// returns truee iff all the characters in s are in the printable range.
func isValidGameText(s string) bool {
for _, c := range s {
if c < ' ' || c > 'z' {
return false
}
}
return true
}