Skip to content

Commit 88751ed

Browse files
committed
implement penetrating dice
1 parent d7933fe commit 88751ed

File tree

9 files changed

+384
-29
lines changed

9 files changed

+384
-29
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+
- implement penetrating dice ala Hackmaster
18+
- `1d6p` -- roll d6, if 6 is rolled explode with d6s subtracting one each time.
19+
- `1d100p20` -- roll a d100, if 100 is rolled penetrate with d20s
1720
- added dependency on fast_immutable_collections
1821
- remove metadata & score from RollResult
1922
- allow compounding, exploding, and rerolls for 'odd' die (`dF, D66, and d[vals]`)

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ void main() {
5454
* `1D66` -- roll `1` D66, aka `1d6*10 + 1d6`
5555
* **_NOTE_**: you _must_ use uppercase `D66`, lowercase `d66` will be interpreted as a 66-sided die
5656
* `2d[2,3,5,7]`-- roll 2 dice with values `[2,3,5,7]`
57+
* `4d6p` -- penetrating dice. Similar to exploding, and -1 is added for each subsequent reroll.
58+
* `1d20p6`, `1d100p20` -- HackMaster rules say a d20 penetrates with d6s, and a d100 penetrates with d20s
5759

5860
* exploding dice
5961
* `4d6!` -- roll `4` `6`-sided dice, explode if max (`6`) is rolled (re-roll and include in results)
@@ -422,7 +424,7 @@ stdout.writeln('${rollResult.opType.name} -> $rollResult');
422424
423425
// if you want to listen for the RollSummary
424426
DiceExpression.registerSummaryListener((rollSummary) {
425-
stdout.writeln('$rollSummary');
427+
stdout.writeln('$rollSummary');[README.md](README.md)
426428
});
427429
428430
```
@@ -431,11 +433,7 @@ Alternatively, you may not want to know _all_ roll events, and are only interest
431433
the events for your specific roll. In that case, pass an 'onRoll' method to the `roll()` method
432434

433435
```dart
434-
DiceExpression.create
435-
('2d20kh
436-
'
437-
)
438-
.roll(
436+
DiceExpression.create('2d20kh').roll(
439437
onRoll: (rr) => stdout.writeln('roll - $rr'),
440438
onSummary: (summary) => stdout.writeln('summary - $
441439
summary

lib/src/ast_dice.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import 'package:collection/collection.dart';
12
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
23
import 'package:petitparser/parser.dart';
34

45
import 'ast_core.dart';
6+
import 'ast_ops.dart';
57
import 'dice_roller.dart';
68
import 'enums.dart';
79
import 'roll_result.dart';
10+
import 'rolled_die.dart';
811

912
/// roll fudge dice
1013
class FudgeDice extends UnaryDice {
@@ -57,6 +60,87 @@ class CSVDice extends UnaryDice {
5760
}
5861
}
5962

63+
class PenetratingDice extends UnaryDice {
64+
PenetratingDice(
65+
super.op,
66+
super.left,
67+
super.roller, {
68+
required String nsides,
69+
required String nsidesPenetration,
70+
}) : nsides = int.parse(nsides),
71+
nsidesPenetration = nsidesPenetration.isEmpty
72+
? int.parse(nsides)
73+
: int.parse(nsidesPenetration);
74+
75+
final int nsides;
76+
final int nsidesPenetration;
77+
final limit = defaultRerollLimit;
78+
79+
@override
80+
String toString() => '(${left}d${nsides}p$nsidesPenetration)';
81+
82+
@override
83+
RollResult eval() {
84+
final lhs = left();
85+
final ndice = lhs.totalOrDefault(() => 1);
86+
87+
final roll = roller.roll(ndice, nsides);
88+
89+
final results = <RolledDie>[];
90+
final discarded = <RolledDie>[];
91+
roll.results.forEachIndexed((i, rolledDie) {
92+
if (rolledDie.isMaxResult) {
93+
var sum = rolledDie.result;
94+
RolledDie rerolled;
95+
var numPenetrated = 0;
96+
discarded.add(
97+
RolledDie.copyWith(rolledDie, discarded: true, penetrator: true),
98+
);
99+
do {
100+
rerolled = roller
101+
.roll(
102+
1,
103+
nsidesPenetration,
104+
'(penetration ind $i, $numPenetrated)',
105+
)
106+
.results
107+
.first;
108+
discarded.add(
109+
RolledDie.copyWith(rerolled, discarded: true, penetrator: true),
110+
);
111+
sum += rerolled.result;
112+
numPenetrated++;
113+
} while (rerolled.isMaxResult && numPenetrated < limit);
114+
discarded.add(
115+
RolledDie.singleVal(
116+
result: -numPenetrated,
117+
discarded: true,
118+
penetrator: true,
119+
),
120+
);
121+
results.add(
122+
RolledDie.copyWith(
123+
rolledDie,
124+
result: sum - numPenetrated,
125+
penetrated: true,
126+
from: discarded,
127+
),
128+
);
129+
} else {
130+
results.add(rolledDie);
131+
}
132+
});
133+
134+
return RollResult(
135+
expression: toString(),
136+
opType: OpType.rollPenetration,
137+
results: results,
138+
discarded: lhs.discarded + discarded,
139+
left: lhs,
140+
);
141+
}
142+
}
143+
60144
/// roll n % dice
61145
class PercentDice extends UnaryDice {
62146
PercentDice(super.name, super.left, super.roller);

lib/src/ast_ops.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ class CompoundingDice extends BinaryDice {
377377

378378
bool shouldCompound(RolledDie rolledDie) {
379379
final val = rolledDie.result;
380-
if (!rolledDie.dieType.compoundable) {
380+
if (!rolledDie.dieType.explodable) {
381381
logger.finest('$rolledDie cannot compound due to dieType');
382382
return false;
383383
}
@@ -409,6 +409,7 @@ class CompoundingDice extends BinaryDice {
409409
var sum = v.result;
410410
RolledDie rerolled;
411411
var numCompounded = 0;
412+
discarded.add(RolledDie.copyWith(v, discarded: true, compounded: true));
412413
do {
413414
rerolled = roller
414415
.reroll(v, '(compound ind $i, #$numCompounded)')

lib/src/enums.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,17 @@ enum DieType implements Comparable<DieType> {
99
// 1d[1,3,5,7,9]
1010
special(hasPotentialValues: true),
1111
// single value (e.g. a sum or count of dice)
12-
singleVal(explodable: false, compoundable: false, hasPotentialValues: true);
12+
singleVal(explodable: false, hasPotentialValues: true);
1313

1414
const DieType({
1515
this.explodable = true,
16-
this.compoundable = true,
1716
this.hasPotentialValues = false,
1817
this.hasNSides = true,
1918
});
2019

2120
/// can the die be exploded?
2221
final bool explodable;
2322

24-
/// can the die be compounded?
25-
final bool compoundable;
26-
2723
/// whether the RolledDie must have non-empty potentialValues
2824
final bool hasPotentialValues;
2925

@@ -47,6 +43,7 @@ enum OpType {
4743
rollPercent,
4844
rollD66,
4945
rollVals,
46+
rollPenetration,
5047
reroll,
5148
compound,
5249
explode,

lib/src/parser.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,27 @@ Parser<DiceExpression> parserBuilder(DiceRoller roller) {
4040
char(']').trim(),
4141
),
4242
(a, op) => CSVDice(op.toString(), a, roller, op.$3),
43+
)
44+
..postfix(
45+
seq4(
46+
char('d').trim(),
47+
digit().plus().flatten().trim(),
48+
char('p').trim(),
49+
digit().plus().optional().flatten().trim(),
50+
),
51+
(a, op) => PenetratingDice(
52+
op.toString(),
53+
a,
54+
roller,
55+
nsides: op.$2,
56+
nsidesPenetration: op.$4,
57+
),
4358
);
4459
builder.group().left(
4560
char('d').trim(),
4661
(a, op, b) => StdDice(op, a, b, roller),
4762
);
4863

49-
// TODO: !p penetrating dice
5064
// compounding dice (has to be in separate group from exploding)
5165
builder.group().left(
5266
(string('!!') &

lib/src/roll_summary.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ class RollSummary extends Equatable {
8585
return buffer.toString();
8686
}
8787

88-
// TODO: implement fromJson?
8988
Map<String, dynamic> toJson() =>
9089
{
9190
'expression': expression,

lib/src/rolled_die.dart

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
2222
this.explosion = false,
2323
this.compoundedFinal = false,
2424
this.compounded = false,
25+
this.penetrated = false,
26+
this.penetrator = false,
2527
this.reroll = false,
2628
this.rerolled = false,
2729
this.clampCeiling = false,
@@ -65,21 +67,23 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
6567

6668
factory RolledDie.singleVal({
6769
required int result,
68-
Iterable<RolledDie>? from,
70+
bool discarded = false,
71+
bool penetrator = false,
72+
Iterable<RolledDie>? from = const IList.empty(),
6973
}) => RolledDie(
7074
result: result,
7175
nsides: 1,
76+
discarded: discarded,
77+
penetrator: penetrator,
7278
dieType: DieType.singleVal,
7379
potentialValues: [result],
74-
from: IList.orNull(from) ?? const IList.empty(),
80+
from: IList(from),
7581
);
7682

77-
factory RolledDie.d66({required int result, Iterable<RolledDie>? from}) =>
78-
RolledDie(
79-
result: result,
80-
dieType: DieType.d66,
81-
from: IList.orNull(from) ?? const IList.empty(),
82-
);
83+
factory RolledDie.d66({
84+
required int result,
85+
Iterable<RolledDie>? from = const IList.empty(),
86+
}) => RolledDie(result: result, dieType: DieType.d66, from: IList(from));
8387

8488
factory RolledDie.copyWith(
8589
RolledDie other, {
@@ -93,6 +97,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
9397
bool? explosion,
9498
bool? compounded,
9599
bool? compoundedFinal,
100+
bool? penetrator,
101+
bool? penetrated,
96102
bool? reroll,
97103
bool? rerolled,
98104
bool? clampHigh,
@@ -106,6 +112,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
106112
discarded: discarded ?? other.discarded,
107113
success: success ?? other.success,
108114
failure: failure ?? other.failure,
115+
penetrated: penetrated ?? other.penetrated,
116+
penetrator: penetrator ?? other.penetrator,
109117
critSuccess: critSuccess ?? other.critSuccess,
110118
critFailure: critFailure ?? other.critFailure,
111119
exploded: exploded ?? other.exploded,
@@ -182,6 +190,12 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
182190
/// true if the die is the sum a multiple die due to compounding
183191
final bool compoundedFinal;
184192

193+
/// true if the die was a discarded result during penetration
194+
final bool penetrator;
195+
196+
/// true if the die was the result of penetration
197+
final bool penetrated;
198+
185199
/// true if the (discarded) result is from a reroll
186200
final bool rerolled;
187201

@@ -194,6 +208,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
194208
/// true if the result has been clamped via `C<`
195209
final bool clampFloor;
196210

211+
bool get isMaxResult => result == maxPotentialValue;
212+
197213
@override
198214
List<Object?> get props => [
199215
result,
@@ -215,6 +231,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
215231
rerolled,
216232
clampCeiling,
217233
clampFloor,
234+
penetrated,
235+
penetrator,
218236
];
219237

220238
Map<String, dynamic> toJson() =>
@@ -236,6 +254,8 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
236254
'rerolled': rerolled,
237255
'clampHigh': clampCeiling,
238256
'clampLow': clampFloor,
257+
'penetrated': penetrated,
258+
'penetrator': penetrator,
239259
}..removeWhere(
240260
(k, v) =>
241261
v == null ||
@@ -278,11 +298,17 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
278298
if (explosion) {
279299
buffer.write('🔥'); //'⇪');
280300
}
301+
if (penetrated) {
302+
buffer.write('➶');
303+
}
304+
if (penetrator) {
305+
buffer.write('⥅');
306+
}
281307
if (compoundedFinal) {
282308
buffer.write('∑');
283309
}
284310
if (compounded) {
285-
buffer.write('+');
311+
buffer.write('');
286312
}
287313
if (clampCeiling) {
288314
buffer.write('⌈⌉');
@@ -316,14 +342,23 @@ class RolledDie extends Equatable implements Comparable<RolledDie> {
316342
return buffer.toString();
317343
}
318344

319-
//TODO: should this compare other fields too?
320345
@override
321346
int compareTo(RolledDie other) => result
322347
.compareTo(other.result)
323348
.if0(dieType.compareTo(other.dieType))
324-
.if0(
325-
dieType == DieType.polyhedral && other.dieType == DieType.polyhedral
326-
? nsides.compareTo(other.nsides)
327-
: 0,
328-
);
349+
.if0(nsides.compareTo(other.nsides))
350+
.if0(discarded.compareTo(other.discarded))
351+
.if0(success.compareTo(other.success))
352+
.if0(failure.compareTo(other.failure))
353+
.if0(failure.compareTo(other.failure))
354+
.if0(critSuccess.compareTo(other.critSuccess))
355+
.if0(critFailure.compareTo(other.critFailure))
356+
.if0(exploded.compareTo(other.exploded))
357+
.if0(explosion.compareTo(other.explosion))
358+
.if0(compoundedFinal.compareTo(other.compoundedFinal))
359+
.if0(compounded.compareTo(other.compounded))
360+
.if0(reroll.compareTo(other.reroll))
361+
.if0(rerolled.compareTo(other.rerolled))
362+
.if0(clampCeiling.compareTo(other.clampCeiling))
363+
.if0(clampFloor.compareTo(other.clampFloor));
329364
}

0 commit comments

Comments
 (0)