View Javadoc
1   package de.aikiit.game.kaiser;
2   
3   import lombok.Getter;
4   import org.assertj.core.util.VisibleForTesting;
5   
6   import java.math.BigDecimal;
7   import java.math.RoundingMode;
8   import java.security.SecureRandom;
9   import java.util.Random;
10  
11  import static org.apache.commons.lang3.compare.ComparableUtils.is;
12  
13  /**
14   * This class encapsulates the game's logics and operations on its attributes.
15   * As the game is based on rounds {@link #startNewRound()} starts a new round and performs calculations based on the player's actions.
16   * <br />
17   * In each round there is a chance for famine-induced loss,
18   * which is handled in {@link #processFamine()} based on an underlying random probability factor.
19   * <br />
20   * Apart from these automated things the player can:
21   * <ul>
22   *     <li>{@link #buyLand(Long)} buy new land</li>
23   *     <li>{@link #sellLand(Long)} sell existing land</li>
24   *     <li>{@link #cultivate(Long)} cultivate land in order to achieve new yields</li>
25   *     <li>{@link #feedToPopulation(Long)} give food to your population</li>
26   * </ul>
27   * After user interactions the round is finished in {@link #finishRoundAfterActions()}.
28   */
29  @Getter
30  public class KaiserEngine {
31      /**
32       * e - External damage, e.g. loss due to rats.
33       */
34      private BigDecimal externalDamage = BigDecimal.ZERO; // e
35      private BigDecimal deathToll; // d
36      private BigDecimal increase; // i in original-  birthRate?
37      private Integer zYear; // why z in original?
38      private BigDecimal population = BigDecimal.ZERO; // h in original
39      private BigDecimal area = BigDecimal.ZERO;
40      private BigDecimal yield = BigDecimal.ZERO;
41      private BigDecimal supplies = BigDecimal.ZERO;
42      private BigDecimal humans = BigDecimal.ZERO;
43      private BigDecimal deathTollSum; // d1 in original
44      private BigDecimal percentDeathToll; // p1 in original
45      private BigDecimal q = BigDecimal.ONE; // q - disaster/famineQuotient
46      private BigDecimal cost = BigDecimal.ZERO;
47  
48      private static final Random RANDOM = new SecureRandom();
49  
50      /**
51       * Default constructor to start a game with the given default settings.
52       */
53      public KaiserEngine() {
54          this.population = BigDecimal.valueOf(95L);
55          this.zYear = 0;
56          this.yield = BigDecimal.valueOf(3L);
57          this.supplies = BigDecimal.valueOf(2800L);
58          this.humans = BigDecimal.valueOf(3000L);
59          this.area = this.humans.divide(this.yield, RoundingMode.HALF_UP);
60          this.increase = BigDecimal.valueOf(5L);
61          this.deathToll = BigDecimal.ZERO;
62          this.percentDeathToll = BigDecimal.ZERO;
63          this.deathTollSum = BigDecimal.ZERO;
64          this.externalDamage = this.humans.subtract(this.supplies);
65      }
66  
67      /**
68       * Starts a new round in performs initial calculations before user actions are taken into account.
69       */
70      public void startNewRound() {
71          this.area = this.humans.divide(this.yield, 0, RoundingMode.HALF_UP);
72          this.externalDamage = this.humans.subtract(this.supplies);
73          this.zYear++;
74          this.population = this.population.add(this.increase);
75  
76          processFamine();
77          this.cost = getRandomNumberUntil(10);
78          this.yield = cost.add(BigDecimal.valueOf(17L));
79      }
80  
81      /**
82       * Helper method to retrieve a new random number without any comma (scale=0).
83       *
84       * @param threshold number is greater than 0 and at most threshold.
85       * @return a new random number.
86       */
87      BigDecimal getRandomNumberUntil(int threshold) {
88          return BigDecimal.valueOf(RANDOM.nextInt(threshold + 1) + 1).setScale(0, RoundingMode.HALF_EVEN);
89      }
90  
91      /**
92       * Evaluate internally, if a famine is happening in the current round.
93       * If so this method performs all necessary calculations/reductions within the currently running game.
94       */
95      public void processFamine() {
96          if (is(q).lessThan(BigDecimal.ZERO)) {
97              this.population = this.population.divide(BigDecimal.valueOf(2L), 0, RoundingMode.HALF_UP);
98              System.out.println(KaiserEnginePrinter.ORANGE);
99              System.out.println("Eine fürchterliche Seuche hat die halbe Stadt dahingerafft!");
100             System.out.println(KaiserEnginePrinter.ANSI_RESET);
101         }
102         refreshFamineQuotient();
103     }
104 
105     /**
106      * Explicitly trigger the recalculation of the given internal famine calculation factor.
107      */
108     void refreshFamineQuotient() {
109         this.q = getRandomNumberUntil(10).divide(BigDecimal.TEN, 0, RoundingMode.HALF_UP).subtract(new BigDecimal("0.3"));
110 
111     }
112 
113     @VisibleForTesting
114     void setQ(BigDecimal q) {
115         this.q = q;
116     }
117 
118     @VisibleForTesting
119     void setSupplies(BigDecimal supplies) {
120         this.supplies = supplies;
121     }
122 
123 
124     @VisibleForTesting
125     void setArea(BigDecimal area) {
126         this.area = area;
127     }
128 
129     /**
130      * Calculates the available area per person in the current game.
131      *
132      * @return area per capita, called <b>L</b> in original. Land ownership?
133      */
134     public BigDecimal getAreaPerCapita() {
135         return area.divide(population, 0, RoundingMode.HALF_UP);
136     }
137 
138     /**
139      * Performs an act of buying land (new land is acquired by reducing the supplies according to the current land price).
140      * You cannot buy more than you can afford.
141      *
142      * @param buy how many hectares you want to buy. Negative input is ignored.
143      * @return the given number of hectares. {@code 0} means that the player does not want to buy anything, which will trigger the possibility to sell land.
144      */
145     public long buyLand(Long buy) {
146         if (buy < 0) {
147             System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET);
148         }
149 
150         if (buy > 0) {
151             if (is(this.yield.multiply(BigDecimal.valueOf(buy))).lessThanOrEqualTo(this.supplies)) {
152                 this.area = this.area.add(BigDecimal.valueOf(buy));
153                 this.supplies = this.supplies.subtract(this.yield.multiply(BigDecimal.valueOf(buy)));
154                 this.cost = BigDecimal.ZERO; // price is recalculated per round
155             } else {
156                 throw new IllegalArgumentException("Not Enough Supplies");
157             }
158         }
159         return buy;
160     }
161 
162     /**
163      * Performs an act of selling land (resulting in an increase of supplies as the land is sold to the current land price).
164      * You cannot sell more than you have.
165      *
166      * @param sell how many hectares you want to sell. Negative input is ignored.
167      */
168     public void sellLand(Long sell) {
169         if (sell < 0) {
170             System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET);
171             return;
172         }
173 
174         if (is(BigDecimal.valueOf(sell)).lessThan(this.area)) {
175             this.area = this.area.subtract(BigDecimal.valueOf(sell));
176             this.supplies = this.supplies.add(this.yield.multiply(BigDecimal.valueOf(sell)));
177             this.cost = BigDecimal.ZERO; // price is recalculated per round
178         } else {
179             throw new IllegalArgumentException("Not Enough Land");
180         }
181     }
182 
183     /**
184      * Performs an act of using supplies to feed your population.
185      * You cannot give more than you have.
186      *
187      * @param feed how many dzt you want to feed
188      */
189     public void feedToPopulation(Long feed) {
190         if (feed < 0) {
191             System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET);
192             return;
193         }
194 
195         if (feed != 0) {
196             if (is(BigDecimal.valueOf(feed)).lessThanOrEqualTo(this.supplies)) {
197                 this.supplies = this.supplies.subtract(BigDecimal.valueOf(feed));
198                 this.cost = BigDecimal.ONE; // price is recalculated per round
199             } else {
200                 throw new IllegalArgumentException("Not Enough in Stock");
201             }
202         }
203     }
204 
205     /**
206      * Performs an act of using your area and people to cultivate, plant crops for the upcoming season/next round.
207      * You cannot give more than you have.
208      *
209      * @param cultivate how many hectares you want to use for agricultural purposes. Negative input is ignored. An input of {@code 0} will trigger a recalculation of the current land price.
210      */
211     public void cultivate(Long cultivate) {
212         if (cultivate == 0) {
213             calculateNewPrice();
214             return;
215         }
216 
217         if (cultivate < 0) {
218             System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET);
219             return;
220         }
221 
222         if (is(this.area).lessThan(BigDecimal.valueOf(cultivate))) {
223             throw new IllegalArgumentException("You cannot cultivate more area than you have.");
224         }
225 
226         BigDecimal halfCultivate = BigDecimal.valueOf(cultivate).divide(BigDecimal.valueOf(2L), 0, RoundingMode.HALF_UP);
227         if (is(this.supplies).lessThan(halfCultivate)) {
228             throw new IllegalArgumentException("You cannot cultivate more than you have.");
229         }
230 
231         if (is(BigDecimal.valueOf(cultivate)).greaterThan(getPopulation().multiply(BigDecimal.TEN))) {
232             throw new IllegalArgumentException("Not enough workers available.");
233         }
234 
235         // perform seeding
236         this.supplies = this.supplies.subtract(halfCultivate);
237         calculateNewPrice();
238 
239         // yields after cultivation and population increase
240         this.yield = this.cost;
241         this.humans = this.yield.multiply(BigDecimal.valueOf(cultivate));
242 
243         // cultivation kills rats ;)
244         this.externalDamage = BigDecimal.ZERO;
245         calculateNewPrice();
246 
247         // but add some external damage in some cases in a naiive manner
248         // original condition stated: if int(c/2) <> c/2
249         if (this.cost.divide(BigDecimal.valueOf(2L), 0, RoundingMode.DOWN).intValue() == this.cost.divide(BigDecimal.valueOf(2L), 0, RoundingMode.UP).intValue()) {
250             this.externalDamage = this.supplies.divide(this.cost, 0, RoundingMode.HALF_UP);
251         }
252         this.supplies = this.supplies.subtract(this.externalDamage).add(this.humans);
253         calculateNewPrice();
254     }
255 
256     @VisibleForTesting
257     /**
258      * Change price for next round.
259      */
260     void calculateNewPrice() {
261         this.cost = getRandomNumberUntil(5);
262     }
263 
264     /**
265      * Perform "round"-final calculations such as
266      * <ul>
267      * <li>number of people that died</li>
268      * <li>adapt overall (internal) death statistics</li>
269      * <li>refresh internal famine quotient</li>
270      * </ul>
271      */
272     public void finishRoundAfterActions() {
273         BigDecimal factor = BigDecimal.valueOf(20L).multiply(this.area).add(this.supplies);
274         this.increase = cost.multiply(factor).divide(this.population, 0, RoundingMode.HALF_UP).divide(BigDecimal.valueOf(100).add(BigDecimal.ONE), 0, RoundingMode.HALF_UP);
275 
276         this.cost = this.q.divide(BigDecimal.valueOf(20L), 0, RoundingMode.HALF_UP);
277         refreshFamineQuotient();
278 
279         if (is(this.population).lessThan(this.cost)) {
280             this.deathToll = BigDecimal.ZERO;
281             return; // start new round without any deaths
282         }
283 
284         // calculate deaths
285         this.deathToll = this.population.subtract(this.cost);
286         if (is(this.deathToll).greaterThan(this.population.multiply(BigDecimal.valueOf(0.45)))) {
287             System.out.println(KaiserEnginePrinter.ANSI_YELLOW);
288             System.out.println("Sie haben " + this.deathToll + " Menschen in nur einem Jahr verhungern lassen!");
289             System.out.println("Auf Grund dieser extremen Misswirtschaft, werden Sie nicht nur aus Amt und Würden gejagt,");
290             System.out.println("sondern auch zum Versager des Jahres erklärt.");
291             System.out.println(KaiserEnginePrinter.ANSI_RESET);
292             return; // TODO stop the game here!
293         }
294 
295         // calc death statistics
296         // p1 = ((z-1)*p1+D*100/p)/z
297         BigDecimal tempQuotient = this.percentDeathToll.multiply(BigDecimal.valueOf(this.zYear - 1)).add(this.deathToll.multiply(BigDecimal.valueOf(100)).divide(this.population, 0, RoundingMode.HALF_UP));
298         this.percentDeathToll = tempQuotient.divide(BigDecimal.valueOf(this.zYear), 0, RoundingMode.HALF_UP);
299 
300         this.population = this.cost; // TODO why? shouldn't this somehow be added up?
301         this.deathTollSum = this.deathTollSum.add(this.deathToll);
302     }
303 }