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 |
1
1. startNewRound : Replaced integer addition with subtraction → KILLED |
this.zYear++; |
74 | this.population = this.population.add(this.increase); | |
75 | ||
76 |
1
1. startNewRound : removed call to de/aikiit/game/kaiser/KaiserEngine::processFamine → SURVIVED |
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 |
3
1. getRandomNumberUntil : Replaced integer addition with subtraction → SURVIVED 2. getRandomNumberUntil : Replaced integer addition with subtraction → KILLED 3. getRandomNumberUntil : replaced return value with null for de/aikiit/game/kaiser/KaiserEngine::getRandomNumberUntil → KILLED |
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 |
1
1. processFamine : negated conditional → KILLED |
if (is(q).lessThan(BigDecimal.ZERO)) { |
97 | this.population = this.population.divide(BigDecimal.valueOf(2L), 0, RoundingMode.HALF_UP); | |
98 |
1
1. processFamine : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ORANGE); |
99 |
1
1. processFamine : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println("Eine fürchterliche Seuche hat die halbe Stadt dahingerafft!"); |
100 |
1
1. processFamine : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_RESET); |
101 | } | |
102 |
1
1. processFamine : removed call to de/aikiit/game/kaiser/KaiserEngine::refreshFamineQuotient → SURVIVED |
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 | /** | |
114 | * Allow setting the area value for testing purposes. | |
115 | * @param q q value. | |
116 | */ | |
117 | @VisibleForTesting | |
118 | void setQ(BigDecimal q) { | |
119 | this.q = q; | |
120 | } | |
121 | ||
122 | /** | |
123 | * Allow setting the supplies value for testing purposes. | |
124 | * @param supplies current supplies. | |
125 | */ | |
126 | @VisibleForTesting | |
127 | void setSupplies(BigDecimal supplies) { | |
128 | this.supplies = supplies; | |
129 | } | |
130 | ||
131 | /** | |
132 | * Allow setting the area value for testing purposes. | |
133 | * @param area current area. | |
134 | */ | |
135 | @VisibleForTesting | |
136 | void setArea(BigDecimal area) { | |
137 | this.area = area; | |
138 | } | |
139 | ||
140 | /** | |
141 | * Calculates the available area per person in the current game. | |
142 | * | |
143 | * @return area per capita, called <b>L</b> in original. Land ownership? | |
144 | */ | |
145 | public BigDecimal getAreaPerCapita() { | |
146 |
1
1. getAreaPerCapita : replaced return value with null for de/aikiit/game/kaiser/KaiserEngine::getAreaPerCapita → KILLED |
return area.divide(population, 0, RoundingMode.HALF_UP); |
147 | } | |
148 | ||
149 | /** | |
150 | * Performs an act of buying land (new land is acquired by reducing the supplies according to the current land price). | |
151 | * You cannot buy more than you can afford. | |
152 | * | |
153 | * @param buy how many hectares you want to buy. Negative input is ignored. | |
154 | * @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. | |
155 | */ | |
156 | public long buyLand(Long buy) { | |
157 |
2
1. buyLand : changed conditional boundary → SURVIVED 2. buyLand : negated conditional → SURVIVED |
if (buy < 0) { |
158 |
1
1. buyLand : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET); |
159 | } | |
160 | ||
161 |
2
1. buyLand : changed conditional boundary → SURVIVED 2. buyLand : negated conditional → KILLED |
if (buy > 0) { |
162 |
1
1. buyLand : negated conditional → KILLED |
if (is(this.yield.multiply(BigDecimal.valueOf(buy))).lessThanOrEqualTo(this.supplies)) { |
163 | this.area = this.area.add(BigDecimal.valueOf(buy)); | |
164 | this.supplies = this.supplies.subtract(this.yield.multiply(BigDecimal.valueOf(buy))); | |
165 | this.cost = BigDecimal.ZERO; // price is recalculated per round | |
166 | } else { | |
167 | throw new IllegalArgumentException("Not Enough Supplies"); | |
168 | } | |
169 | } | |
170 |
1
1. buyLand : replaced long return with 0 for de/aikiit/game/kaiser/KaiserEngine::buyLand → KILLED |
return buy; |
171 | } | |
172 | ||
173 | /** | |
174 | * Performs an act of selling land (resulting in an increase of supplies as the land is sold to the current land price). | |
175 | * You cannot sell more than you have. | |
176 | * | |
177 | * @param sell how many hectares you want to sell. Negative input is ignored. | |
178 | */ | |
179 | public void sellLand(Long sell) { | |
180 |
2
1. sellLand : changed conditional boundary → SURVIVED 2. sellLand : negated conditional → KILLED |
if (sell < 0) { |
181 |
1
1. sellLand : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET); |
182 | return; | |
183 | } | |
184 | ||
185 |
1
1. sellLand : negated conditional → KILLED |
if (is(BigDecimal.valueOf(sell)).lessThan(this.area)) { |
186 | this.area = this.area.subtract(BigDecimal.valueOf(sell)); | |
187 | this.supplies = this.supplies.add(this.yield.multiply(BigDecimal.valueOf(sell))); | |
188 | this.cost = BigDecimal.ZERO; // price is recalculated per round | |
189 | } else { | |
190 | throw new IllegalArgumentException("Not Enough Land"); | |
191 | } | |
192 | } | |
193 | ||
194 | /** | |
195 | * Performs an act of using supplies to feed your population. | |
196 | * You cannot give more than you have. | |
197 | * | |
198 | * @param feed how many dzt you want to feed | |
199 | */ | |
200 | public void feedToPopulation(Long feed) { | |
201 |
2
1. feedToPopulation : changed conditional boundary → SURVIVED 2. feedToPopulation : negated conditional → KILLED |
if (feed < 0) { |
202 |
1
1. feedToPopulation : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET); |
203 | return; | |
204 | } | |
205 | ||
206 |
1
1. feedToPopulation : negated conditional → KILLED |
if (feed != 0) { |
207 |
1
1. feedToPopulation : negated conditional → KILLED |
if (is(BigDecimal.valueOf(feed)).lessThanOrEqualTo(this.supplies)) { |
208 | this.supplies = this.supplies.subtract(BigDecimal.valueOf(feed)); | |
209 | this.cost = BigDecimal.ONE; // price is recalculated per round | |
210 | } else { | |
211 | throw new IllegalArgumentException("Not Enough in Stock"); | |
212 | } | |
213 | } | |
214 | } | |
215 | ||
216 | /** | |
217 | * Performs an act of using your area and people to cultivate, plant crops for the upcoming season/next round. | |
218 | * You cannot give more than you have. | |
219 | * | |
220 | * @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. | |
221 | */ | |
222 | public void cultivate(Long cultivate) { | |
223 |
1
1. cultivate : negated conditional → KILLED |
if (cultivate == 0) { |
224 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → KILLED |
calculateNewPrice(); |
225 | return; | |
226 | } | |
227 | ||
228 |
2
1. cultivate : changed conditional boundary → SURVIVED 2. cultivate : negated conditional → KILLED |
if (cultivate < 0) { |
229 |
1
1. cultivate : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_PURPLE + "Ignoriere negative Eingaben - Du willst mich wohl verkackeiern." + KaiserEnginePrinter.ANSI_RESET); |
230 | return; | |
231 | } | |
232 | ||
233 |
1
1. cultivate : negated conditional → KILLED |
if (is(this.area).lessThan(BigDecimal.valueOf(cultivate))) { |
234 | throw new IllegalArgumentException("You cannot cultivate more area than you have."); | |
235 | } | |
236 | ||
237 | BigDecimal halfCultivate = BigDecimal.valueOf(cultivate).divide(BigDecimal.valueOf(2L), 0, RoundingMode.HALF_UP); | |
238 |
1
1. cultivate : negated conditional → KILLED |
if (is(this.supplies).lessThan(halfCultivate)) { |
239 | throw new IllegalArgumentException("You cannot cultivate more than you have."); | |
240 | } | |
241 | ||
242 |
1
1. cultivate : negated conditional → KILLED |
if (is(BigDecimal.valueOf(cultivate)).greaterThan(getPopulation().multiply(BigDecimal.TEN))) { |
243 | throw new IllegalArgumentException("Not enough workers available."); | |
244 | } | |
245 | ||
246 | // perform seeding | |
247 | this.supplies = this.supplies.subtract(halfCultivate); | |
248 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
calculateNewPrice(); |
249 | ||
250 | // yields after cultivation and population increase | |
251 | this.yield = this.cost; | |
252 | this.humans = this.yield.multiply(BigDecimal.valueOf(cultivate)); | |
253 | ||
254 | // cultivation kills rats ;) | |
255 | this.externalDamage = BigDecimal.ZERO; | |
256 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
calculateNewPrice(); |
257 | ||
258 | // but add some external damage in some cases in a naiive manner | |
259 | // original condition stated: if int(c/2) <> c/2 | |
260 |
1
1. cultivate : negated conditional → SURVIVED |
if (this.cost.divide(BigDecimal.valueOf(2L), 0, RoundingMode.DOWN).intValue() == this.cost.divide(BigDecimal.valueOf(2L), 0, RoundingMode.UP).intValue()) { |
261 | this.externalDamage = this.supplies.divide(this.cost, 0, RoundingMode.HALF_UP); | |
262 | } | |
263 | this.supplies = this.supplies.subtract(this.externalDamage).add(this.humans); | |
264 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
calculateNewPrice(); |
265 | } | |
266 | ||
267 | /** | |
268 | * Change price for next round. | |
269 | */ | |
270 | @VisibleForTesting | |
271 | void calculateNewPrice() { | |
272 | this.cost = getRandomNumberUntil(5); | |
273 | } | |
274 | ||
275 | /** | |
276 | * Perform "round"-final calculations such as | |
277 | * <ul> | |
278 | * <li>number of people that died</li> | |
279 | * <li>adapt overall (internal) death statistics</li> | |
280 | * <li>refresh internal famine quotient</li> | |
281 | * </ul> | |
282 | */ | |
283 | public void finishRoundAfterActions() { | |
284 | BigDecimal factor = BigDecimal.valueOf(20L).multiply(this.area).add(this.supplies); | |
285 | this.increase = cost.multiply(factor).divide(this.population, 0, RoundingMode.HALF_UP).divide(BigDecimal.valueOf(100).add(BigDecimal.ONE), 0, RoundingMode.HALF_UP); | |
286 | ||
287 | this.cost = this.q.divide(BigDecimal.valueOf(20L), 0, RoundingMode.HALF_UP); | |
288 |
1
1. finishRoundAfterActions : removed call to de/aikiit/game/kaiser/KaiserEngine::refreshFamineQuotient → SURVIVED |
refreshFamineQuotient(); |
289 | ||
290 |
1
1. finishRoundAfterActions : negated conditional → SURVIVED |
if (is(this.population).lessThan(this.cost)) { |
291 | this.deathToll = BigDecimal.ZERO; | |
292 | return; // start new round without any deaths | |
293 | } | |
294 | ||
295 | // calculate deaths | |
296 | this.deathToll = this.population.subtract(this.cost); | |
297 |
1
1. finishRoundAfterActions : negated conditional → KILLED |
if (is(this.deathToll).greaterThan(this.population.multiply(BigDecimal.valueOf(0.45)))) { |
298 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_YELLOW); |
299 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println("Sie haben " + this.deathToll + " Menschen in nur einem Jahr verhungern lassen!"); |
300 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println("Auf Grund dieser extremen Misswirtschaft, werden Sie nicht nur aus Amt und Würden gejagt,"); |
301 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println("sondern auch zum Versager des Jahres erklärt."); |
302 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_RESET); |
303 | return; // TODO stop the game here! | |
304 | } | |
305 | ||
306 | // calc death statistics | |
307 | // p1 = ((z-1)*p1+D*100/p)/z | |
308 |
1
1. finishRoundAfterActions : Replaced integer subtraction with addition → NO_COVERAGE |
BigDecimal tempQuotient = this.percentDeathToll.multiply(BigDecimal.valueOf(this.zYear - 1)).add(this.deathToll.multiply(BigDecimal.valueOf(100)).divide(this.population, 0, RoundingMode.HALF_UP)); |
309 | this.percentDeathToll = tempQuotient.divide(BigDecimal.valueOf(this.zYear), 0, RoundingMode.HALF_UP); | |
310 | ||
311 | this.population = this.cost; // TODO why? shouldn't this somehow be added up? | |
312 | this.deathTollSum = this.deathTollSum.add(this.deathToll); | |
313 | } | |
314 | } | |
Mutations | ||
73 |
1.1 |
|
76 |
1.1 |
|
88 |
1.1 2.2 3.3 |
|
96 |
1.1 |
|
98 |
1.1 |
|
99 |
1.1 |
|
100 |
1.1 |
|
102 |
1.1 |
|
146 |
1.1 |
|
157 |
1.1 2.2 |
|
158 |
1.1 |
|
161 |
1.1 2.2 |
|
162 |
1.1 |
|
170 |
1.1 |
|
180 |
1.1 2.2 |
|
181 |
1.1 |
|
185 |
1.1 |
|
201 |
1.1 2.2 |
|
202 |
1.1 |
|
206 |
1.1 |
|
207 |
1.1 |
|
223 |
1.1 |
|
224 |
1.1 |
|
228 |
1.1 2.2 |
|
229 |
1.1 |
|
233 |
1.1 |
|
238 |
1.1 |
|
242 |
1.1 |
|
248 |
1.1 |
|
256 |
1.1 |
|
260 |
1.1 |
|
264 |
1.1 |
|
288 |
1.1 |
|
290 |
1.1 |
|
297 |
1.1 |
|
298 |
1.1 |
|
299 |
1.1 |
|
300 |
1.1 |
|
301 |
1.1 |
|
302 |
1.1 |
|
308 |
1.1 |