|
| 1 | +# -------------------------------- Input data -------------------------------- # |
| 2 | +importos,pathfinding,complex_utils,copy |
| 3 | + |
| 4 | +test_data= {} |
| 5 | + |
| 6 | +test=1 |
| 7 | +test_data[test]= { |
| 8 | +"input":"""####### |
| 9 | +#G..#E# |
| 10 | +#E#E.E# |
| 11 | +#G.##.# |
| 12 | +#...#E# |
| 13 | +#...E.# |
| 14 | +#######""", |
| 15 | +"expected": ["36334 (37, 982, E)","Unknown"], |
| 16 | +} |
| 17 | + |
| 18 | +test+=1 |
| 19 | +test_data[test]= { |
| 20 | +"input":"""####### |
| 21 | +#E..EG# |
| 22 | +#.#G.E# |
| 23 | +#E.##E# |
| 24 | +#G..#.# |
| 25 | +#..E#.# |
| 26 | +#######""", |
| 27 | +"expected": ["39514 (46 rounds, 859 HP, E)","Unknown"], |
| 28 | +} |
| 29 | + |
| 30 | +test+=1 |
| 31 | +test_data[test]= { |
| 32 | +"input":"""####### |
| 33 | +#E.G#.# |
| 34 | +#.#G..# |
| 35 | +#G.#.G# |
| 36 | +#G..#.# |
| 37 | +#...E.# |
| 38 | +#######""", |
| 39 | +"expected": ["27755 (35 rounds, 793 HP, G)","Unknown"], |
| 40 | +} |
| 41 | + |
| 42 | +test+=1 |
| 43 | +test_data[test]= { |
| 44 | +"input":"""####### |
| 45 | +#.G...# |
| 46 | +#...EG# |
| 47 | +#.#.#G# |
| 48 | +#..G#E# |
| 49 | +#.....# |
| 50 | +#######""", |
| 51 | +"expected": ["Unknown","15 attack power, 4988 (29 rounds, 172 HP)"], |
| 52 | +} |
| 53 | + |
| 54 | +test="real" |
| 55 | +input_file=os.path.join( |
| 56 | +os.path.dirname(__file__), |
| 57 | +"Inputs", |
| 58 | +os.path.basename(__file__).replace(".py",".txt"), |
| 59 | +) |
| 60 | +test_data[test]= { |
| 61 | +"input":open(input_file,"r+").read().strip(), |
| 62 | +"expected": ["207542","64688"], |
| 63 | +} |
| 64 | + |
| 65 | +# -------------------------------- Control program execution ------------------------- # |
| 66 | + |
| 67 | +case_to_test=4 |
| 68 | +part_to_test=2 |
| 69 | +verbose_level=2 |
| 70 | + |
| 71 | +# -------------------------------- Initialize some variables ------------------------- # |
| 72 | + |
| 73 | +puzzle_input=test_data[case_to_test]["input"] |
| 74 | +puzzle_expected_result=test_data[case_to_test]["expected"][part_to_test-1] |
| 75 | +puzzle_actual_result="Unknown" |
| 76 | + |
| 77 | + |
| 78 | +# -------------------------------- Player class definition --------------------------- # |
| 79 | + |
| 80 | + |
| 81 | +classPlayer: |
| 82 | +position=0 |
| 83 | +type="" |
| 84 | +HP=200 |
| 85 | +graph="" |
| 86 | +alive=True |
| 87 | +attack_power=3 |
| 88 | + |
| 89 | +def__init__(self,type,position,attack_power=3): |
| 90 | +self.position=position |
| 91 | +self.type=type |
| 92 | +ifself.type=="E": |
| 93 | +self.attack_power=attack_power |
| 94 | + |
| 95 | +def__lt__(self,other): |
| 96 | +ifself.position.imag<other.position.imag: |
| 97 | +returnTrue |
| 98 | +else: |
| 99 | +returnself.position.real<other.position.real |
| 100 | + |
| 101 | +defmove(self,graph,creatures): |
| 102 | +""" |
| 103 | + Searches for the closest ennemy |
| 104 | +
|
| 105 | + :param Graph graph: The game map |
| 106 | + :param list creatures: A list of creatures |
| 107 | + :return: The target position |
| 108 | + """ |
| 109 | + |
| 110 | +# Modify graph so that allies are walls, ennemies are traps |
| 111 | +self.graph=copy.deepcopy(graph) |
| 112 | +verbose=False |
| 113 | +ifFalse: |
| 114 | +verbose=True |
| 115 | +allies= [ |
| 116 | +c.position |
| 117 | +forcincreatures |
| 118 | +ifc.type==self.typeandc!=selfandc.alive |
| 119 | + ] |
| 120 | +ennemies= [c.positionforcincreaturesifc.type!=self.typeandc.alive] |
| 121 | +self.graph.add_traps(ennemies) |
| 122 | +self.graph.add_walls(allies) |
| 123 | + |
| 124 | +# Run BFS from my position to determine closest target |
| 125 | +self.graph.breadth_first_search(self.position) |
| 126 | + |
| 127 | +# Determine all target positions (= cells next to the ennemies), then choose closest |
| 128 | +target_positions= [ |
| 129 | + (self.graph.distance_from_start[e+dir],e+dir) |
| 130 | +foreinennemies |
| 131 | +fordirincomplex_utils.directions_straight |
| 132 | +ife+dirinself.graph.distance_from_start |
| 133 | + ] |
| 134 | +ifnottarget_positions: |
| 135 | +return |
| 136 | + |
| 137 | +min_distance=min([pos[0]forposintarget_positions]) |
| 138 | +closest_targets= [pos[1]forposintarget_positionsifpos[0]==min_distance] |
| 139 | +target=complex_utils.complex_sort(closest_targets,"reading")[0] |
| 140 | + |
| 141 | +ifmin_distance==0: |
| 142 | +return |
| 143 | + |
| 144 | +ifverbose: |
| 145 | +print("before",self.position,target_positions,closest_targets,target) |
| 146 | + |
| 147 | +# Then we do the opposite, to know in which direction to go |
| 148 | +# Run BFS from the target |
| 149 | +self.graph.breadth_first_search(target) |
| 150 | +# Determine which direction to go to is best |
| 151 | +next_positions= [ |
| 152 | + (self.graph.distance_from_start[self.position+dir],self.position+dir,) |
| 153 | +fordirincomplex_utils.directions_straight |
| 154 | +ifself.position+dirinself.graph.vertices |
| 155 | + ] |
| 156 | +min_distance=min([pos[0]forposinnext_positions]) |
| 157 | +closest_positions= [pos[1]forposinnext_positionsifpos[0]==min_distance] |
| 158 | +target=complex_utils.complex_sort(closest_positions,"reading")[0] |
| 159 | +ifverbose: |
| 160 | +print( |
| 161 | +"after",self.position,next_positions,closest_positions,target,self |
| 162 | + ) |
| 163 | + |
| 164 | +self.position=target |
| 165 | + |
| 166 | +defattack(self,creatures): |
| 167 | +""" |
| 168 | + Attacks an ennemy in range |
| 169 | +
|
| 170 | + :param Graph graph: The game map |
| 171 | + :param list creatures: A list of creatures |
| 172 | + :return: Nothing |
| 173 | + """ |
| 174 | + |
| 175 | +# Find who to attack |
| 176 | +ennemies= [ |
| 177 | +c |
| 178 | +forcincreatures |
| 179 | +fordirincomplex_utils.directions_straight |
| 180 | +ifself.position+dir==c.positionandc.type!=self.typeandc.alive |
| 181 | + ] |
| 182 | +ifnotennemies: |
| 183 | +return |
| 184 | + |
| 185 | +min_HP_ennemies=player_sort( |
| 186 | + [eforeinennemiesife.HP==min([e.HPforeinennemies])] |
| 187 | + ) |
| 188 | +ennemy=player_sort(min_HP_ennemies)[0] |
| 189 | + |
| 190 | +ennemy.lose_HP(self.attack_power) |
| 191 | + |
| 192 | +deflose_HP(self,HP): |
| 193 | +""" |
| 194 | + Loses HP following an attack |
| 195 | +
|
| 196 | + :param int HP: How many HP to lose |
| 197 | + :return: Nothing |
| 198 | + """ |
| 199 | +self.HP-=HP |
| 200 | +self.alive=self.HP>0 |
| 201 | + |
| 202 | + |
| 203 | +defplayer_sort(players): |
| 204 | +players.sort(key=lambdaa: (-a.position.imag,a.position.real)) |
| 205 | +returnplayers |
| 206 | + |
| 207 | + |
| 208 | +# -------------------------------- Actual code execution ----------------------------- # |
| 209 | + |
| 210 | + |
| 211 | +ifpart_to_test==1: |
| 212 | +grid=puzzle_input |
| 213 | + |
| 214 | +# Initial grid with everything |
| 215 | +graph=pathfinding.Graph() |
| 216 | +graph.grid_to_vertices(grid) |
| 217 | + |
| 218 | +# Identify all creatures |
| 219 | +creatures=graph.grid_search(grid, ("E","G")) |
| 220 | + |
| 221 | +creatures= [ |
| 222 | +Player(type,position)fortypeincreaturesforpositionincreatures[type] |
| 223 | + ] |
| 224 | +factions=set(c.typeforcincreatures) |
| 225 | + |
| 226 | +round=0 |
| 227 | +ifverbose_level>=2: |
| 228 | +print("Start") |
| 229 | +print(graph.vertices_to_grid({c.position:c.typeforcincreatures})) |
| 230 | +print([(c.type,c.position,c.HP)forcinplayer_sort(creatures)]) |
| 231 | +whileTrue: |
| 232 | +player_sort(creatures) |
| 233 | +fori,creatureinenumerate(creatures): |
| 234 | +ifnotcreature.alive: |
| 235 | +continue |
| 236 | +creature.move(graph,creatures) |
| 237 | +creature.attack(creatures) |
| 238 | + |
| 239 | +creatures= [cforcincreaturesifc.alive] |
| 240 | +factions=set(c.typeforcincreatures) |
| 241 | +iflen(factions)==1: |
| 242 | +break |
| 243 | + |
| 244 | +round+=1 |
| 245 | +ifverbose_level>=3: |
| 246 | +print("round",round) |
| 247 | +print(graph.vertices_to_grid({c.position:c.typeforcincreatures})) |
| 248 | +print([(c.type,c.position,c.HP,c.alive)forcinplayer_sort(creatures)]) |
| 249 | + |
| 250 | +ifverbose_level>=2: |
| 251 | +print("End of combat") |
| 252 | +print(graph.vertices_to_grid({c.position:c.typeforcincreatures})) |
| 253 | +print([(c.type,c.position,c.HP)forcinplayer_sort(creatures)]) |
| 254 | +print( |
| 255 | +"Reached round:", |
| 256 | +round, |
| 257 | +"- Remaining HP:", |
| 258 | +sum(c.HPforcincreatures), |
| 259 | +"- Winner:", |
| 260 | +factions, |
| 261 | + ) |
| 262 | +puzzle_actual_result=sum(c.HPforcincreaturesifc.alive)*round |
| 263 | + |
| 264 | + |
| 265 | +else: |
| 266 | +grid=puzzle_input |
| 267 | + |
| 268 | +# Initial grid with everything |
| 269 | +graph=pathfinding.Graph() |
| 270 | +graph.grid_to_vertices(grid) |
| 271 | + |
| 272 | +# Identify all creatures |
| 273 | +creatures_positions=graph.grid_search(grid, ("E","G")) |
| 274 | + |
| 275 | +forattackinrange(3,100): |
| 276 | +creatures= [ |
| 277 | +Player(type,position,attack) |
| 278 | +fortypeincreatures_positions |
| 279 | +forpositionincreatures_positions[type] |
| 280 | + ] |
| 281 | +factions=set(c.typeforcincreatures) |
| 282 | +dead_elves=0 |
| 283 | + |
| 284 | +round=0 |
| 285 | +whiledead_elves==0: |
| 286 | +player_sort(creatures) |
| 287 | +fori,creatureinenumerate(creatures): |
| 288 | +ifnotcreature.alive: |
| 289 | +continue |
| 290 | +creature.move(graph,creatures) |
| 291 | +creature.attack(creatures) |
| 292 | + |
| 293 | +dead_elves=len([cforcincreaturesifc.type=="E"andnotc.alive]) |
| 294 | +creatures= [cforcincreaturesifc.alive] |
| 295 | +factions=set(c.typeforcincreatures) |
| 296 | +iflen(factions)==1: |
| 297 | +break |
| 298 | + |
| 299 | +round+=1 |
| 300 | + |
| 301 | +ifverbose_level>=2: |
| 302 | +print("End of combat with attack",attack) |
| 303 | +ifverbose_level>=3: |
| 304 | +print(graph.vertices_to_grid({c.position:c.typeforcincreatures})) |
| 305 | +print( |
| 306 | +"Reached round:", |
| 307 | +round, |
| 308 | +"- Remaining HP:", |
| 309 | +sum(c.HPforcincreatures), |
| 310 | +"- Winner:", |
| 311 | +factions, |
| 312 | + ) |
| 313 | +print("Dead elves:",dead_elves) |
| 314 | + |
| 315 | +iffactions==set("E",): |
| 316 | +puzzle_actual_result=sum(c.HPforcincreaturesifc.alive)*round |
| 317 | +break |
| 318 | + |
| 319 | +# -------------------------------- Outputs / results -------------------------------- # |
| 320 | + |
| 321 | +print("Expected result : "+str(puzzle_expected_result)) |
| 322 | +print("Actual result : "+str(puzzle_actual_result)) |