Skip to content

Commit f1b2513

Browse files
committed
refactor ast into multiple files. cleanup rerolls. allow explode of dF, d66
1 parent bbd0c76 commit f1b2513

File tree

10 files changed

+611
-360
lines changed

10 files changed

+611
-360
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
## ⚠️⚠️ Breaking changes ⚠️⚠️
44

5-
Roll results have changed significantly (again). It's amazing what having users will do to a package!
5+
Roll results have changed significantly. Now, the individual die results are modeled
6+
as a RolledDie object, rather than an integer. The RolledDie object incorporates
7+
a lot of the metadata/scoring in pre-8.0.0 results.
8+
9+
Having a more elaborate result will make it easier for client applications
10+
to render the outcome of a roll. Each RolledDie knows its result, the # of sides on the die,
11+
whether it was counted as a success/failure, and
612

713
## 📈 Enhancements
814

915
- upgrade to dart 3.8.0
1016
- upgrade to petitparser 7.0.0
17+
- added dependency on fast_immutable_collections
1118
- remove metadata & score from RollResult
1219
- rather than simple list of Integers representing the output of a roll or operation, now each
1320
die rolled is a 'RolledDie' object which has the metadata/score.
14-
- the RollResult's results field changes from List<int> to List<RolledDie>. RolledDie represents not just the
21+
- the RollResult's results field changes from List<int> to IList<RolledDie>. RolledDie represents not just the
1522
outcome of the roll, but also includes metadata about the type of die, nsides of the die (if polyhedral die),
1623
and metadata about the dice expression's modification of the rolls.
1724
- RollResult.results will include both discarded an not-discarded RolledDie.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@ void main() {
117117
* `4d6 #<=2` -- roll 4d6, count any <= 2
118118
* `4d6 #=5` -- roll 4d6, count any equal to 5
119119
* successes and failures
120-
* A normal count operation `#` discards the rolled dice and changes the result to be the count
120+
* A simple count operation `#` discards the rolled dice and changes the result to be the count of results matching the condition
121121
* For example, `2d6#<=3` rolls `[3,4]` then counts which results are `<=3` , returning `[1]`
122122
* But, sometimes you want to be able to count successes/failures without discarding the dice rolls.
123123
In this case, use modifiers `#s`, `#f`, `#cs`, `#cf` to add metadata to the results.
124+
* without modifiers, `#s` or `#cs` will count the result as a success if it matches the maximum possible value for the die
125+
* without modifiers, `#f` or `#cf` will count the result as a failure if it matches the minimum possible value for the die
124126
* `6d6 #f<=2 #s>=5 #cs6 #cf1` -- roll 6d6, count results <= 2 as failures, >= 5 as successes, =6 as critical
125127
successes, =1 as critical failures
126128
* The above returns a result like:

lib/src/ast_core.dart

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
2+
3+
import 'dice_expression.dart';
4+
import 'dice_roller.dart';
5+
import 'results.dart';
6+
import 'utils.dart';
7+
8+
/// All our operations will inherit from this class.
9+
/// The `call()` method will be called by the parent node.
10+
/// The `eval()` method is called from the node
11+
abstract class DiceOp extends DiceExpression with LoggingMixin {
12+
// each child class should override this to implement their operation
13+
RollResult eval();
14+
15+
// all children can share this call operator -- and it'll let us be consistent w/ regard to logging
16+
@override
17+
RollResult call() {
18+
final result = eval();
19+
logger.finer(() => '$result');
20+
return result;
21+
}
22+
}
23+
24+
/// base class for unary operations
25+
abstract class Unary extends DiceOp {
26+
Unary(this.name, this.left);
27+
28+
final String name;
29+
final DiceExpression left;
30+
31+
@override
32+
String toString() => '($left)$name';
33+
}
34+
35+
/// base class for binary operations
36+
abstract class Binary extends DiceOp {
37+
Binary(this.name, this.left, this.right);
38+
39+
final String name;
40+
final DiceExpression left;
41+
final DiceExpression right;
42+
43+
@override
44+
String toString() => '($left $name $right)';
45+
}
46+
47+
/// multiply operation (flattens results)
48+
class MultiplyOp extends Binary {
49+
MultiplyOp(super.name, super.left, super.right);
50+
51+
@override
52+
RollResult eval() => left() * right();
53+
}
54+
55+
/// add operation
56+
class AddOp extends Binary {
57+
AddOp(super.name, super.left, super.right);
58+
59+
@override
60+
RollResult eval() => left() + right();
61+
}
62+
63+
/// subtraction operation
64+
class SubOp extends Binary {
65+
SubOp(super.name, super.left, super.right);
66+
67+
@override
68+
RollResult eval() => left() - right();
69+
}
70+
71+
/// base class for unary dice operations
72+
abstract class UnaryDice extends Unary {
73+
UnaryDice(super.name, super.left, this.roller);
74+
75+
final DiceRoller roller;
76+
77+
@override
78+
String toString() => '($left$name)';
79+
}
80+
81+
/// base class for binary dice expressions
82+
abstract class BinaryDice extends Binary {
83+
BinaryDice(super.name, super.left, super.right, this.roller);
84+
85+
final DiceRoller roller;
86+
}
87+
88+
/// A value expression. The token we read from input will be a String,
89+
/// it must parse as an int, and an empty string will return empty set.
90+
class SimpleValue extends DiceExpression {
91+
SimpleValue(this.value)
92+
: _results = RollResult(
93+
expression: value,
94+
opType: OpType.value,
95+
results: value.isEmpty
96+
? []
97+
: [RolledDie.singleVal(result: int.parse(value))],
98+
);
99+
100+
final String value;
101+
final RollResult _results;
102+
103+
@override
104+
RollResult call() => _results;
105+
106+
@override
107+
String toString() => value;
108+
}

lib/src/ast_dice.dart

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
2+
import 'package:petitparser/parser.dart';
3+
4+
import 'ast_core.dart';
5+
import 'dice_roller.dart';
6+
import 'results.dart';
7+
8+
/// roll fudge dice
9+
class FudgeDice extends UnaryDice {
10+
FudgeDice(super.name, super.left, super.roller);
11+
12+
@override
13+
RollResult eval() {
14+
final lhs = left();
15+
final ndice = lhs.totalOrDefault(() => 1);
16+
17+
// redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here.
18+
if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) {
19+
throw FormatException(
20+
'Invalid number of dice ($ndice)',
21+
toString(),
22+
left.toString().length,
23+
);
24+
}
25+
final roll = roller.rollFudge(ndice);
26+
return RollResult.fromRollResult(
27+
roll,
28+
expression: toString(),
29+
opType: roll.opType,
30+
left: lhs,
31+
);
32+
}
33+
}
34+
35+
class CSVDice extends UnaryDice {
36+
CSVDice(super.op, super.left, super.roller, this.vals);
37+
38+
final SeparatedList<String, String> vals;
39+
40+
@override
41+
String toString() => '(${left}d${vals.elements})';
42+
43+
@override
44+
RollResult eval() {
45+
final lhs = left();
46+
final ndice = lhs.totalOrDefault(() => 1);
47+
48+
final roll = roller.rollVals(ndice, IList(vals.elements.map(int.parse)));
49+
50+
return RollResult.fromRollResult(
51+
roll,
52+
expression: toString(),
53+
opType: OpType.rollVals,
54+
left: lhs,
55+
);
56+
}
57+
}
58+
59+
/// roll n % dice
60+
class PercentDice extends UnaryDice {
61+
PercentDice(super.name, super.left, super.roller);
62+
63+
@override
64+
RollResult eval() {
65+
final lhs = left();
66+
final ndice = lhs.totalOrDefault(() => 1);
67+
final roll = roller.roll(ndice, 100);
68+
return RollResult.fromRollResult(
69+
roll,
70+
expression: toString(),
71+
opType: OpType.rollPercent,
72+
left: lhs,
73+
);
74+
}
75+
}
76+
77+
/// roll n D66
78+
class D66Dice extends UnaryDice {
79+
D66Dice(super.name, super.left, super.roller);
80+
81+
@override
82+
RollResult eval() {
83+
final lhs = left();
84+
final ndice = lhs.totalOrDefault(() => 1);
85+
final roll = roller.rollD66(ndice);
86+
return RollResult.fromRollResult(
87+
roll,
88+
expression: toString(),
89+
opType: OpType.rollD66,
90+
left: lhs,
91+
);
92+
}
93+
}
94+
95+
/// roll N dice of Y sides.
96+
class StdDice extends BinaryDice {
97+
StdDice(super.name, super.left, super.right, super.roller);
98+
99+
@override
100+
String toString() => '($left$name$right)';
101+
102+
@override
103+
RollResult eval() {
104+
final lhs = left();
105+
final rhs = right();
106+
final ndice = lhs.totalOrDefault(() => 1);
107+
final nsides = rhs.totalOrDefault(() => 1);
108+
109+
// redundant w/ RangeError checks in the DiceRoller. But we can construct better error messages here.
110+
if (ndice < DiceRoller.minDice || ndice > DiceRoller.maxDice) {
111+
throw FormatException(
112+
'Invalid number of dice ($ndice)',
113+
toString(),
114+
left.toString().length,
115+
);
116+
}
117+
if (nsides < DiceRoller.minSides || nsides > DiceRoller.maxSides) {
118+
throw FormatException(
119+
'Invalid number of sides ($nsides)',
120+
toString(),
121+
left.toString().length + name.length + 1,
122+
);
123+
}
124+
final roll = roller.roll(ndice, nsides);
125+
return RollResult.fromRollResult(
126+
roll,
127+
expression: toString(),
128+
opType: roll.opType,
129+
left: lhs,
130+
right: rhs,
131+
);
132+
}
133+
}

0 commit comments

Comments
 (0)