diff --git a/challenges/2020-spring.py b/challenges/2020-spring.py new file mode 100644 index 0000000..cf09750 --- /dev/null +++ b/challenges/2020-spring.py @@ -0,0 +1,398 @@ +import json +import math +import random + +import sys +from enum import Enum +from typing import List, Set, Iterable, Union, Tuple, Dict + +LOGGING = True +ENEMY_SIGHT_DISTANCE = 3 +DISTANCE_THRESHOLD_FOR_SPEED = 3 +WALL = "#" +PATH = " " +UNKNOWN_PATH = "." + + +class Type(Enum): + ROCK = "ROCK" + PAPER = "PAPER" + SCISSORS = "SCISSORS" + + def __gt__(self, other): + wins_over = { + Type.ROCK: Type.SCISSORS, + Type.SCISSORS: Type.PAPER, + Type.PAPER: Type.ROCK, + } + loser = wins_over[self] + return other is loser + + def get_winner(self): + winner_for = { + Type.SCISSORS: Type.ROCK, + Type.PAPER: Type.SCISSORS, + Type.ROCK: Type.PAPER, + } + return winner_for[self] + + +def log(something, *args, **kwargs): + if LOGGING: + print(something, *args, file=sys.stderr, **kwargs) + + +class Position: + def __init__(self, x, y): + self.x = x + self.y = y + + def distance_to(self, other: "Position") -> int: + return int(math.fabs(self.x - other.x) + math.fabs(self.y - other.y)) + + def __hash__(self): + return hash((self.x, self.y)) + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def __str__(self): + return json.dumps(self.to_dict()) + + def __repr__(self): + return str(self) + + def to_dict(self): + return {"x": self.x, "y": self.y} + + +class Pac(Position): + def __init__(self, line: str): + split = line.split() + self.pac_id = int(split[0]) + self.mine = split[1] != "0" + self.type_id = Type[split[4].upper()] # type: Type + self.speed_turns_left = int(split[5]) + self.ability_cooldown = int(split[6]) + + x = int(split[2]) + y = int(split[3]) + super(Pac, self).__init__(x, y) + + def __hash__(self): + return hash((self.pac_id, self.mine)) + + def __eq__(self, other: "Pac") -> bool: + return ( + isinstance(other, Pac) + and self.pac_id == other.pac_id + and self.mine == other.mine + ) + + def to_dict(self) -> Dict: + dct = super(Pac, self).to_dict() + dct.update({"id": self.pac_id, "mine": self.mine}) + return dct + + def move_to(self, unit: Position) -> str: + return f"MOVE {self.pac_id} {unit.x} {unit.y}" + + def speed(self) -> str: + return f"SPEED {self.pac_id}" + + def switch(self, type: Type, comment: str = "") -> str: + return f"SWITCH {self.pac_id} {type.value} {comment}" + + def target_type(self, other: "Pac") -> Union[Type, None]: + winner_type = other.type_id.get_winner() + if winner_type is self.type_id: + return None + return winner_type + + @property + def can_use_power(self) -> bool: + return self.ability_cooldown <= 1 + + @property + def is_speeding(self) -> bool: + return self.speed_turns_left > 0 + + +class Pellet(Position): + def __init__(self, line: str): + x, y, value = [int(i) for i in line.split()] + self.value = value + super(Pellet, self).__init__(x, y) + + def __hash__(self): + return hash((self.x, self.y, self.value)) + + def __eq__(self, other: "Pellet"): + return ( + isinstance(other, Pellet) + and super(Pellet, self).__eq__(other) + and self.value == other.value + ) + + def to_dict(self): + dct = super().to_dict() + dct.update({"value": self.value}) + return dct + + @property + def is_super(self) -> bool: + return self.value > 1 + + +class Grid: + def __init__(self, line: str, process_rows: bool = True): + # Invariants + # width: size of the grid + # height: top left corner is (x=0, y=0) + width, height = [int(i) for i in line.split()] + self.width = width # type: int + self.height = height # type: int + self.map = [] # type: List[List[str]] + if process_rows: + for i in range(self.height): + self.map.append(list(input().replace(PATH, UNKNOWN_PATH))) + + # Variants + self.pacs = set() # type: Set[Pac] + self.pellets = set() # type: Set[Pellet] + + self.previous_pacs = set() + + @property + def my_pacs(self) -> Iterable[Pac]: + return filter(lambda pac: pac.mine, self.pacs) + + @property + def enemy_pacs(self) -> Iterable[Pac]: + return filter(lambda pac: not pac.mine, self.pacs) + + def pac_at_position(self, x, y, enemy_filter=None) -> Union[Pac, None]: + if enemy_filter is True: + pacs = self.enemy_pacs + elif enemy_filter is False: + pacs = self.my_pacs + else: + pacs = self.pacs + + x = x % self.width + y = y % self.height + for pac in pacs: + if pac.x == x and pac.y == y: + return pac + + def new_turn(self): + self.previous_pacs = self.pacs + self.pacs = set() + self.pellets = set() + + def add_pellet(self, line: str) -> Pellet: + pellet = Pellet(line) + self.set_map_at_coordinates(pellet, str(pellet.value)) + self.pellets.add(pellet) + return pellet + + def add_pac(self, line: str) -> Pac: + pac = Pac(line) + self.pacs.add(pac) + return pac + + def get_random_position(self) -> Position: + return Position( + random.randint(0, self.width - 1), random.randint(0, self.height - 1) + ) + + def get_closest_unknown(self, me: Pac) -> Union[Position, None]: + closest = None + for y, row in enumerate(self.map): + for x, cell in enumerate(row): + if cell != UNKNOWN_PATH: + continue + pos = Position(x, y) + if not closest or me.distance_to(pos) < me.distance_to(closest): + closest = pos + + return closest + + def get_action( + self, me: Pac, already_targeted: Set[Position] + ) -> Tuple[str, Union[Position, None]]: + enemy = self.closest_enemy_in_sight(me) + if me.can_use_power and enemy: + new_type = me.target_type(enemy) + if new_type: + return ( + me.switch(new_type, f"switching to {new_type.value} for {enemy}"), + None, + ) + + if enemy and me.type_id > enemy.type_id: + return me.move_to(enemy), enemy + + target = self.find_target(me, already_targeted) + if me.can_use_power: + return me.speed(), None + + return me.move_to(target), target + + def find_target(self, me: Pac, already_targeted: Set[Position]) -> Position: + rnd = self.get_random_position() + default = self.get_closest_unknown(me) + same_position = False + min_dist = 2 if me.is_speeding else 1 + for pac in self.previous_pacs: + if pac == me and pac.x == me.x and pac.y == me.y: + same_position = True + break + + if same_position: + return default or rnd + + closest_pellet_distance = math.inf + closest_pellet = None + closest_super_pellet_distance = math.inf + closest_super_pellet = None + fallback_pellet = None + for pellet in self.pellets: + if pellet in already_targeted: + continue + distance = me.distance_to(pellet) + if pellet.is_super: + if distance < closest_super_pellet_distance: + closest_super_pellet_distance = distance + closest_super_pellet = pellet + else: + if min_dist <= distance < closest_pellet_distance: + closest_pellet_distance = distance + closest_pellet = pellet + elif distance < closest_pellet_distance: + fallback_pellet = pellet + + if ( + fallback_pellet + and closest_pellet + and math.fabs( + me.distance_to(fallback_pellet) - me.distance_to(closest_pellet) + ) + > 3 + ): + closest_pellet = fallback_pellet + + log(me.pac_id, closest_super_pellet, closest_pellet, default) + return ( + closest_super_pellet or closest_pellet or fallback_pellet or default or rnd + ) + + def get_map_at_coordinates(self, x: int, y: int) -> str: + x = x % self.width + y = y % self.height + return self.map[y][x] + + def set_map_at_coordinates(self, position: Position, value: str): + x = position.x % self.width + y = position.y % self.height + self.map[y][x] = value + + def closest_enemy_in_sight(self, me: Pac) -> Union[Pac, None]: + try_right, try_left, try_up, try_down = True, True, True, True + closest_enemy = None + self.set_map_at_coordinates(me, PATH) + for i in range(1, max(self.height, self.width)): + if try_right: + x = me.x + i + y = me.y + cell = self.get_map_at_coordinates(x, y) + if cell == WALL: + try_right = False + elif cell == UNKNOWN_PATH: + self.set_map_at_coordinates(Position(x, y), PATH) + pac_at_position = self.pac_at_position(x, y, enemy_filter=True) + if ( + pac_at_position + and me.distance_to(pac_at_position) <= ENEMY_SIGHT_DISTANCE + and not closest_enemy + ): + closest_enemy = pac_at_position + if try_left: + x = me.x - i + y = me.y + cell = self.get_map_at_coordinates(x, y) + if cell == WALL: + try_left = False + elif cell == UNKNOWN_PATH: + self.set_map_at_coordinates(Position(x, y), PATH) + pac_at_position = self.pac_at_position(x, y, enemy_filter=True) + + if ( + pac_at_position + and me.distance_to(pac_at_position) <= ENEMY_SIGHT_DISTANCE + and not closest_enemy + ): + closest_enemy = pac_at_position + if try_up: + x = me.x + y = me.y - i + cell = self.get_map_at_coordinates(x, y) + if cell == WALL: + try_up = False + elif cell == UNKNOWN_PATH: + self.set_map_at_coordinates(Position(x, y), PATH) + pac_at_position = self.pac_at_position(x, y, enemy_filter=True) + + if ( + pac_at_position + and me.distance_to(pac_at_position) <= ENEMY_SIGHT_DISTANCE + and not closest_enemy + ): + closest_enemy = pac_at_position + if try_down: + x = me.x + y = me.y + i + cell = self.get_map_at_coordinates(x, y) + if cell == WALL: + try_down = False + elif cell == UNKNOWN_PATH: + self.set_map_at_coordinates(Position(x, y), PATH) + pac_at_position = self.pac_at_position(x, y, enemy_filter=True) + + if ( + pac_at_position + and me.distance_to(pac_at_position) <= ENEMY_SIGHT_DISTANCE + and not closest_enemy + ): + closest_enemy = pac_at_position + return closest_enemy + + +def main(): + grid = Grid(input()) + + # game loop + while True: + grid.new_turn() + my_score, opponent_score = [int(i) for i in input().split()] # type: int, int + + visible_pac_count = int(input()) # type: int + for i in range(visible_pac_count): + grid.add_pac(input()) + + visible_pellet_count = int(input()) # type: int + for _ in range(visible_pellet_count): + grid.add_pellet(input()) + + output = [] + targets = set() + for pac in grid.my_pacs: + action, target = grid.get_action(pac, targets) + output.append(action) + targets.add(target) + + print(" | ".join(output)) + + +if __name__ == "__main__": + main()