/
Roller.pm6
377 lines (295 loc) · 9.17 KB
/
Roller.pm6
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
use Dice::Roller::Rollable;
use Dice::Roller::Selector;
unit class Dice::Roller does Dice::Roller::Rollable;
our $debug = False; # Accessible as $Dice::Roller::debug;
# Grammar defining a dice string:-
# ------------------------------
grammar DiceGrammar {
token TOP { ^ <expression> [ ';' \s* <expression> ]* ';'? $ }
proto rule expression {*}
proto token add_op {*}
proto token selector {*}
rule expression:sym<add> { <add_op>? <term> [ <add_op> <term> ]* }
token term { <roll> | <modifier> }
token add_op:sym<+> { <sym> }
token add_op:sym<-> { <sym> }
regex roll { <quantity> <die> <selector>* }
token quantity { \d+ }
token die { d(\d+) }
token selector:sym<kh> { ':' <sym>(\d+) } # keep highest n
token selector:sym<kl> { ':' <sym>(\d+) } # keep lowest n
token selector:sym<dh> { ':' <sym>(\d+) } # drop highest n
token selector:sym<dl> { ':' <sym>(\d+) } # drop lowest n
regex modifier { (\d+) }
}
# Other classes we use internally to represent the parsed dice string:-
# -------------------------------------------------------------------
# A single polyhedron.
class Die does Dice::Roller::Rollable {
has Int $.faces; # All around me different faces I see
has @.distribution; # We will use this when rolling; this allows for non-linear dice to be added later.
has $.value is rw; # Which face is showing, if any?
submethod BUILD(:$!faces) {
# Initialise the distribution of values with a range of numbers from 1 to the number of faces the die has.
@!distribution = 1..$!faces;
}
method contents {
return [];
}
method roll {
$!value = @.distribution.pick;
return self;
}
method set-max {
$!value = @.distribution.max;
return self;
}
method set-min {
$!value = @.distribution.min;
return self;
}
method is-max returns Bool {
return $!value == @.distribution.max;
}
method is-min returns Bool {
return $!value == @.distribution.min;
}
method total returns Int {
return $!value // 0;
}
method Num {
return $!value;
}
method Str {
return "[$!value]" if $!value;
return "(d$!faces)";
}
}
# Some fixed value adjusting a roll's total outcome.
class Modifier does Dice::Roller::Rollable {
has Int $.value is required;
method contents {
return [];
}
method is-max {
return True;
}
method is-min {
return True;
}
method total returns Int {
return $!value;
}
method Str {
return $!value.Str;
}
}
# A thing that selects or adjusts certain dice from a Roll.
# In this case, we want to keep the highest num rolls.
class KeepHighest does Dice::Roller::Selector {
has Int $.num = 1;
method select ($roll) {
my $keep = $.num min $roll.dice.elems;
my $drop = $roll.dice.elems - $keep;
say "Selecting highest $keep rolls (dropping $drop) from '$roll'" if $debug;
my @removed = $roll.sort.dice.splice(0, $drop); # Replace 0..^drop with empty
say "Discarding: " ~ @removed if $debug;
}
}
class KeepLowest does Dice::Roller::Selector {
has Int $.num = 1;
method select ($roll) {
my $keep = $.num min $roll.dice.elems;
my $drop = $roll.dice.elems - $keep;
say "Selecting lowest $keep rolls (dropping $drop) from '$roll'" if $debug;
my @removed = $roll.sort.dice.splice($keep); # Replace keep..* with empty
say "Discarding: " ~ @removed if $debug;
}
}
class DropHighest does Dice::Roller::Selector {
has Int $.num = 1;
method select ($roll) {
my $drop = $.num min $roll.dice.elems;
my $keep = $roll.dice.elems - $drop;
say "Dropping highest $drop rolls (keeping $keep) from '$roll'" if $debug;
my @removed = $roll.sort.dice.splice($keep); # Replace keep..* with empty
say "Discarding: " ~ @removed if $debug;
}
}
class DropLowest does Dice::Roller::Selector {
has Int $.num = 1;
method select ($roll) {
my $drop = $.num min $roll.dice.elems;
my $keep = $roll.dice.elems - $drop;
say "Dropping lowest $drop rolls (keeping $keep) from '$roll'" if $debug;
my @removed = $roll.sort.dice.splice(0, $drop); # Replace 0..^drop with empty
say "Discarding: " ~ @removed if $debug;
}
}
# A roll of one or more polyhedra, with some rule about how we combine them.
class Roll does Dice::Roller::Rollable {
has Int $.quantity;
has Die @.dice is rw;
has Dice::Roller::Selector @.selectors;
method contents {
return @.dice;
}
method roll {
@!dice».roll;
for @!selectors -> $selector {
$selector.select(self);
}
return self;
}
# One thing that most Rollables don't do that's useful for Roll to be able to do,
# sort the $.dice in ascending order - primarily so Selectors can do their work.
# This sorts in-place.
method sort {
@!dice = @!dice.sort: { $^a.value cmp $^b.value };
return self;
}
method Str {
if any(@!dice».value) {
# one or more dice have been rolled, we don't need to prefix our quantity, they'll have literal values.
return join('', @!dice);
} else {
# no dice have been rolled, we return a more abstract representation.
return $!quantity ~ @!dice[0];
}
}
}
class Expression does Dice::Roller::Rollable {
has Pair @.operations;
method contents {
return @!operations».value;
}
method add(Str $op, Dice::Roller::Rollable $value) {
@!operations.push( $op => $value );
}
# Expression needs to reimplement Total since we can now subtract parts of the roll.
method total returns Int {
my $total = 0;
for @!operations -> $op-pair {
given $op-pair.key {
when '+' { $total += $op-pair.value.total }
when '-' { $total -= $op-pair.value.total }
default { die "unhandled Expression type " ~ $op-pair.key }
}
}
return $total;
}
method Str {
my Str $str = "";
for @!operations -> $op-pair {
$str ~= $op-pair.key if $str;
$str ~= $op-pair.value;
}
return $str;
}
}
# Because returning an Array of Expressions doesn't seem to be working well for us,
# let's stick the various (individual) rolls into one of these.
class RollSet does Dice::Roller::Rollable {
has Dice::Roller::Rollable @.rolls;
method contents {
return @!rolls;
}
method group-totals returns List {
return @!rolls».total;
}
method Str {
return join('; ', @!rolls);
}
}
# Actions used to build our internal representation from the grammar:-
# ------------------------------------------------------------------
class DiceActions {
method TOP($/) {
# .parse returns a RollSet with an array of Expression objects,
# one entry for each of the roll expressions separated by ';' in the string.
make RollSet.new( rolls => $<expression>».made );
}
method expression:sym<add>($/) {
my $expression = Expression.new;
my Str $op = '+';
for $/.caps -> Pair $term_or_op {
given $term_or_op.key {
when "term" {
my $term = $term_or_op.value;
$expression.add($op, $term.made);
}
when "add_op" {
$op = $term_or_op.value.made;
}
}
}
make $expression;
}
method add_op:sym<+>($/) {
make $/.Str;
}
method add_op:sym<->($/) {
make $/.Str;
}
method term($/) {
make $<roll>.made // $<modifier>.made;
}
method roll($/) {
# While there is only one 'die' token within the 'roll' grammar, we actually want
# to construct the Roll object with multiple Die objects as appropriate, so that
# we can roll and remember the face value of individual die.
my Int $quantity = $<quantity>.made;
my Die @dice = (1..$quantity).map({ $<die>.made.clone });
my Dice::Roller::Selector @selectors = $<selector>».made;
make Roll.new( :$quantity, :@dice, :@selectors );
}
method quantity($/) {
make $/.Int;
}
method die($/) {
make Die.new( faces => $0.Int );
}
method modifier($/) {
make Modifier.new( value => "$0".Int );
}
method selector:sym<kh>($/) {
make KeepHighest.new( num => $0.Int );
}
method selector:sym<kl>($/) {
make KeepLowest.new( num => $0.Int );
}
method selector:sym<dh>($/) {
make DropHighest.new( num => $0.Int );
}
method selector:sym<dl>($/) {
make DropLowest.new( num => $0.Int );
}
}
# Attributes of a Dice::Roller:-
# ----------------------------
# Attributes are all private by default, and defined with the '!' twigil. But using '.' instead instructs
# Perl 6 to define the $!string attribute and automagically generate a .string *accessor* that can be
# used publically. Note that this accessor will be read-only by default.
has Str $.string is required;
has Match $.match is required;
has RollSet $.rollset is required;
# We define a custom .new method to allow for positional (non-named) parameters:-
method new(Str $string) {
my $match = DiceGrammar.parse($string, :actions(DiceActions));
die "Failed to parse '$string'!" unless $match;
#say "Parsed: ", $match.gist if $debug;
return self.bless(string => $string, match => $match, rollset => $match.made);
}
# Note that in general, doing extra constructor work should happen in the BUILD submethod; doing our own
# special new method here may complicate things in subclasses. But we do want a nice simple constructor,
# and defining our own 'new' seems to be the best way to accomplish this.
# http://doc.perl6.org/language/objects#Object_Construction
method contents {
return $!rollset;
}
method group-totals returns List {
return $!rollset.group-totals;
}
method Str {
return $!rollset.Str;
}