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 | @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 |
1
1. getAreaPerCapita : replaced return value with null for de/aikiit/game/kaiser/KaiserEngine::getAreaPerCapita → KILLED |
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 |
2
1. buyLand : changed conditional boundary → SURVIVED 2. buyLand : negated conditional → SURVIVED |
if (buy < 0) { |
147 |
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); |
148 | } | |
149 | ||
150 |
2
1. buyLand : changed conditional boundary → SURVIVED 2. buyLand : negated conditional → KILLED |
if (buy > 0) { |
151 |
1
1. buyLand : negated conditional → KILLED |
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 |
1
1. buyLand : replaced long return with 0 for de/aikiit/game/kaiser/KaiserEngine::buyLand → KILLED |
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 |
2
1. sellLand : changed conditional boundary → SURVIVED 2. sellLand : negated conditional → KILLED |
if (sell < 0) { |
170 |
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); |
171 | return; | |
172 | } | |
173 | ||
174 |
1
1. sellLand : negated conditional → KILLED |
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 |
2
1. feedToPopulation : changed conditional boundary → SURVIVED 2. feedToPopulation : negated conditional → KILLED |
if (feed < 0) { |
191 |
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); |
192 | return; | |
193 | } | |
194 | ||
195 |
1
1. feedToPopulation : negated conditional → KILLED |
if (feed != 0) { |
196 |
1
1. feedToPopulation : negated conditional → KILLED |
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 |
1
1. cultivate : negated conditional → KILLED |
if (cultivate == 0) { |
213 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → KILLED |
calculateNewPrice(); |
214 | return; | |
215 | } | |
216 | ||
217 |
2
1. cultivate : changed conditional boundary → SURVIVED 2. cultivate : negated conditional → KILLED |
if (cultivate < 0) { |
218 |
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); |
219 | return; | |
220 | } | |
221 | ||
222 |
1
1. cultivate : negated conditional → KILLED |
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 |
1
1. cultivate : negated conditional → KILLED |
if (is(this.supplies).lessThan(halfCultivate)) { |
228 | throw new IllegalArgumentException("You cannot cultivate more than you have."); | |
229 | } | |
230 | ||
231 |
1
1. cultivate : negated conditional → KILLED |
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 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
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 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
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 |
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()) { |
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 |
1
1. cultivate : removed call to de/aikiit/game/kaiser/KaiserEngine::calculateNewPrice → SURVIVED |
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 |
1
1. finishRoundAfterActions : removed call to de/aikiit/game/kaiser/KaiserEngine::refreshFamineQuotient → SURVIVED |
refreshFamineQuotient(); |
278 | ||
279 |
1
1. finishRoundAfterActions : negated conditional → SURVIVED |
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 |
1
1. finishRoundAfterActions : negated conditional → KILLED |
if (is(this.deathToll).greaterThan(this.population.multiply(BigDecimal.valueOf(0.45)))) { |
287 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println(KaiserEnginePrinter.ANSI_YELLOW); |
288 |
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!"); |
289 |
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,"); |
290 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
System.out.println("sondern auch zum Versager des Jahres erklärt."); |
291 |
1
1. finishRoundAfterActions : removed call to java/io/PrintStream::println → SURVIVED |
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 |
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)); |
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 | } | |
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 |
|
135 |
1.1 |
|
146 |
1.1 2.2 |
|
147 |
1.1 |
|
150 |
1.1 2.2 |
|
151 |
1.1 |
|
159 |
1.1 |
|
169 |
1.1 2.2 |
|
170 |
1.1 |
|
174 |
1.1 |
|
190 |
1.1 2.2 |
|
191 |
1.1 |
|
195 |
1.1 |
|
196 |
1.1 |
|
212 |
1.1 |
|
213 |
1.1 |
|
217 |
1.1 2.2 |
|
218 |
1.1 |
|
222 |
1.1 |
|
227 |
1.1 |
|
231 |
1.1 |
|
237 |
1.1 |
|
245 |
1.1 |
|
249 |
1.1 |
|
253 |
1.1 |
|
277 |
1.1 |
|
279 |
1.1 |
|
286 |
1.1 |
|
287 |
1.1 |
|
288 |
1.1 |
|
289 |
1.1 |
|
290 |
1.1 |
|
291 |
1.1 |
|
297 |
1.1 |