This commit is contained in:
dave 2018-12-24 20:55:45 -05:00
parent 8bb9e32344
commit 89ee8ffd51
3 changed files with 461 additions and 0 deletions

256
24/a.py Executable file
View File

@ -0,0 +1,256 @@
#!/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()

182
24/b.py Executable file
View File

@ -0,0 +1,182 @@
#!/usr/bin/env python3
from sys import exit
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
"""
if self.damtype in target.immune:
multiplier = 0
elif self.damtype in target.weak:
multiplier = 2
else:
multiplier = 1
damage = multiplier * self.effpower
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):
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)
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)
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))
return sorted(battles, key=lambda b: b[0].initiative, reverse=True)
def runsim(armies):
while True: # each loop is one round of fighting
battles = assignbattles(armies)
if not battles: # one side has been wiped out
break
had_casualties = False
for attacker, defender in battles:
if attacker.units > 0:
wiped, dmg, killed = attacker.attack(defender)
had_casualties = had_casualties or killed > 0
if wiped:
armies.remove(defender)
if len(battles) == 1:
break
if not had_casualties:
return
if armies[0].team == "Immune System":
print(sum([i.units for i in armies]))
exit(0)
def main():
boost = 80 # semi-arbitrary starting point to save time. Tweak if it doesn't fit your input
while True:
armies = parsearmies("input.txt")
for unit in armies:
if unit.team == "Immune System":
unit.damage += boost
print(boost)
runsim(armies)
boost += 1
if __name__ == '__main__':
main()

23
24/input.txt Normal file
View File

@ -0,0 +1,23 @@
Immune System:
1514 units each with 8968 hit points (weak to cold) with an attack that does 57 bludgeoning damage at initiative 9
2721 units each with 6691 hit points (weak to cold) with an attack that does 22 slashing damage at initiative 15
1214 units each with 10379 hit points (immune to bludgeoning) with an attack that does 69 fire damage at initiative 16
2870 units each with 4212 hit points with an attack that does 11 radiation damage at initiative 12
1239 units each with 5405 hit points (weak to cold) with an attack that does 37 cold damage at initiative 18
4509 units each with 4004 hit points (weak to cold; immune to radiation) with an attack that does 8 slashing damage at initiative 20
3369 units each with 10672 hit points (weak to slashing) with an attack that does 29 cold damage at initiative 11
2890 units each with 11418 hit points (weak to fire; immune to bludgeoning) with an attack that does 30 cold damage at initiative 8
149 units each with 7022 hit points (weak to slashing) with an attack that does 393 radiation damage at initiative 13
2080 units each with 5786 hit points (weak to fire; immune to slashing, bludgeoning) with an attack that does 20 fire damage at initiative 7
Infection:
817 units each with 47082 hit points (immune to slashing, radiation, bludgeoning) with an attack that does 115 cold damage at initiative 3
4183 units each with 35892 hit points with an attack that does 16 bludgeoning damage at initiative 1
7006 units each with 11084 hit points with an attack that does 2 fire damage at initiative 2
4804 units each with 25411 hit points with an attack that does 10 cold damage at initiative 14
6262 units each with 28952 hit points (weak to fire) with an attack that does 7 slashing damage at initiative 10
628 units each with 32906 hit points (weak to slashing) with an attack that does 99 radiation damage at initiative 4
5239 units each with 46047 hit points (immune to fire) with an attack that does 14 bludgeoning damage at initiative 6
1173 units each with 32300 hit points (weak to cold, slashing) with an attack that does 53 bludgeoning damage at initiative 19
3712 units each with 12148 hit points (immune to cold; weak to slashing) with an attack that does 5 slashing damage at initiative 17
334 units each with 43582 hit points (weak to cold, fire) with an attack that does 260 cold damage at initiative 5