-
Notifications
You must be signed in to change notification settings - Fork 1
/
Day24.kt
139 lines (122 loc) · 5.28 KB
/
Day24.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
/*
* Copyright (c) 2018 by Todd Ginsberg
*/
/**
* Advent of Code 2018, Day 24 - Immune System Simulator 20XX
*
* Problem Description: http://adventofcode.com/2018/day/24
* Blog Post/Commentary: https://todd.ginsberg.com/post/advent-of-code/2018/day24/
*/
package com.ginsberg.advent2018
class Day24(immuneInput: List<String>, infectionInput: List<String>) {
private val fighters: List<Group> = immuneInput.map { Group.of(Team.ImmuneSystem, it) } + infectionInput.map { Group.of(Team.Infection, it) }
fun solvePart1(): Int =
fightBattle(fighters).sumBy { it.units }
fun solvePart2(): Int =
generateSequence(1, Int::inc).mapNotNull { boost ->
val combatants = fightBattle(boostImmuneSystem(boost))
if (combatants.all { it.team == Team.ImmuneSystem }) combatants.sumBy { it.units }
else null
}.first()
private fun boostImmuneSystem(boost: Int): List<Group> =
fighters.map {
it.copy(unitDamage = it.unitDamage + if (it.team == Team.ImmuneSystem) boost else 0)
}
// Fight the battle with the given combatants, and return the survivors
private fun fightBattle(combatants: List<Group>): List<Group> {
var deaths: Int? = null
while (deaths != 0) {
deaths = roundOfFighting(combatants)
}
return combatants.filter { it.alive }
}
private fun roundOfFighting(combatants: List<Group>): Int =
findTargets(combatants)
.sortedByDescending { it.first.initiative }
.filterNot { it.second == null }
.map { (attacker, target) ->
if (attacker.alive) attacker.fight(target!!)
else 0
}
.sum()
private fun findTargets(combatants: List<Group>): Set<Pair<Group, Group?>> {
val alreadyTargeted = mutableSetOf<Group>()
return combatants.filter { it.alive }
.sortedWith(compareByDescending<Group> { it.effectivePower() }.thenByDescending { it.initiative })
.map { group ->
group to combatants
.filter { it.alive } // Only fight the living
.filterNot { group.team == it.team } // Only fight the other team
.filterNot { it in alreadyTargeted } // Only fight somebody that isn't already a target
.sortedWith(compareByDescending<Group> { group.calculateDamageTo(it) }.thenByDescending { it.effectivePower() }.thenByDescending { it.initiative })
.filterNot { group.calculateDamageTo(it) == 0 } // Remove any targets that we can't actually damage.
.firstOrNull() // Account for the fact that we *may* not have a target
.also {
// Add our selected target to the targeted list
if (it != null) alreadyTargeted.add(it)
}
}
.toSet()
}
private enum class Team {
ImmuneSystem,
Infection
}
private data class Group(
val team: Team,
var units: Int,
val unitHp: Int,
val unitDamage: Int,
val attackType: String,
val initiative: Int,
val weakTo: Set<String>,
val immuneTo: Set<String>,
var alive: Boolean = true
) {
fun effectivePower(): Int =
units * unitDamage
fun calculateDamageTo(other: Group): Int =
if (this.team == other.team) 0
else effectivePower() * when (attackType) {
in other.immuneTo -> 0
in other.weakTo -> 2
else -> 1
}
fun fight(other: Group): Int {
val unitDeath = calculateDamageTo(other) / other.unitHp
other.units -= unitDeath
if (other.units <= 0) {
other.alive = false
}
return unitDeath
}
companion object {
private val pattern = """^(\d+) units each with (\d+) hit points (\([,; a-z]+\) )?with an attack that does (\d+) (\w+) damage at initiative (\d+)$""".toRegex()
fun of(team: Team, input: String): Group {
pattern.find(input)?.let {
val (units, unitHp, strongWeak, unitDamage, attackType, initiative) = it.destructured
return Group(
team = team,
units = units.toInt(),
unitHp = unitHp.toInt(),
unitDamage = unitDamage.toInt(),
attackType = attackType,
initiative = initiative.toInt(),
weakTo = parseStrongWeak("weak", strongWeak),
immuneTo = parseStrongWeak("immune", strongWeak)
)
}
}
private fun parseStrongWeak(kind: String, input: String): Set<String> =
if (input.isBlank()) emptySet()
else {
val found = input.drop(1).dropLast(2).split("; ").filter { it.startsWith(kind) }
if (found.isEmpty()) {
emptySet()
} else {
found.first().split("to ")[1].split(",").map { it.trim() }.toSet()
}
}
}
}
}