-
Notifications
You must be signed in to change notification settings - Fork 0
/
scanner.go
324 lines (276 loc) · 7.36 KB
/
scanner.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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package roll
import (
"bufio"
"bytes"
"io"
)
var eof = rune(0)
// Return true if ch is a whitespace character
func isWhitespace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n'
}
// Return true if ch is a number
func isNumber(ch rune) bool {
return ch >= '0' && ch <= '9'
}
// Return true if ch is a weird die character
func isDieChar(ch rune) bool {
return ch == 'F' || ch == 'f'
}
// Return true if ch is a comparison character
func isCompare(ch rune) bool {
return ch == '<' || ch == '>' || ch == '='
}
// Return true if ch is a modifier character
func isModifier(ch rune) bool {
return ch == '+' || ch == '-'
}
// Return true if ch is an exploding character
func isExploding(ch rune) bool {
return ch == '!'
}
// Return true if ch is a reroll character
func isReroll(ch rune) bool {
return ch == 'r'
}
// Return true if ch is a grouping character
func isGrouping(ch rune) bool {
return ch == '{' || ch == ',' || ch == '}'
}
// Return true if ch is a keep limit character
func isKeepLimit(ch rune) bool {
return ch == 'k'
}
// Return true if ch is a valid character for indicating a die roll
func isValidDieRoll(ch rune) bool {
return !isWhitespace(ch) && !isGrouping(ch) && !isReroll(ch) && !isExploding(ch) && !isCompare(ch) && !isModifier(ch) && !isKeepLimit(ch) && ch != 'd' && ch != 'D'
}
// Scanner is our lexical scanner for dice roll strings
type Scanner struct {
r *bufio.Reader
}
// NewScanner returns a new instance of scanner
func NewScanner(r io.Reader) *Scanner {
return &Scanner{r: bufio.NewReader(r)}
}
// Scan returns the next token and literal value
func (s *Scanner) Scan() (tok Token, lit string) {
ch := s.read()
switch {
case isWhitespace(ch):
s.unread()
return s.scanWhitespace()
case isNumber(ch):
s.unread()
return s.scanNumber()
case ch == 'd':
s.unread()
return s.scanDieOrDrop()
case ch == 'f':
return tFAILURES, string(ch)
case ch == '!':
s.unread()
return s.scanExplosions()
case ch == 'k':
s.unread()
return s.scanKeep()
case ch == 'r':
s.unread()
return s.scanReroll()
case ch == 's':
s.unread()
return s.scanSort()
case ch == '-':
return tMINUS, string(ch)
case ch == '+':
return tPLUS, string(ch)
case ch == '>':
return tGREATER, string(ch)
case ch == '<':
return tLESS, string(ch)
case ch == '=':
return tEQUAL, string(ch)
case ch == '{':
return tGROUPSTART, string(ch)
case ch == '}':
return tGROUPEND, string(ch)
case ch == ',':
return tGROUPSEP, string(ch)
case ch == eof:
return tEOF, ""
}
return tILLEGAL, string(ch)
}
// scanWhitespace consumes the current rune and all contiguous whitespace.
func (s *Scanner) scanWhitespace() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent whitespace character into the buffer.
// Non-whitespace characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if !isWhitespace(ch) {
s.unread()
break
} else {
buf.WriteRune(ch)
}
}
return tWS, buf.String()
}
// scanNumber consumes the current rune and all contiguous number runes.
func (s *Scanner) scanNumber() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent number character into the buffer.
// Non-number characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if !isNumber(ch) {
s.unread()
break
} else {
_, _ = buf.WriteRune(ch)
}
}
// Otherwise return as a regular identifier.
return tNUM, buf.String()
}
// scanDieOrDrop consumes the current rune and all contiguous die/drop runes.
func (s *Scanner) scanDieOrDrop() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent character into the buffer.
// We assume a die token by default and switch based on subsequent chars.
tok = tDIE
for {
ch := s.read()
if ch == eof {
break
} else if tok == tDIE && ch == 'l' {
tok = tDROPLOW
} else if tok == tDIE && ch == 'h' {
tok = tDROPHIGH
} else if tok == tDIE && !isNumber(ch) && !isDieChar(ch) {
if isValidDieRoll(ch) {
_, _ = buf.WriteRune(ch)
}
s.unread()
break
} else if tok != tDIE && !isNumber(ch) {
if isValidDieRoll(ch) {
_, _ = buf.WriteRune(ch)
}
s.unread()
break
}
_, _ = buf.WriteRune(ch)
}
// Otherwise return as a regular identifier.
return tok, buf.String()
}
// scanKeep consumes the current rune and all contiguous keep runes.
func (s *Scanner) scanKeep() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent character into the buffer.
// We assume an illegal token by default and switch based on later chars.
tok = tILLEGAL
for {
ch := s.read()
if ch == eof {
break
} else if tok == tILLEGAL && ch == 'l' {
tok = tKEEPLOW
} else if tok == tILLEGAL && ch == 'h' {
tok = tKEEPHIGH
} else if tok != tILLEGAL && !isNumber(ch) {
s.unread()
break
}
_, _ = buf.WriteRune(ch)
}
// Otherwise return as a regular identifier.
return tok, buf.String()
}
// scanExplosions consumes the current rune and all contiguous explode runes.
func (s *Scanner) scanExplosions() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent character into the buffer.
// We assume an explode token by default and switch based on later chars.
tok = tEXPLODE
ch := s.read()
if ch == eof {
return tok, buf.String()
}
if ch == '!' {
tok = tCOMPOUND
_, _ = buf.WriteRune(ch)
} else if ch == 'p' {
tok = tPENETRATE
_, _ = buf.WriteRune(ch)
} else {
s.unread()
}
// Otherwise return as a regular identifier.
return tok, buf.String()
}
// scanReroll consumes the current rune and all contiguous reroll runes.
func (s *Scanner) scanReroll() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent character into the buffer.
// Rerolls are simple flags with an optional modifier
tok = tREROLL
ch := s.read()
if ch == eof {
return tok, buf.String()
}
if ch == 'o' {
_, _ = buf.WriteRune(ch)
} else {
s.unread()
}
// Otherwise return as a regular identifier.
return tok, buf.String()
}
// scanSort consumes the current rune and all contiguous sort runes.
func (s *Scanner) scanSort() (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent character into the buffer.
// Sorts are simple flags with an optional modifier
tok = tSORT
ch := s.read()
if ch == eof {
return tok, buf.String()
}
if ch == 'd' {
_, _ = buf.WriteRune(ch)
} else {
s.unread()
}
// Otherwise return as a regular identifier.
return tok, buf.String()
}
// read reads the next rune from the buffered reader.
// Returns the rune(0) if an error occurs (or io.EOF is returned).
func (s *Scanner) read() rune {
ch, _, err := s.r.ReadRune()
if err != nil {
return eof
}
return ch
}
// unread places the previously read rune back on the reader.
func (s *Scanner) unread() { _ = s.r.UnreadRune() }