-
Notifications
You must be signed in to change notification settings - Fork 3
/
RobotApplicationService.kt
641 lines (592 loc) · 28.4 KB
/
RobotApplicationService.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
package com.msd.robot.application
import com.msd.application.GameMapService
import com.msd.application.NoResourceOnPlanetException
import com.msd.application.dto.GameMapPlanetDto
import com.msd.command.*
import com.msd.command.application.*
import com.msd.command.application.command.*
import com.msd.config.kafka.core.FailureException
import com.msd.domain.ResourceType
import com.msd.event.application.EventSender
import com.msd.event.application.SuccessEventSender
import com.msd.event.application.dto.*
import com.msd.planet.domain.Planet
import com.msd.robot.domain.LevelTooLowException
import com.msd.robot.domain.Robot
import com.msd.robot.domain.RobotDomainService
import com.msd.robot.domain.UpgradeType
import com.msd.robot.domain.exception.InventoryFullException
//import com.msd.robot.domain.exception.PlanetBlockedException
import com.msd.robot.domain.exception.RobotNotFoundException
import mu.KotlinLogging
import org.springframework.stereotype.Service
import java.util.*
import kotlin.math.floor
@Service
class RobotApplicationService(
val gameMapService: GameMapService,
val robotDomainService: RobotDomainService,
val eventSender: EventSender,
val successEventSender: SuccessEventSender
) {
private val logger = KotlinLogging.logger {}
/**
* Takes a list of commands and passes them on to the corresponding method.
* All commands have to be homogeneous, meaning they can only be of a single Command-Type.
* This method is executed asynchronous and does not block the calling controller.
*
* @param commands List of commands that need to be executed.
*/
fun executeCommands(commands: List<Command>) {
try {
when (commands[0]) {
is FightingCommand -> executeAttacks(commands as List<FightingCommand>)
// is FightingItemUsageCommand -> executeFightingItemUsageCommand(commands as List<FightingItemUsageCommand>)
is MineCommand -> executeMining(commands as List<MineCommand>)
// is MovementItemsUsageCommand -> useMovementItem(commands as List<MovementItemsUsageCommand>)
is MovementCommand -> executeMoveCommands(commands as List<MovementCommand>)
// is BlockCommand -> executeBlockCommands(commands as List<BlockCommand>)
is EnergyRegenCommand -> executeEnergyRegenCommands(commands as List<EnergyRegenCommand>)
// is RepairItemUsageCommand -> executeRepairItemUsageCommands(commands as List<RepairItemUsageCommand>)
}
} catch (runtimeException: RuntimeException) {
// If nothing else caught the Exception, it is a bad one, but we still want to throw an event in that case
eventSender.handleRuntimeException(runtimeException, commands)
}
}
/**
* Executes the given [MovementItemsUsageCommand]. The [Robot's][Robot] `player` and the `command` `playerId` must
* match, otherwise and exception is thrown.
*
* @param command the `MovementItemsUsageCommand` specifying which `Robot` should use which `item`
*/
/* private fun useMovementItem(commands: List<MovementItemsUsageCommand>) {
logger.info("Starting execution of MovementItemUsageCommand-Batch")
val robotPlanetPairs = mutableMapOf<MovementItemsUsageCommand, Pair<Robot, GameMapPlanetDto>>()
commands.forEach { command ->
try {
robotPlanetPairs[command] = robotDomainService.useMovementItem(command.robotUUID, command.itemType)
logger.info("[${command.transactionUUID}] Successfully executed MovementItemUsageCommand")
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, command)
} catch (runtimeException: RuntimeException) {
eventSender.handleRuntimeException(runtimeException, listOf(command))
}
}
successEventSender.sendMovementItemEvents(robotPlanetPairs)
logger.info("Finished executing batch of ItemUsageCommands")
}
*/
/**
* Spawns a new [Robot]. The `Robot` belongs to the specified player and will spawn on the Specified [Planet]
*
* @param player the `UUID` of the player
* @param planet the `UUID` of the `Planet`
*/
fun spawn(player: UUID, planet: UUID, transactionId: UUID): Robot {
val robot = Robot(player, Planet(planet))
robotDomainService.saveRobot(robot)
successEventSender.sendSpawnEvents(player, robot, transactionId)
return robot
}
/**
* Executes a batch of [MovementCommand]s by checking whether the robot exists and the player is the owner of the
* robot. To get the new [Planet] the robot should be positioned on, it calls the GameMap MicroService through
* a connector service [GameMapService]. If everything goes right, the robot gets moved and the corresponding events
* get thrown.
*
* @param moveCommand a list of [MovementCommand]s containing the IDs of the robots which have to move, the players
* who send it and the target `Planets`
*/
fun executeMoveCommands(moveCommands: List<MovementCommand>) {
logger.info("Starting execution of MovementCommand-Batch")
val successfulCommands = mutableMapOf<MovementCommand, Triple<Robot, Int, GameMapPlanetDto>>()
moveCommands.forEach { moveCommand ->
try {
successfulCommands[moveCommand] = move(moveCommand)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, moveCommand)
} catch (runtimeException: RuntimeException) {
eventSender.handleRuntimeException(runtimeException, listOf(moveCommand))
}
}
logger.debug("[Movement] Sending success events for command batch")
successfulCommands.forEach { (command, triple) ->
successEventSender.sendMovementEvents(triple.first, triple.second, command, triple.third)
}
}
private fun move(
moveCommand: MovementCommand
): Triple<Robot, Int, GameMapPlanetDto> {
logger.info("Entered move method")
val robotId = moveCommand.robotUUID
val robot = robotDomainService.getRobot(robotId)
logger.info("Retrieved robot")
val planetDto =
gameMapService.retrieveTargetPlanetIfRobotCanReach(
robot.planet.planetId,
moveCommand.targetPlanetUUID
)
val cost = planetDto.movementDifficulty
val planet = planetDto.toPlanet()
// try {
robot.move(planet, cost)
robotDomainService.saveRobot(robot)
logger.info("[${moveCommand.transactionUUID}] Successfully executed MoveCommand")
return Triple(robot, cost, planetDto)
/* } catch (pbe: PlanetBlockedException) {
logger.info(
"[${moveCommand.transactionUUID}] " +
"Impeded robot ${robot.id} from moving because planet was blocked."
)
robotDomainService.saveRobot(robot)
throw pbe
}
*/
}
/**
* Makes the [Robot] specified in the [BlockCommand] block its current [Planet].
*
* @param blockCommand The `BlockCommand` which specifies which robot should block
* @throws RobotNotFoundException if no robot with the ID specified in the `BlockCommand` can be found
* @throws InvalidPlayerException if the PlayerIDs specified in the `BlockCommand` and `Robot` don't match
*/
/* fun executeBlockCommands(blockCommands: List<BlockCommand>) {
logger.info("Starting execution of BlockCommand-Batch")
blockCommands.forEach { blockCommand ->
try {
val robot = robotDomainService.getRobot(blockCommand.robotUUID)
robot.block()
robotDomainService.saveRobot(robot)
successEventSender.sendBlockEvent(robot, blockCommand)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, blockCommand)
}
}
}
*/
/**
* Regenerates the `energy` of a user specified in [energyRegenCommand]. If the specified [Robot] can not be found or the
* players don't match an exception is thrown.
*
* @param energyRegenCommands a list of [EnergyRegenCommand]s in which the robot which should regenerate its
* `energy` and its player is specified
* @throws RobotNotFoundException When a `Robot` with the specified ID can't be found
*/
fun executeEnergyRegenCommands(energyRegenCommands: List<EnergyRegenCommand>) {
logger.info("Starting execution of EnergyRegenCommand-Batch")
energyRegenCommands.forEach { energyRegenCommand ->
try {
val robot = robotDomainService.getRobot(energyRegenCommand.robotUUID)
robot.regenerateEnergy()
robotDomainService.saveRobot(robot)
successEventSender.sendEnergyRegenEvent(robot, energyRegenCommand)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, energyRegenCommand)
}
}
}
/**
* Upgrades the [Robot's][Robot] specified Upgrade to the given level.
*
* @param robotId the `Robot` which should be updated
* @param upgradeType The upgrade which should increase its level
* @param level the level to which the upgrade should increase
* @throws RobotNotFoundException if there is not `Robot` with the specified ID
* @throws UpgradeException if there is an attempt to skip a level, downgrade or upgrade past the max level
*/
fun upgrade(robotId: UUID, upgradeType: UpgradeType, level: Int) {
val robot = robotDomainService.getRobot(robotId)
robot.upgrade(upgradeType, level)
robotDomainService.saveRobot(robot)
logger.info("Successfully upgraded $upgradeType of robot $robotId")
}
/**
* Execute all attack commands. This has to make sure that all attacks get executed, even if a robot dies during
* the round. After all commands have been executed, dead robots get deleted and their resources distributed
* equally among all living robots on the planet.
*
* This method should never throw any exception. Exceptions occurring during the execution of a single command get
* handled right then and should not disturb the execution of the following commands.
*
* @param fightingCommands A list of AttackCommands that should be executed
*/
fun executeAttacks(fightingCommands: List<FightingCommand>) {
logger.info("Starting execution of FightingCommand-Batch")
val battleFields = executeFights(fightingCommands)
postFightCleanup(battleFields)
logger.info("Finished executing batch of FightingCommands")
}
/**
* Execute the fightingCommands by retrieving attacker and attacked for each command and dealing the damage.
* For each attack a fightingEvent is emitted and the planets on which the fighting occurs are stored for
* cleanup.
*
* @return the list of planet-IDs on which a fight happened.
*/
private fun executeFights(
fightingCommands: List<FightingCommand>
): MutableSet<UUID> {
val battleFields = mutableSetOf<UUID>()
fightingCommands.forEach {
try {
val attacker = robotDomainService.getRobot(it.robotUUID)
val target = robotDomainService.getRobot(it.targetRobotUUID)
robotDomainService.fight(attacker, target)
successEventSender.sendFightingEvent(it, target, attacker)
battleFields.add(attacker.planet.planetId)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, it)
}
}
logger.debug(
"Successful Fights happened on following planets:\n" +
battleFields.fold("") { agg, battlefield -> "$agg- $battlefield,\n" }
)
return battleFields
}
/**
* Clean up the affected planets (called battleFields) and send the events for resource distribution
*/
private fun postFightCleanup(battleFields: MutableSet<UUID>) {
logger.debug("Starting cleanup for planets after fight")
battleFields.forEach { planetId ->
val affectedRobots = robotDomainService.postFightCleanup(planetId)
logger.debug(
"Clean up on planet $planetId affected following robots:\n" +
affectedRobots.fold("") { agg, robot -> "$agg- $robot.id\n" }
)
affectedRobots.forEach {
successEventSender.sendResourceDistributionEvent(it)
}
}
}
/**
* Execute the list of RepairItemUsageCommands by:
* - Making the specified [Robot] use the specified [ReparationItem][RepairItemType].
* - Handling failures by calling the eventSender without stopping the execution of the remaining commands.
* - Sending events after successful usage.
*
* @param commands a list of [RepairItemUsageCommand]s which specify which `Robot` should use which item
*/
/*
fun executeRepairItemUsageCommands(commands: List<RepairItemUsageCommand>) {
logger.info("Starting execution of RepairItemUsageCommand-Batch")
commands.forEach { command ->
try {
val robots = robotDomainService.useRepairItem(command.robotUUID, command.itemType)
successEventSender.sendRepairItemEvent(command, robots)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, command)
}
}
}
/**
* Executes all [AttackItemUsageCommands][FightingItemUsageCommand]. The failure of one command execution does not
* impair the other command executions. After all commands have been executed, the battlefields get cleaned up,
* i.e. all dead robots get removed and their resources distributed between the remaining robots on the planet.
*
* @param usageCommands: The AttackItemUsageCommands that should be executed
*/
fun executeFightingItemUsageCommand(usageCommands: List<FightingItemUsageCommand>) {
val battleFields = useFightingItem(usageCommands)
postFightCleanup(battleFields)
}
/**
* Executes the FightingItemUsageCommand
*/
private fun useFightingItem(
usageCommands: List<FightingItemUsageCommand>
): MutableSet<UUID> {
logger.info("Starting execution of FightingItemUsageCommand-Batch")
val battleFields = mutableSetOf<UUID>()
usageCommands.forEach {
try {
val robot = robotDomainService.getRobot(it.robotUUID)
val (battlefield, targetRobots) = robotDomainService.useAttackItem(
it.robotUUID,
it.targetUUID,
it.itemType
)
successEventSender.sendAttackItemEvents(targetRobots, it, robot)
battleFields.add(battlefield)
} catch (fe: FailureException) {
eventSender.handleFailureException(fe, it)
}
}
return battleFields
}
*/
/**
* Repairs the specified [Robot] to full health.
*
* @param robotId the [UUID] of the to be repaired robot.
*/
fun repair(robotId: UUID) {
val robot = robotDomainService.getRobot(robotId)
robot.repair()
logger.info("Successfully repaired robot $robotId")
robotDomainService.saveRobot(robot)
}
/**
* Executes all mining commands.
*
* @param mineCommands: A list of MineCommands that need to be executed.
*/
fun executeMining(mineCommands: List<MineCommand>) {
logger.info("Starting execution of MiningCommand-Batch")
val resourcesByPlanets = getResourcesOnPlanets(mineCommands)
logger.debug("Fetched resources on planets")
val validMineCommands = replaceIdsWithObjectsIfMineCommandIsValid(mineCommands, resourcesByPlanets)
val amountsByGroupedPlanet = calculateResourceAmountRequestedPerPlanet(validMineCommands)
amountsByGroupedPlanet.forEach { (planet, amount) ->
mineResourcesOnPlanet(validMineCommands, planet, amount)
}
validMineCommands.forEach {
successEventSender.sendMiningEvent(it)
}
logger.info("Finished executing batch of MineCommands")
}
/**
* Create a map of planets and the resource each offers. Each planet either has a single resource or none,
* represented with a null value.
*
* @param mineCommands: A list of commands, for which the resources should be fetched.
* @return a map assigning each planet a resource or a null value
*/
private fun getResourcesOnPlanets(mineCommands: List<MineCommand>): Map<UUID, ResourceType?> = mineCommands
.map {
try {
robotDomainService.getRobot(it.robotUUID).planet.planetId
} catch (rnfe: RobotNotFoundException) {
// we will handle this later, for we are just interested in the planets
logger.debug("[Mining] Robot with ${it.robotUUID} not found")
null
}
}
.filterNotNull()
.distinct()
.map {
it to try {
gameMapService.getResourceOnPlanet(it)
} catch (re: RuntimeException) {
// if there was any problem we just put null, this will cause an exception to be thrown later on
logger.debug("[Mining] Failed to get resource on planet $it from map service")
null
}
}
.filter { it.second != null }
.toList()
.toMap()
/**
* Creates a list of [ValidMineCommand]s, which represent valid [MineCommand]s but using the entity objects instead
* of their IDs. A MineCommand is valid, if
* 1. the specified robot UUID corresponds to an actual robot
* 2. the planet on which the robot is positioned exists and has a resource patch on it
* 3. the robot has the necessary mining level to mine the resource on the planet
*
* @param mineCommands: The list of [MineCommand]s which gets filtered for validity
* @param planetsToResources: A map of planets assigning each a resourceType or a null value, representing no
* resource present on the planet.
* @return a list of [ValidMineCommand]s, representing only the MineCommands which are valid and having replaced
* the IDs with the corresponding entities.
*/
private fun replaceIdsWithObjectsIfMineCommandIsValid(
mineCommands: List<MineCommand>,
planetsToResources: Map<UUID, ResourceType?>
): MutableList<ValidMineCommand> {
val validMineCommands = mutableListOf<ValidMineCommand>()
// TODO("Throw NotEnoughEnergyException here?")
for (mineCommand in mineCommands) {
try {
val robot = robotDomainService.getRobot(mineCommand.robotUUID)
val resource = planetsToResources[robot.planet.planetId]
?: throw NoResourceOnPlanetException(robot.planet.planetId)
val validMineCommand = ValidMineCommand(
robot, robot.planet.planetId,
mineCommand.transactionUUID, resource, robot.miningSpeed
)
if (!robot.canMine(resource))
throw LevelTooLowException("The mining level of the robot is too low to mine the resource $resource")
validMineCommands.add(validMineCommand)
logger.debug("[${mineCommand.transactionUUID}] Created ValidMineCommand")
} catch (re: FailureException) {
eventSender.handleFailureException(re, mineCommand)
}
}
return validMineCommands
}
/**
* Return the accumulated amount of requested resources for each distinct planet in the mineCommands.
* This is achieved by grouping the commands by their planet and then accumulating the requested amount of each
* planet.
*
* @Param mineCommands: A list of ValidMineCommands, each containing a requested amount and a planet.
* @return A map connecting the distinct planets to the amount of resources requested from their resource.
*/
private fun calculateResourceAmountRequestedPerPlanet(mineCommands: MutableList<ValidMineCommand>): Map<UUID, Int> {
val amountsByGroupedPlanet = mineCommands.groupingBy { it.planet }.fold(
{ _, _ -> 0 },
{ _, acc, element ->
acc + element.amountRequested
}
)
return amountsByGroupedPlanet
}
/**
* Tries to mine the specified amount of resources on the planet and distributes the actual amount between the
* mining robots on the planet.
*
* @param validMineCommands the list of commands specifying all minings taking place
* @param planet: The planetId for which to execute the MineCommands
* @param amount: pre-aggregated amount of resources to mine on the planet
*/
private fun mineResourcesOnPlanet(
validMineCommands: MutableList<ValidMineCommand>,
planet: UUID,
amount: Int
) {
val miningsOnPlanet = validMineCommands.filter { it.planet == planet }
val miningRobotsOnPlanet = miningsOnPlanet.map { it.robot }
val resource = miningsOnPlanet[0].resource
try {
val minedAmount = gameMapService.mine(planet, amount)
distributeMinedResources(miningRobotsOnPlanet, minedAmount, resource)
logger.debug("Mined and distributed resources on planet $planet: $minedAmount $resource")
} catch (failureException: FailureException) {
eventSender.handleAll(
failureException,
validMineCommands.map {
MineCommand(it.robot.id, it.transactionId)
}
)
} catch (runtimeException: RuntimeException) {
logger.error("[Mining] Error during mining call to map service for planet $planet")
eventSender.handleRuntimeException(
runtimeException,
validMineCommands.map {
MineCommand(it.robot.id, it.transactionId)
}
)
}
}
/**
* Distribute the resources to the robots.
* There is no perfectly fair way to share those resources, but we try to get close. The current method favors
* robots with a higher miningSpeed.
*
* @param robots: The robots to which the resources get distributed.
* @param amount: The amount of resources to distribute.
* @param resource: The type of resource of which the given amount gets distributed.
*/
private fun distributeMinedResources(robots: List<Robot>, amount: Int, resource: ResourceType) {
val (amountDistributed, robotsDecimalPlaces) = distributeByMiningSpeed(robots, amount, resource)
distributeRemainingByDecimalPlaces(robotsDecimalPlaces, amount - amountDistributed, resource)
robotDomainService.saveAll(robots)
}
/**
* Distributes the mined resources to the robots corresponding to their miningSpeed.
* A robot with the mining speed 10 gets double the amount of resources a robot with miningSpeed 5 gets.
*
* @return The remaining resources that could not get distributed this way, because we don't distribute partial resources
* (fractions of a resource), together with the decimalPlaces of the fraction the robot would have
* been assigned if we split the resources into partial resources.
*
* Example: Three robots with the miningSpeeds 10, 15 and 20 get distributed 22 resources. With this method
* we can distribute 20 resources (4, 7 and 9 respectively). If we could distribute fractions of resources robot1
* would have gotten 4.88, robot2 7.33 and robot3 9.77 resources. We distribute the whole resources and return
* the amount left (2) together with the decimal places for each robot(0.88, 0.33 and 0.77). The decimal places can
* be used to determine which robot(s) should get the remaining resources.
*
* @param robots: The robots to which to distribute the resources
* @param amount: Amount of resources to be distributed
* @param resource: The type of resource to be distributed
*/
private fun distributeByMiningSpeed(
robots: List<Robot>,
amount: Int,
resource: ResourceType
): Pair<Int, MutableMap<Robot, Double>> {
logger.debug("Distributing $amount of type $resource by mining speed")
val accumulatedMiningSpeed = robots.fold(0) { acc, robot -> acc + robot.miningSpeed }
var amountDistributed = 0
val robotsDecimalPlaces = mutableMapOf<Robot, Double>()
robots.forEach { robot ->
amountDistributed += distributeToRobotByMiningSpeed(
robot,
accumulatedMiningSpeed,
amount,
robotsDecimalPlaces,
resource
)
}
return Pair(amountDistributed, robotsDecimalPlaces)
}
/**
* Distribute the resource to the robot by its fair share, calculated by the portion of mining speed it has
* relative to the mining speed of all robots mining on the same planet.
*
* @param robot: The robot to receive the resources
* @param accumulatedMiningSpeed: The mining speed of all robots mining on the planet combined
* @param amount: The overall amount to be distributed to all robots
* @param robotsDecimalPlaces: Map that needs to get updated with the decimal places of the correspondingAmount
* @param resourceType: The ResourceType of the distributed resource
*
* @return the amount distributed to the robot
*/
private fun distributeToRobotByMiningSpeed(
robot: Robot,
accumulatedMiningSpeed: Int,
amount: Int,
robotsDecimalPlaces: MutableMap<Robot, Double>,
resourceType: ResourceType
): Int {
val correspondingAmount = floor((robot.miningSpeed.toDouble() / accumulatedMiningSpeed) * amount).toInt()
val remainder = ((robot.miningSpeed.toDouble() / accumulatedMiningSpeed) * amount) - correspondingAmount
robotsDecimalPlaces[robot] = remainder
try {
robot.inventory.addResource(resourceType, correspondingAmount)
} catch (ife: InventoryFullException) {
logger.info("[Mining] Robot did not receive all resources granted to it, because its inventory was full")
}
return correspondingAmount
}
/**
* Distributes the remainingAmount of resources to the robots, favoring the robots with the highest decimalPlaces.
*
* @param robotsDecimalPlaces: A map assigning each robot the remaining fraction of a resource it didn't get
* but deserved due to it's mining level.
* @param remainingAmount: The remaining amount of resources to be distributed
* @param resource: The type of resource to be distributed
*
* Example: The robots get the remaining resources one by one starting with the robot with the highest remaining
* fraction. The fractions (0.88, 0.33 and 0.77) with 2 resources to be distributed lead to a distribution of
* robot1: 1
* robot2: 0
* robot3: 1
*/
private fun distributeRemainingByDecimalPlaces(
robotsDecimalPlaces: MutableMap<Robot, Double>,
remainingAmount: Int,
resource: ResourceType
) {
logger.debug("Distributing $remainingAmount of type $resource by decimal places")
var amountDistributed = 0
val sortedDecimalPlaces = robotsDecimalPlaces.entries.sortedBy { it.value }.reversed()
var index = 0
val fullRobots = mutableSetOf<Robot>()
while (amountDistributed < remainingAmount && fullRobots.count() < sortedDecimalPlaces.size) {
try {
sortedDecimalPlaces[index % sortedDecimalPlaces.size].key.inventory.addResource(resource, 1)
amountDistributed += 1
} catch (ife: InventoryFullException) {
fullRobots.add(sortedDecimalPlaces[index % sortedDecimalPlaces.size].key)
logger.debug(
"[Mining] Robot did not receive all resources granted to it, because its " +
"inventory was full"
)
}
index++
}
}
}