···11+import re
22+import copy
33+import fileinput
44+55+from utils import parse_line, parse_nums
66+77+88+class Group:
99+ def __init__(self, is_infection, num, _count, hp, weaknesses, immunities, dmg, type, initiative):
1010+ self.alliance = 'Infection' if is_infection else 'Immune System'
1111+ self.is_infection = is_infection
1212+ self.num = num
1313+ self.count = _count
1414+ self.hp = hp
1515+ self.weaknesses = weaknesses
1616+ self.immunities = immunities
1717+ self.dmg = dmg + (0 if self.is_infection else BOOST)
1818+ self.type = type
1919+ self.initiative = initiative
2020+2121+ @property
2222+ def power(self):
2323+ return self.count * self.dmg
2424+2525+ def calc_dmg(self, other):
2626+ if self.type in other.immunities:
2727+ return 0
2828+ elif self.type in other.weaknesses:
2929+ return 2 * self.power
3030+ else:
3131+ return self.power
3232+3333+ def deal_dmg(self, other):
3434+ dmg = self.calc_dmg(other)
3535+ units_lost = min(dmg // other.hp, other.count)
3636+ other.count -= units_lost
3737+ return units_lost
3838+3939+4040+def setup_simulation():
4141+ immune = {}
4242+ infect = {}
4343+ groups = []
4444+4545+ for i, line in enumerate(IMMUNE):
4646+ g = Group(False, i, *line)
4747+ immune[i] = g
4848+ groups.append(g)
4949+5050+ for i, line in enumerate(INFECT):
5151+ g = Group(True, i, *line)
5252+ infect[i] = g
5353+ groups.append(g)
5454+5555+ return immune, infect, groups
5656+5757+5858+# Read problem input
5959+IMMUNE = []
6060+INFECT = []
6161+BOOST = 0
6262+DEBUG = False
6363+6464+on_infection = False
6565+6666+for i, line in enumerate(fileinput.input()):
6767+ line = line.strip()
6868+6969+ if line == 'Infection:':
7070+ on_infection = True
7171+ continue
7272+7373+ try:
7474+ _count, hp, dmg, initiative = parse_nums(line, negatives=False)
7575+ except Exception:
7676+ continue
7777+ clauses = next(iter(re.findall(r'(\(.+\))', line)), None)
7878+ type = re.findall(r'(\S+) damage', line)
7979+ type = next(iter(type), None)
8080+8181+ parts = (clauses or '').replace(',', '').replace('(', '').replace(')', '').split(';')
8282+ weaknesses = []
8383+ immunities = []
8484+8585+ if parts[0] != '':
8686+ for part in parts:
8787+ things = part.split()
8888+ if things[0] == 'weak':
8989+ weaknesses = things[2:]
9090+ else:
9191+ immunities = things[2:]
9292+9393+ group = (_count, hp, weaknesses, immunities, dmg, type, initiative)
9494+9595+ if on_infection:
9696+ INFECT.append(group)
9797+ else:
9898+ IMMUNE.append(group)
9999+100100+101101+def simulate(boost=0):
102102+ global BOOST
103103+ BOOST = boost
104104+ immune, infection, groups = setup_simulation()
105105+106106+ last_infcount = None
107107+ last_imscount = None
108108+109109+ while True:
110110+ # Target selection
111111+ targets = {}
112112+113113+ if DEBUG:
114114+ for alliance, group in [("Immune System", immune), ("Infection", infection)]:
115115+ print "{}:".format(alliance)
116116+ for n, g in group.items():
117117+ if g.count > 0:
118118+ print "Group {} contains {} units".format(n, g.count)
119119+120120+ print
121121+122122+ for g in sorted(groups, key=lambda g: (g.power, g.initiative), reverse=True):
123123+ if g.count == 0:
124124+ continue
125125+126126+ other = immune if g.is_infection else infection
127127+128128+ target_num = None
129129+130130+ for h in (z for z in groups if z.alliance != g.alliance):
131131+ if h.count == 0:
132132+ continue
133133+134134+ leave = False
135135+ for a, b in targets.items():
136136+ if a[0] == g.alliance and h.num == b:
137137+ leave = True
138138+ break
139139+140140+ if leave:
141141+ continue
142142+143143+ if DEBUG:
144144+ print "{} group {} would deal defending group {} {} damage".format(g.alliance, g.num, h.num, g.calc_dmg(h))
145145+146146+ if g.calc_dmg(h) > 0:
147147+ if target_num is None:
148148+ target_num = h.num
149149+ else:
150150+ poss = other[target_num]
151151+ if g.calc_dmg(h) > g.calc_dmg(poss):
152152+ target_num = h.num
153153+ elif g.calc_dmg(h) == g.calc_dmg(poss):
154154+ if h.power > poss.power:
155155+ target_num = h.num
156156+ elif h.power == poss.power:
157157+ if h.initiative > poss.initiative:
158158+ target_num = h.num
159159+160160+ targets[g.alliance, g.num] = target_num
161161+162162+ if DEBUG:
163163+ print
164164+165165+ # Attack
166166+ for g in sorted(groups, key=lambda g: g.initiative, reverse=True):
167167+ other = immune if g.is_infection else infection
168168+ h = other.get(targets.get((g.alliance, g.num), None), None)
169169+ if h is None:
170170+ continue
171171+ killed = g.deal_dmg(h)
172172+173173+ if DEBUG:
174174+ print "{} group {} attacks defending group {}, killing {} units".format(g.alliance, g.num, h.num, killed)
175175+176176+ if DEBUG:
177177+ print
178178+ print
179179+180180+ infcount = 0
181181+ imscount = 0
182182+ for g in groups:
183183+ if g.is_infection:
184184+ infcount += g.count
185185+ else:
186186+ imscount += g.count
187187+188188+ if infcount == 0:
189189+ return True, imscount
190190+ elif imscount == 0:
191191+ return False, infcount
192192+ elif infcount == last_infcount and imscount == last_imscount:
193193+ return False, None
194194+195195+ last_infcount = infcount
196196+ last_imscount = imscount
197197+198198+print "Units in the winning army:", simulate()[1]
199199+200200+lo = 0
201201+hi = 1000
202202+203203+while lo < hi:
204204+ mid = (lo + hi) // 2
205205+ res, count = simulate(mid)
206206+ if not res:
207207+ lo = mid + 1
208208+ else:
209209+ hi = mid
210210+211211+print "Immune system units after smallest boost ({}): {}".format(mid + 1, simulate(mid + 1)[1])
+23
2018/inputs/24.txt
···11+Immune System:
22+2321 units each with 10326 hit points (immune to slashing) with an attack that does 42 fire damage at initiative 4
33+2899 units each with 9859 hit points with an attack that does 32 slashing damage at initiative 11
44+4581 units each with 7073 hit points (weak to slashing) with an attack that does 11 radiation damage at initiative 9
55+5088 units each with 7917 hit points (weak to slashing; immune to bludgeoning, fire, radiation) with an attack that does 15 fire damage at initiative 17
66+786 units each with 1952 hit points (immune to fire, bludgeoning, slashing, cold) with an attack that does 23 slashing damage at initiative 16
77+3099 units each with 7097 hit points (weak to bludgeoning) with an attack that does 17 radiation damage at initiative 8
88+4604 units each with 4901 hit points with an attack that does 8 fire damage at initiative 13
99+7079 units each with 10328 hit points with an attack that does 14 bludgeoning damage at initiative 18
1010+51 units each with 11243 hit points with an attack that does 1872 cold damage at initiative 15
1111+4910 units each with 5381 hit points (immune to fire; weak to radiation) with an attack that does 10 slashing damage at initiative 19
1212+1313+Infection:
1414+1758 units each with 23776 hit points with an attack that does 24 radiation damage at initiative 2
1515+4000 units each with 12869 hit points with an attack that does 5 cold damage at initiative 14
1616+2319 units each with 43460 hit points (weak to bludgeoning, cold) with an attack that does 33 radiation damage at initiative 3
1717+1898 units each with 44204 hit points (immune to cold; weak to radiation) with an attack that does 39 radiation damage at initiative 1
1818+2764 units each with 50667 hit points (weak to slashing, radiation) with an attack that does 31 radiation damage at initiative 5
1919+3046 units each with 27907 hit points (immune to radiation, fire) with an attack that does 16 slashing damage at initiative 7
2020+1379 units each with 8469 hit points (immune to cold) with an attack that does 8 cold damage at initiative 20
2121+1824 units each with 25625 hit points (immune to bludgeoning) with an attack that does 23 radiation damage at initiative 6
2222+115 units each with 41114 hit points (immune to fire; weak to slashing, bludgeoning) with an attack that does 686 slashing damage at initiative 10
2323+4054 units each with 51210 hit points (immune to radiation, cold, fire) with an attack that does 22 cold damage at initiative 12