/**
* Parse groups & teams from input
*/
function parse($input) {
$id = 0;
$groups = $input->split("\n\n")->map(function ($army, $team) use (&$id) {
return $army->lines()->slice(1)->map(function ($group) use ($team, &$id) {
preg_match(implode("", [
"/(\d+) units each with (\d+) hit points",
" ?\(?([^)]*)\)?",
" with an attack that does (\d+) ([^ ]+) damage",
" at initiative (\d+)/"
]), $group, $matches);
[$_, $count, $hp, $defstring, $atk, $type, $ini] = $matches;
$def = [];
if ($defstring) foreach (explode("; ", $defstring) as $d) {
$d = explode(" to ", $d);
$def[$d[0]] = explode(", ", $d[1]);
}
return [$id++, $team, $count, $hp, $atk, $type, $ini, $def];
});
}, true)->merge();
$teams = $groups->reduce(function ($t, $group) {
$t[$group[1]][] = $group[0];
return $t;
}, []);
return [$groups, $teams];
}
/**
* Damage done from an attaching team to a defending team
*/
function dmg($attacking, $defending) {
$mult = in_array($attacking[5], $defending[7]["immune"] ?? []) ? 0
: (in_array($attacking[5], $defending[7]["weak"] ?? []) ? 2 : 1);
return $attacking[2] * $attacking[4] * $mult;
}
/**
* FIIIIGHT
*/
function fight($groups, $teams) {
while (true) {
// Keep track of HPs at begining of turn to detect endless fights
$hps = $groups->sortKeys()->column(2)->join("|");
// Sort living groups by effective power and initiative
$groups = $groups->sort(fn ($a, $b) =>
($b[2] * $b[4] * 1000 + $b[6]) <=> ($a[2] * $a[4] * 1000 + $a[6])
);
// Chose target for each group
$targets = set();
foreach ($groups as $g=>$group) {
$enemies = $groups
->removeKeys($teams[$group[1]])
->removeKeys($targets)
->map(fn ($enemy) => dmg($group, $enemy));
if (!$enemies->empty() && $enemies->max()) $targets[$g] = $enemies->search($enemies->max());
}
// Attack each target in order of initiative
$groups = $groups->sort(fn ($a, $b) => $b[6] <=> $a[6]);
foreach ($groups as $g=>$group) {
if (!isset($targets[$g])) continue;
if (!isset($groups[$g])) continue;
$target = $targets[$g];
$enemy = &$groups[$targets[$g]];
$dmg = dmg($group, $enemy);
$enemy[2] -= floor($dmg / $enemy[3]);
// Target has no more solider, remove it from lists
if ($enemy[2] <= 0) {
unset($groups[$target]);
$teams = array_map(fn ($t) => array_diff($t, [$target]), $teams);
// One team has no more groups, end fight
if (empty($teams[0]) || empty($teams[1])) return [$groups, !empty($teams[0])];
};
}
// Nothing changed this turn, the fight will be endless
if ($hps == $groups->sortKeys()->column(2)->join("|")) return [null, null];
}
}
// ==================================================
// > PART 1
// ==================================================
[$groups, $teams] = parse($input);
$solution_1 = fight(clone $groups, $teams)[0]->column(2)->sum();
// ==================================================
// > PART 2
// ==================================================
do {
foreach ($teams[0] as $t) $groups[$t][4] += 1; // Could be faster with binary search or the like
[$left, $has_won] = fight(clone $groups, $teams);
} while (!$has_won);
$solution_2 = $left->column(2)->sum();