advent2018/24/a.py

257 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
The immune system and the infection each have an army made up of several groups;
Each group consists of one or more identical units.
The armies repeatedly fight until only one army has units remaining.
Units within a group all have the same hit points, attack damage, attack type, initiative, and sometimes weaknesses or immunities.
(higher initiative units attack first and win ties)
Each group also has an effective power: the number of units in that group multiplied by their attack damage.
Each fight consists of two phases: target selection and attacking.
Target selection:
In decreasing order of effective power,
in a tie, the group with the higher initiative chooses first.
The attacking group chooses to target the group in the enemy army to which it would deal the most damage
(after accounting for weaknesses and immunities,
but not accounting for whether the defending group has enough units to actually receive all of that damage).
If an attacking group is considering two defending groups to which it would deal equal damage,
it chooses to target the defending group with the largest effective power;
if there is still a tie, it chooses the defending group with the highest initiative.
If it cannot deal any defending groups damage, it does not choose a target.
Defending groups can only be chosen as a target by one attacking group.
Attacking phase:
each group deals damage to the target it selected, if any. Groups attack in decreasing order of initiative,
regardless of whether they are part of the infection or the immune system.
By default, an attacking group would deal damage equal to its effective power to the defending group.
Immune means no damage
Weak means 2x damage
The defending group only loses whole units from damage;
damage is always dealt in such a way that it kills the most units possible,
Any remaining damage to a unit that does not immediately kill it is ignored
After the fight is over, if both armies still contain units, a new fight begins;
combat only ends once one army has lost all of its units.
"""
from pprint import pprint
import pdb
import re
RE_ARMY = re.compile(r'([0-9]+) units each with ([0-9]+) hit points (\(([^\)]+)\) )?with an attack that does ([0-9]+) ([a-z]+) damage at initiative ([0-9]+)')
RE_MODIFIER = re.compile(r'(weak|immune) to ([a-z, ]+)')
class Army(object):
def __init__(self, tag, team, units, hp, weaknesses, immunes, damage, damage_type, initiative):
# Group number for debugging purposes
self.tag = tag
# Team string name
self.team = team
# Number of units
self.units = units
# Health per unit
self.hp = hp
# set() of weaknesses (string names)
self.weak = weaknesses
# set() of immunities (string names)
self.immune = immunes
# attack power
self.damage = damage
# attack type (string name)
self.damtype = damage_type
# initiative level
self.initiative = initiative
@property
def effpower(self):
return self.units * self.damage
def __repr__(self):
mods = []
modstr = ""
if self.immune:
mods.append("immunte:{}".format('|'.join(self.immune)))
if self.weak:
mods.append("weak:{}".format('|'.join(self.weak)))
if mods:
modstr = " {}".format(' '.join(mods))
return "<Army tag:{} team:'{}' eff:{} units:{} hp:{} damage:{} damtype:{} init:{}{}>" \
.format(self.tag, self.team, self.effpower, self.units, self.hp, self.damage, self.damtype, self.initiative, modstr)
def attack(self, target, simulate=False):
"""
Perform a battle
:param target: army that self will attack
:param simulate: if true, don't modify the target army.
:return: maximum damage dealt if simulating
if not simulating, True if the target army was wiped out. False otherwise
"""
"""
Attacking phase:
each group deals damage to the target it selected, if any. Groups attack in decreasing order of initiative,
regardless of whether they are part of the infection or the immune system.
By default, an attacking group would deal damage equal to its effective power to the defending group.
Immune means no damage
Weak means 2x damage
The defending group only loses whole units from damage;
damage is always dealt in such a way that it kills the most units possible,
Any remaining damage to a unit that does not immediately kill it is ignored
After the fight is over, if both armies still contain units, a new fight begins;
combat only ends once one army has lost all of its units.
"""
# Determine weakness/immunity damage multiplier
if self.damtype in target.immune:
multiplier = 0
elif self.damtype in target.weak:
multiplier = 2
else:
multiplier = 1
damage = multiplier * self.effpower
# if multiplier == 0:
# return False, 0, 0
dead = damage // target.hp
killed = min(target.units, dead)
if simulate:
return damage
target.units -= killed
return target.units == 0, damage, killed
def parsearmies(fname):
armies = []
with open(fname) as f:
teamname = None
groupnum = 1
for line in f.readlines():
line = line.strip()
if not line:
continue
if ":" in line: # Found a new army like "Infection:"
teamname = line[0:-1]
groupnum = 1
else: # an army line to parse
units, hp, _, modifiers, atkpwr, atktype, initia = RE_ARMY.search(line).groups()
mods = {"weak": set(),
"immune": set()}
if modifiers:
for prop in RE_MODIFIER.findall(modifiers):
mods[prop[0]].update(prop[1].split(', '))
armies.append(Army(groupnum, teamname, int(units), int(hp), mods["weak"], mods["immune"],
int(atkpwr), atktype, int(initia)))
groupnum +=1
return armies
def assignbattles(armies):
"""
Target selection:
In decreasing order of effective power,
in a tie, the group with the higher initiative chooses first.
The attacking group chooses to target the group in the enemy army to which it would deal the most damage
(after accounting for weaknesses and immunities,
but not accounting for whether the defending group has enough units to actually receive all of that damage).
If an attacking group is considering two defending groups to which it would deal equal damage,
it chooses to target the defending group with the largest effective power;
if there is still a tie, it chooses the defending group with the highest initiative.
If it cannot deal any defending groups damage, it does not choose a target.
Defending groups can only be chosen as a target by one attacking group.
"""
battles = [] # list of tuples (attacker, defender)
# armies in this list need to be assigned a target
assign = sorted(armies,
key=lambda a: a.effpower * 100000000 + a.initiative, # strongest armies pick a target first
reverse=True)
# print("Assign order")
# for ass in assign:
# print("- {} group {} (eff {})".format(ass.team, ass.tag, ass.effpower))
# print()
targets = set(assign) # these armies can still be attacked
while assign:
army = assign.pop(0) # we're finding a target for this army
best = None # best target
best_dmg = -1 # damage inflicted to best target
best_eff = -1
best_init = -1
considered = False
for target in targets:
if army.team == target.team or army == target: # don't attack same team
continue
considered = True
dmg = army.attack(target, simulate=True)
# print("{} {} -> {} {} damage: {} (target eff={}, init={})".format(army.team, army.tag, target.team, target.tag, dmg, target.effpower, target.initiative))
if (dmg > best_dmg) or \
(dmg == best_dmg and target.effpower > best_eff) or \
(dmg == best_dmg and target.effpower == best_eff and target.initiative > best_init):
best = target
best_dmg = dmg
best_init = target.initiative
best_eff = target.effpower
if not considered: # no targets available
continue
if best_dmg == 0:
continue
targets.remove(best)
battles.append((army, best))
# print("\nBest target for ({} group {} (effpwr {}) :: dmg={})\n\t{} is \n\t{}\n\n".format(army.team, army.tag, army.effpower, best_dmg, army, best))
return sorted(battles, key=lambda b: b[0].initiative, reverse=True)
def main():
armies = parsearmies("input.txt")
while True: # each loop is one round of fighting
# print("\n========== Round ==========")
battles = assignbattles(armies)
if not battles: # one side has been wiped out
break
# print()
for attacker, defender in battles:
if attacker.units > 0:
wiped, dmg, killed = attacker.attack(defender)
# print("{} group {} (i{}) attacks {} group {}, dealing {}, killing {}".format(attacker.team, attacker.tag, attacker.initiative, defender.team, defender.tag, dmg, killed))
if wiped:
# print("{} group {} is wiped out! ({})".format(defender.team, defender.tag, defender.units))
armies.remove(defender)
if len(battles) == 1:
break
# pdb.set_trace()
# input()
# print()
# print()
# pprint(armies)
print(sum([i.units for i in armies]))
if __name__ == '__main__':
main()