Advent of code 2018/24
Ajax Direct

Answer

Part 1 :
Part 2 :
/**
 * 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();