1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package de.kaiserpfalzedv.rpg.core.dice;
19
20 import de.kaiserpfalzedv.rpg.core.dice.bag.GenericNumericDie;
21 import de.kaiserpfalzedv.rpg.core.dice.mat.ExpressionTotal;
22 import de.kaiserpfalzedv.rpg.core.dice.mat.RollTotal;
23 import lombok.RequiredArgsConstructor;
24 import lombok.ToString;
25 import lombok.extern.slf4j.Slf4j;
26 import net.objecthunter.exp4j.ExpressionBuilder;
27
28 import jakarta.annotation.PostConstruct;
29 import jakarta.enterprise.context.Dependent;
30 import jakarta.inject.Inject;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.List;
34 import java.util.Optional;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38
39
40
41
42
43
44 @Dependent
45 @RequiredArgsConstructor(onConstructor_ = {@Inject})
46 @ToString
47 @Slf4j
48 public class DiceParser {
49 static private final String DICE_PATTERN =
50 "(?<pre>(([A-Za-z]+)?[(])?)?"
51 + "(?<amount>\\d+)?"
52 + "(?<type>([dD])?[A-Za-z][0-9A-Za-z]+)"
53 + "(?<post>.*)?";
54
55 static private final Pattern PATTERN = Pattern.compile(DICE_PATTERN);
56
57 private final Collection<Die> dice;
58
59 @PostConstruct
60 public void startUp() {
61 log.debug("Loaded dice: {}", dice);
62 }
63
64 public RollTotal parse(final String diceString) {
65 String[] dieString = diceString.split("\\s+");
66
67 log.debug("working on di(c)e roll: {}", (Object[]) dieString);
68
69 RollTotal.RollTotalBuilder result = RollTotal.builder();
70
71
72 List<ExpressionTotal> expressions = new ArrayList<>();
73 for (String d : dieString) {
74 parseSingleDie(d).ifPresent(expressions::add);
75 }
76
77 return result.expressions(expressions).build();
78 }
79
80
81
82
83
84
85
86 public Optional<ExpressionTotal> parseSingleDie(final String dieString) {
87 Matcher m = PATTERN.matcher(dieString);
88
89 if (m.matches()) {
90 String pre = m.group("pre");
91 if (pre == null || pre.isBlank()) {
92 pre = "";
93 }
94
95 String amountString = m.group("amount");
96 int amount = 1;
97 if (amountString != null && !amountString.isBlank()) {
98 amount = Integer.parseInt(amountString);
99 }
100
101 String dieIdentifier = m.group("type");
102 if (dieIdentifier == null) {
103 dieIdentifier = "D6";
104 }
105
106 if (dieIdentifier.startsWith("w") || dieIdentifier.startsWith("W")) {
107 dieIdentifier = "D" + dieIdentifier.substring(1);
108 }
109 dieIdentifier = dieIdentifier.toUpperCase();
110
111 String post = m.group("post");
112 if (post != null && post.isBlank()) {
113 post = "";
114 }
115
116 StringBuilder expressionString = new StringBuilder();
117 if (! pre.isBlank()) {
118 expressionString.append(pre).append("x").append(post);
119 } else {
120 expressionString.append("x").append(post);
121 }
122
123 String expression = expressionString.toString();
124 log.trace("Die roll expression: input='{}', amount={}, expression='{}'", dieString, amount, expression);
125
126 Die die;
127 try {
128 die = selectDieType(dieIdentifier);
129 } catch (NumberFormatException e) {
130 log.warn("Can't find a valid die for this expression!");
131
132 return Optional.empty();
133 }
134
135 try {
136 new ExpressionBuilder(expression).variable("x").build();
137 } catch (IllegalArgumentException e) {
138 log.warn("Expression '" + dieString + "' is not valid: " + e.getMessage());
139
140 return Optional.empty();
141 }
142
143 ExpressionTotal result = ExpressionTotal.builder()
144 .rolls(die.roll(amount))
145 .expression(expression)
146 .build();
147
148 log.debug("Parsed die: {}", result);
149 return Optional.of(result);
150 }
151
152 return Optional.empty();
153 }
154
155
156 private Die selectDieType(final String qualifier) {
157 for (Die die : dice) {
158 log.trace("Checking die type: qualifier={}, die={}", qualifier, die.getDieType());
159 if (die.getDieType().equalsIgnoreCase(qualifier))
160 return die;
161 }
162
163 return new GenericNumericDie(Integer.parseInt(qualifier.substring(1)));
164 }
165 }