|
| 1 | +# -------------------------------- Input data ---------------------------------------- # |
| 2 | +importos,re |
| 3 | + |
| 4 | +test_data= {} |
| 5 | + |
| 6 | +test=1 |
| 7 | +test_data[test]= { |
| 8 | +"input":"""Immune System: |
| 9 | +17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2 |
| 10 | +989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3 |
| 11 | +
|
| 12 | +Infection: |
| 13 | +801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1 |
| 14 | +4485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4""", |
| 15 | +"expected": ["5216","Unknown"], |
| 16 | +} |
| 17 | + |
| 18 | + |
| 19 | +test="real" |
| 20 | +input_file=os.path.join( |
| 21 | +os.path.dirname(__file__), |
| 22 | +"Inputs", |
| 23 | +os.path.basename(__file__).replace(".py",".txt"), |
| 24 | +) |
| 25 | +test_data[test]= { |
| 26 | +"input":open(input_file,"r+").read().strip(), |
| 27 | +"expected": ["22676","4510"], |
| 28 | +} |
| 29 | + |
| 30 | +# -------------------------------- Control program execution ------------------------- # |
| 31 | + |
| 32 | +case_to_test="real" |
| 33 | +part_to_test=2 |
| 34 | +verbose=False |
| 35 | + |
| 36 | +# -------------------------------- Initialize some variables ------------------------- # |
| 37 | + |
| 38 | +puzzle_input=test_data[case_to_test]["input"] |
| 39 | +puzzle_expected_result=test_data[case_to_test]["expected"][part_to_test-1] |
| 40 | +puzzle_actual_result="Unknown" |
| 41 | + |
| 42 | + |
| 43 | +# -------------------------------- Actual code execution ----------------------------- # |
| 44 | + |
| 45 | + |
| 46 | +defchoose_target(opponents,unit,ignore_targets): |
| 47 | +targets= [] |
| 48 | +foropponentinopponents: |
| 49 | +# Same team |
| 50 | +ifopponent[-2]==unit[-2]: |
| 51 | +continue |
| 52 | +# target is already targetted |
| 53 | +ifopponent[-2:]inignore_targets: |
| 54 | +continue |
| 55 | + |
| 56 | +# Determine multipliers |
| 57 | +ifunit[3]inopponent[5]: |
| 58 | +multiplier=0 |
| 59 | +elifunit[3]inopponent[6]: |
| 60 | +multiplier=2 |
| 61 | +else: |
| 62 | +multiplier=1 |
| 63 | + |
| 64 | +# Order: damage, effective power, initiative |
| 65 | +target= ( |
| 66 | +unit[0]*unit[2]*multiplier, |
| 67 | +opponent[0]*opponent[2], |
| 68 | +opponent[4], |
| 69 | +opponent, |
| 70 | + ) |
| 71 | +targets.append(target) |
| 72 | + |
| 73 | +targets.sort(reverse=True) |
| 74 | + |
| 75 | +iflen(targets)>0: |
| 76 | +returntargets[0] |
| 77 | + |
| 78 | + |
| 79 | +defdetermine_damage(attacker,defender): |
| 80 | +# Determine multipliers |
| 81 | +ifattacker[3]indefender[5]: |
| 82 | +multiplier=0 |
| 83 | +elifattacker[3]indefender[6]: |
| 84 | +multiplier=2 |
| 85 | +else: |
| 86 | +multiplier=1 |
| 87 | + |
| 88 | +returnattacker[0]*attacker[2]*multiplier |
| 89 | + |
| 90 | + |
| 91 | +defattack_order(units): |
| 92 | +# Decreasing order of initiative |
| 93 | +units.sort(key=lambdaunit:unit[4],reverse=True) |
| 94 | +returnunits |
| 95 | + |
| 96 | + |
| 97 | +deftarget_selection_order(units): |
| 98 | +# Decreasing order of effective power then initiative |
| 99 | +units.sort(key=lambdaunit: (unit[0]*unit[2],unit[4]),reverse=True) |
| 100 | +returnunits |
| 101 | + |
| 102 | + |
| 103 | +defteams(units): |
| 104 | +teams=set([unit[-2]forunitinunits]) |
| 105 | +returnteams |
| 106 | + |
| 107 | + |
| 108 | +defteam_size(units): |
| 109 | +teams= { |
| 110 | +team:len([unitforunitinunitsifunit[-2]==team]) |
| 111 | +forteamin ("Immune System:","Infection:") |
| 112 | + } |
| 113 | +returnteams |
| 114 | + |
| 115 | + |
| 116 | +regex="([0-9]*) units each with ([0-9]*) hit points (?:\((immune|weak) to ([a-z]*)(?:, ([a-z]*))*(?:; (immune|weak) to ([a-z]*)(?:, ([a-z]*))*)?\))? ?with an attack that does ([0-9]*) ([a-z]*) damage at initiative ([0-9]*)" |
| 117 | +units= [] |
| 118 | +forstringinpuzzle_input.split("\n"): |
| 119 | +ifstring=="": |
| 120 | +continue |
| 121 | + |
| 122 | +ifstring=="Immune System:"orstring=="Infection:": |
| 123 | +team=string |
| 124 | +continue |
| 125 | + |
| 126 | +matches=re.match(regex,string) |
| 127 | +ifmatchesisNone: |
| 128 | +print(string) |
| 129 | +items=matches.groups() |
| 130 | + |
| 131 | +# nb_units, hitpoints, damage, damage type, initative, immune, weak, team, number |
| 132 | +unit= [ |
| 133 | +int(items[0]), |
| 134 | +int(items[1]), |
| 135 | +int(items[-3]), |
| 136 | +items[-2], |
| 137 | +int(items[-1]), |
| 138 | + [], |
| 139 | + [], |
| 140 | +team, |
| 141 | +team_size(units)[team]+1, |
| 142 | + ] |
| 143 | +foriteminitems[2:-3]: |
| 144 | +ifitemisNone: |
| 145 | +continue |
| 146 | +ifitemin ("immune","weak"): |
| 147 | +attack_type=item |
| 148 | +else: |
| 149 | +ifattack_type=="immune": |
| 150 | +unit[-4].append(item) |
| 151 | +else: |
| 152 | +unit[-3].append(item) |
| 153 | + |
| 154 | +units.append(unit) |
| 155 | + |
| 156 | + |
| 157 | +boost=0 |
| 158 | +min_boost=0 |
| 159 | +max_boost=10**9 |
| 160 | +winner="Infection:" |
| 161 | +base_units= [unit.copy()forunitinunits] |
| 162 | +whileTrue: |
| 163 | +ifpart_to_test==2: |
| 164 | +# Update boost for part 2 |
| 165 | +ifwinner=="Infection:"orwinner=="None": |
| 166 | +min_boost=boost |
| 167 | +ifmax_boost==10**9: |
| 168 | +boost+=20 |
| 169 | +else: |
| 170 | +boost= (min_boost+max_boost)//2 |
| 171 | +else: |
| 172 | +max_boost=boost |
| 173 | +boost= (min_boost+max_boost)//2 |
| 174 | +ifmin_boost==max_boost-1: |
| 175 | +break |
| 176 | + |
| 177 | +units= [unit.copy()forunitinbase_units] |
| 178 | +foruidinrange(len(units)): |
| 179 | +ifunits[uid][-2]=="Immune System:": |
| 180 | +units[uid][2]+=boost |
| 181 | +print("Applying boost",boost) |
| 182 | + |
| 183 | +whilelen(teams(units))>1: |
| 184 | +units_killed=0 |
| 185 | +ifverbose: |
| 186 | +print() |
| 187 | +print("New Round") |
| 188 | +print([(x[-2:],x[0],"units")forxinunits]) |
| 189 | +order=target_selection_order(units) |
| 190 | +targets= {} |
| 191 | +forunitinorder: |
| 192 | +target=choose_target(units,unit, [x[3][-2:]forxintargets.values()]) |
| 193 | +iftarget: |
| 194 | +iftarget[0]!=0: |
| 195 | +targets[unit[-2]+str(unit[-1])]=target |
| 196 | + |
| 197 | +order=attack_order(units) |
| 198 | +forunitinorder: |
| 199 | +ifunit[-2]+str(unit[-1])notintargets: |
| 200 | +continue |
| 201 | +target=targets[unit[-2]+str(unit[-1])] |
| 202 | +position=units.index(target[3]) |
| 203 | +damage=determine_damage(unit,target[3]) |
| 204 | +kills=determine_damage(unit,target[3])//target[3][1] |
| 205 | +units_killed+=kills |
| 206 | +target[3][0]-=kills |
| 207 | +iftarget[3][0]>0: |
| 208 | +units[position]=target[3] |
| 209 | +else: |
| 210 | +delunits[position] |
| 211 | + |
| 212 | +ifverbose: |
| 213 | +print( |
| 214 | +unit[-2:], |
| 215 | +"attacked", |
| 216 | +target[3][-2:], |
| 217 | +"dealt", |
| 218 | +damage, |
| 219 | +"damage and killed", |
| 220 | +kills, |
| 221 | + ) |
| 222 | + |
| 223 | +ifunits_killed==0: |
| 224 | +break |
| 225 | + |
| 226 | +puzzle_actual_result=sum([x[0]forxinunits]) |
| 227 | +ifpart_to_test==1: |
| 228 | +break |
| 229 | +else: |
| 230 | +ifunits_killed==0: |
| 231 | +winner="None" |
| 232 | +else: |
| 233 | +winner=units[0][-2] |
| 234 | +print("Boost",boost," - Winner:",winner) |
| 235 | +ifverbose: |
| 236 | +print([unit[0]forunitinunits]) |
| 237 | + |
| 238 | + |
| 239 | +# -------------------------------- Outputs / results --------------------------------- # |
| 240 | + |
| 241 | +print("Expected result : "+str(puzzle_expected_result)) |
| 242 | +print("Actual result : "+str(puzzle_actual_result)) |