Skip to content

Commit 2fa23f3

Browse files
committed
add support for commas and sorting
1 parent 88751ed commit 2fa23f3

File tree

10 files changed

+290
-41
lines changed

10 files changed

+290
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ whether it was counted as a success/failure.
1414

1515
- upgrade to dart 3.8.0
1616
- upgrade to petitparser 7.0.0
17+
- add 'sort' to dice syntax -- `4d6 s` (ascending) or `4d6 sd` (descending)
18+
- allow commas to separate different dice expressions
19+
- `4d4,6d6,8d8` --
1720
- implement penetrating dice ala Hackmaster
1821
- `1d6p` -- roll d6, if 6 is rolled explode with d6s subtracting one each time.
1922
- `1d100p20` -- roll a d100, if 100 is rolled penetrate with d20s

README.md

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ void main() {
107107
* cap/clamp:
108108
* `4d20 C<5` -- roll 4d20, change any value < 5 to 5
109109
* `4d20 C>15` -- roll 4d20, change any value > 15 to 15
110+
* sorting results
111+
* `4d20 s` -- results sorted in ascending order
112+
* `4d20 sd` -- results sorted in descending order
110113

111114
* scoring dice rolls:
112115
* counting:
@@ -234,16 +237,100 @@ Sometimes it's nice to change the output type so you can see the graph of result
234237

235238
```console
236239
# show the result graph:
237-
dart run example/main.dart -o pretty '3d6 #cs #cf'
238-
(((3d6) #cs ) #cf ) ===> RollSummary(total: 9, results: [1(d6)❌, 3(d6), 5(d6)], critFailureCount: 1)
239-
(((3d6) #cs ) #cf ) =count=> RollResult(total: 9, results: [1(d6)❌, 3(d6), 5(d6)])
240-
((3d6) #cs ) =count=> RollResult(total: 9, results: [1(d6), 3(d6), 5(d6)])
241-
(3d6) =rollDice=> RollResult(total: 9, results: [1(d6), 3(d6), 5(d6)])
242-
243-
dart run example/main.dart -o json '3d6 #cs #cf'
244-
{"expression":"(((3d6) #cs ) #cf )","total":9,"results":[2,3,4],"detailedResults":{"expression":"(((3d6) #cs ) #cf )","opType":"count","nsides":6,"ndice":3,"results":[2,3,4],"left":{"expression":"((3d6) #cs )","opType":"count","nsides":6,"ndice":3,"results":[2,3,4],"left":{"expression":"(3d6)","opType":"rollDice","nsides":6,"ndice":3,"results":[2,3,4],"metadata":{"rolled":[2,3,4]}}}},"metadata":{"rolled":[2,3,4]}}
245-
240+
dart run example/main.dart -o pretty '3d6 #cs #cf'
241+
(((3d6) #cs ) #cf ) ===> RollSummary(total: 13, results: [6(d6✅), 5(d6), 2(d6)], critSuccessCount: 1)
242+
(((3d6) #cs ) #cf ) =count=> RollResult(total: 13, results: [6(d6✅), 5(d6), 2(d6)])
243+
((3d6) #cs ) =count=> RollResult(total: 13, results: [6(d6✅), 5(d6), 2(d6)])
244+
(3d6) =rollDice=> RollResult(total: 13, results: [6(d6), 5(d6), 2(d6)])
246245

246+
dart run example/main.dart -o json '3d6 #cs #cf' | jq
247+
{
248+
"expression": "(((3d6) #cs ) #cf )",
249+
"total": 12,
250+
"results": [
251+
{
252+
"result": 4,
253+
"nsides": 6,
254+
"dieType": "polyhedral"
255+
},
256+
{
257+
"result": 5,
258+
"nsides": 6,
259+
"dieType": "polyhedral"
260+
},
261+
{
262+
"result": 3,
263+
"nsides": 6,
264+
"dieType": "polyhedral"
265+
}
266+
],
267+
"detailedResults": {
268+
"expression": "(((3d6) #cs ) #cf )",
269+
"opType": "count",
270+
"results": [
271+
{
272+
"result": 4,
273+
"nsides": 6,
274+
"dieType": "polyhedral"
275+
},
276+
{
277+
"result": 5,
278+
"nsides": 6,
279+
"dieType": "polyhedral"
280+
},
281+
{
282+
"result": 3,
283+
"nsides": 6,
284+
"dieType": "polyhedral"
285+
}
286+
],
287+
"left": {
288+
"expression": "((3d6) #cs )",
289+
"opType": "count",
290+
"results": [
291+
{
292+
"result": 4,
293+
"nsides": 6,
294+
"dieType": "polyhedral"
295+
},
296+
{
297+
"result": 5,
298+
"nsides": 6,
299+
"dieType": "polyhedral"
300+
},
301+
{
302+
"result": 3,
303+
"nsides": 6,
304+
"dieType": "polyhedral"
305+
}
306+
],
307+
"left": {
308+
"expression": "(3d6)",
309+
"opType": "rollDice",
310+
"results": [
311+
{
312+
"result": 4,
313+
"nsides": 6,
314+
"dieType": "polyhedral"
315+
},
316+
{
317+
"result": 5,
318+
"nsides": 6,
319+
"dieType": "polyhedral"
320+
},
321+
{
322+
"result": 3,
323+
"nsides": 6,
324+
"dieType": "polyhedral"
325+
}
326+
],
327+
"total": 12
328+
},
329+
"total": 12
330+
},
331+
"total": 12
332+
}
333+
}
247334
```
248335

249336
## Statistics output

lib/src/ast_core.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dice_expression.dart';
22
import 'dice_roller.dart';
33
import 'enums.dart';
4+
import 'extensions.dart';
45
import 'roll_result.dart';
56
import 'rolled_die.dart';
67
import 'utils.dart';
@@ -44,6 +45,48 @@ abstract class Binary extends DiceOp {
4445
String toString() => '($left $name $right)';
4546
}
4647

48+
class CommaOp extends Binary {
49+
CommaOp(super.name, super.left, super.right);
50+
51+
@override
52+
RollResult eval() {
53+
final lhs = left();
54+
final rhs = right();
55+
56+
final results = <RolledDie>[];
57+
final discarded = <RolledDie>[];
58+
59+
discarded.addAll(lhs.discarded);
60+
discarded.addAll(rhs.discarded);
61+
62+
if (lhs.opType == OpType.comma) {
63+
results.addAll(lhs.results);
64+
} else {
65+
results.add(
66+
RolledDie.singleVal(result: lhs.results.sum, from: lhs.results),
67+
);
68+
discarded.addAll(lhs.results.map(RolledDie.discard));
69+
}
70+
if (rhs.opType == OpType.comma) {
71+
results.addAll(rhs.results);
72+
} else {
73+
results.add(
74+
RolledDie.singleVal(result: rhs.results.sum, from: rhs.results),
75+
);
76+
discarded.addAll(rhs.results.map(RolledDie.discard));
77+
}
78+
79+
return RollResult(
80+
expression: toString(),
81+
opType: OpType.comma,
82+
results: results,
83+
discarded: discarded,
84+
left: lhs,
85+
right: rhs,
86+
);
87+
}
88+
}
89+
4790
/// multiply operation (flattens results)
4891
class MultiplyOp extends Binary {
4992
MultiplyOp(super.name, super.left, super.right);

lib/src/ast_dice.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class PenetratingDice extends UnaryDice {
101101
.roll(
102102
1,
103103
nsidesPenetration,
104-
'(penetration ind $i, $numPenetrated)',
104+
'(penetration ind $i, #$numPenetrated)',
105105
)
106106
.results
107107
.first;

lib/src/ast_ops.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ import 'rolled_die.dart';
88
/// default limit for rerolls/exploding/compounding to avoid getting stuck in loop
99
const defaultRerollLimit = 1000;
1010

11+
class SortOp extends Unary {
12+
SortOp(super.name, super.left);
13+
14+
@override
15+
RollResult eval() {
16+
final lhs = left();
17+
final bool reversed = name == 'sd';
18+
19+
return RollResult(
20+
results: reversed ? lhs.results.sortReversed() : lhs.results.sort(),
21+
discarded: reversed ? lhs.discarded.sortReversed() : lhs.discarded.sort(),
22+
opType: OpType.sort,
23+
24+
expression: toString(),
25+
left: lhs,
26+
);
27+
}
28+
}
29+
1130
/// variation on count -- count how many results from lhs are =,<,> rhs.
1231
class CountOp extends Binary {
1332
CountOp(
@@ -38,8 +57,10 @@ class CountOp extends Binary {
3857

3958
bool shouldCount(RolledDie rolledDie) {
4059
var rhsEmptyAndSimpleCount = false;
60+
var calculatedDefault = false;
4161
final target = rhs.totalOrDefault(() {
42-
// if missing RHS, we can make assumptions depending on operator.
62+
calculatedDefault = true;
63+
// if missing RHS, we can make assumptions depending on operator and the dietype
4364
switch (name) {
4465
case '#':
4566
// example: '3d6#' should be 3. target is ignored in case statement below.
@@ -49,7 +70,7 @@ class CountOp extends Binary {
4970
// example: '3d6#s' should match 6, or '3D66' should match 66
5071
return rolledDie.maxPotentialValue;
5172
case '#f' || '#cf':
52-
// generally should be 1.
73+
// generally should be 1 or whatever the minimum potential val is
5374
return rolledDie.minPotentialValue;
5475
default:
5576
throw FormatException(
@@ -82,6 +103,12 @@ class CountOp extends Binary {
82103
// that is, '3d6#' should return 3
83104
return true;
84105
} else {
106+
// don't allow a singleVal/nvals(with 1 element) be counted as a success just because it's the min or max.
107+
if (calculatedDefault &&
108+
rolledDie.dieType.requirePotentialValues &&
109+
rolledDie.potentialValues.length == 1) {
110+
return false;
111+
}
85112
// if not missing rhs, treat it as equivalent to '#='.
86113
// that is, '3d6#2' should count 2s
87114
return v == target;

lib/src/dice_roller.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class DiceRoller with LoggingMixin {
3838
return rollFudge(1, msg);
3939
case DieType.d66:
4040
return rollD66(1, msg);
41-
case DieType.special:
41+
case DieType.nvals:
4242
return rollVals(1, rolledDie.potentialValues, msg);
4343
default:
4444
return RollResult(
@@ -128,7 +128,7 @@ class DiceRoller with LoggingMixin {
128128
(i) => RolledDie(
129129
result: i,
130130
nsides: sideVals.length,
131-
dieType: DieType.special,
131+
dieType: DieType.nvals,
132132
potentialValues: sideVals,
133133
),
134134
),

lib/src/enums.dart

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,30 @@ enum DieType implements Comparable<DieType> {
33
// normal polyhedral (1d6, 1d20, etc)
44
polyhedral(),
55
// fudge dice
6-
fudge(hasPotentialValues: true),
6+
fudge(requirePotentialValues: true),
77
// 1D66 (equivalent to `1d6*10 + 1d6`).
8-
d66(hasNSides: false),
8+
d66(requireNSides: false),
99
// 1d[1,3,5,7,9]
10-
special(hasPotentialValues: true),
10+
nvals(requirePotentialValues: true),
1111
// single value (e.g. a sum or count of dice)
12-
singleVal(explodable: false, hasPotentialValues: true);
12+
singleVal(explodable: false, requirePotentialValues: true)
13+
//penetration(explodable: false)
14+
;
1315

1416
const DieType({
1517
this.explodable = true,
16-
this.hasPotentialValues = false,
17-
this.hasNSides = true,
18+
this.requirePotentialValues = false,
19+
this.requireNSides = true,
1820
});
1921

2022
/// can the die be exploded?
2123
final bool explodable;
2224

2325
/// whether the RolledDie must have non-empty potentialValues
24-
final bool hasPotentialValues;
26+
final bool requirePotentialValues;
2527

2628
/// whether the RolledDie must have non-zero nsides
27-
final bool hasNSides;
29+
final bool requireNSides;
2830

2931
@override
3032
int compareTo(DieType dieType) => index.compareTo(dieType.index);
@@ -47,6 +49,8 @@ enum OpType {
4749
reroll,
4850
compound,
4951
explode,
52+
sort,
53+
comma,
5054
}
5155

5256
enum CountType { count, success, failure, critSuccess, critFailure }

lib/src/parser.dart

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import 'ast_ops.dart';
66
import 'dice_expression.dart';
77
import 'dice_roller.dart';
88

9-
// TODO: support commas `(<expr>,<expr>,<expr>)kh` -- evaluate each subexpression into a new die result, discarding the ones that had been combined
10-
119
Parser<DiceExpression> parserBuilder(DiceRoller roller) {
1210
final builder = ExpressionBuilder<DiceExpression>();
1311
// numbers
@@ -128,15 +126,23 @@ Parser<DiceExpression> parserBuilder(DiceRoller roller) {
128126
..left(char('-').trim(), (a, op, b) => SubOp(op, a, b));
129127
// count >=, <=, <, >, =,
130128
// #s, #cs, #f, #cf -- count (critical) successes / failures
131-
builder.group().left(
132-
(char('#') &
133-
char('c').optional() &
134-
pattern('sf').optional() &
135-
pattern('<>').optional() &
136-
char('=').optional())
137-
.flatten()
138-
.trim(),
139-
(a, op, b) => CountOp(op.toLowerCase(), a, b),
140-
);
129+
builder.group()
130+
..left(
131+
(char('#') &
132+
char('c').optional() &
133+
pattern('sf').optional() &
134+
pattern('<>').optional() &
135+
char('=').optional())
136+
.flatten()
137+
.trim(),
138+
(a, op, b) => CountOp(op.toLowerCase(), a, b),
139+
)
140+
..postfix(
141+
(char('s', ignoreCase: true) & char('d', ignoreCase: true).optional())
142+
.flatten()
143+
.trim(),
144+
(a, op) => SortOp(op.toLowerCase(), a),
145+
)
146+
..left(char(',').trim(), (a, op, b) => CommaOp(op, a, b));
141147
return builder.build().end();
142148
}

lib/src/rolled_die.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
3030
this.clampFloor = false,
3131
this.from = const IList.empty(),
3232
}) : potentialValues = IList(potentialValues) {
33-
if (dieType.hasPotentialValues && potentialValues.isEmpty) {
33+
if (dieType.requirePotentialValues && potentialValues.isEmpty) {
3434
throw ArgumentError(
3535
'Invalid die -- ${dieType.name} must have a potentialValues field',
3636
);
3737
}
38-
if (dieType.hasNSides && nsides == 0) {
38+
if (dieType.requireNSides && nsides == 0) {
3939
throw ArgumentError(
4040
'Invalid die -- ${dieType.name} must have a nsides != 0',
4141
);
@@ -49,7 +49,7 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
4949
minPotentialValue = 1;
5050
case DieType.singleVal:
5151
maxPotentialValue = minPotentialValue = result;
52-
case DieType.special || DieType.fudge:
52+
case DieType.nvals || DieType.fudge:
5353
maxPotentialValue = potentialValues.max;
5454
minPotentialValue = potentialValues.min;
5555
}
@@ -210,6 +210,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
210210

211211
bool get isMaxResult => result == maxPotentialValue;
212212

213+
bool get isCountable => minPotentialValue != maxPotentialValue;
214+
213215
@override
214216
List<Object?> get props => [
215217
result,

0 commit comments

Comments
 (0)