View Javadoc
1   /*
2    * Copyright (c) 2021 Kaiserpfalz EDV-Service, Roland T. Lichti.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
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   * ParseDiceString -- Parses a string to the DiceRollCommand.
40   *
41   * @author klenkes74 {@literal <rlichti@kaiserpfalz-edv.de>}
42   * @since 2020-01-03
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       * Parses the string of a single die roll.
82       *
83       * @param dieString The die string to parse.
84       * @return The roll preparsed for the services.
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 }