|
|
@@ -0,0 +1,1017 @@
|
|
|
+import sys
|
|
|
+import time
|
|
|
+import heapq
|
|
|
+import math
|
|
|
+
|
|
|
+debug = True
|
|
|
+
|
|
|
+t0 = time.time()
|
|
|
+
|
|
|
+
|
|
|
+# todo: attacking strategy: place one guy nearer to opponent hq
|
|
|
+# todo: considerate shielding monsters threatening opponent hq
|
|
|
+# todo: review the order heroes are playing in each strategy
|
|
|
+
|
|
|
+
|
|
|
+def log(*msg):
|
|
|
+ if debug:
|
|
|
+ print("{} - ".format(str(time.time() - t0)[1:5]), *msg, file=sys.stderr, flush=True)
|
|
|
+
|
|
|
+
|
|
|
+def time_to(total, step):
|
|
|
+ """ number of steps to reach total """
|
|
|
+ return total // step + (1 if total % step > 0 else 0)
|
|
|
+
|
|
|
+
|
|
|
+class BaseClass:
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<{self.__class__.__name__}: {self.__dict__}>"
|
|
|
+
|
|
|
+
|
|
|
+class Queue(BaseClass):
|
|
|
+ def __init__(self):
|
|
|
+ self.items = []
|
|
|
+
|
|
|
+ def __bool__(self):
|
|
|
+ return bool(self.items)
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return str(self.items)
|
|
|
+
|
|
|
+ def values(self):
|
|
|
+ return (v for _, v in self.items)
|
|
|
+
|
|
|
+ def put(self, priority, item):
|
|
|
+ while priority in [p for p, _ in self.items]:
|
|
|
+ priority += 1
|
|
|
+ heapq.heappush(self.items, (priority, item))
|
|
|
+
|
|
|
+ def get(self):
|
|
|
+ return heapq.heappop(self.items)[1]
|
|
|
+
|
|
|
+ def get_items(self):
|
|
|
+ return heapq.heappop(self.items)
|
|
|
+
|
|
|
+
|
|
|
+class GridItem(BaseClass):
|
|
|
+ def __init__(self, pos=(-1, -1)):
|
|
|
+ self._previous_pos = None
|
|
|
+ self._pos = pos
|
|
|
+
|
|
|
+ @property
|
|
|
+ def x(self):
|
|
|
+ return self.pos[0]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def y(self):
|
|
|
+ return self.pos[1]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def pos(self):
|
|
|
+ return self._pos
|
|
|
+
|
|
|
+ @pos.setter
|
|
|
+ def pos(self, pos):
|
|
|
+ self._previous_pos = self.pos
|
|
|
+ self._pos = pos
|
|
|
+
|
|
|
+ @property
|
|
|
+ def previous_pos(self):
|
|
|
+ return self._previous_pos
|
|
|
+
|
|
|
+
|
|
|
+class HeadQuarter(GridItem):
|
|
|
+ SIGHT = 6000
|
|
|
+
|
|
|
+ def __init__(self, pos):
|
|
|
+ super().__init__(pos)
|
|
|
+ self.health = 3
|
|
|
+ self.mana = 10
|
|
|
+
|
|
|
+
|
|
|
+class Role:
|
|
|
+ NAME = "None"
|
|
|
+ POSITION = (1500, 1500)
|
|
|
+ ALT_POSITIONS = []
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return self.__class__.__name__
|
|
|
+
|
|
|
+
|
|
|
+class Defender(Role):
|
|
|
+ NAME = "Defender"
|
|
|
+ POSITION = (1250, 1250)
|
|
|
+
|
|
|
+
|
|
|
+class DefenderRight(Defender):
|
|
|
+ NAME = "Defender (right)"
|
|
|
+ POSITION = (500, 3000)
|
|
|
+
|
|
|
+
|
|
|
+class DefenderLeft(Defender):
|
|
|
+ NAME = "Defender (left)"
|
|
|
+ POSITION = (3000, 500)
|
|
|
+
|
|
|
+
|
|
|
+class GateKeeper(Role):
|
|
|
+ NAME = "Gatekeeper"
|
|
|
+ POSITION = (4500, 3000)
|
|
|
+
|
|
|
+
|
|
|
+class GateKeeperRight(GateKeeper):
|
|
|
+ NAME = "Gatekeeper (right)"
|
|
|
+ POSITION = (1500, 5000)
|
|
|
+
|
|
|
+
|
|
|
+class GateKeeperLeft(GateKeeper):
|
|
|
+ NAME = "Gatekeeper (left)"
|
|
|
+ POSITION = (5500, 1000)
|
|
|
+
|
|
|
+
|
|
|
+class Hunter(Role):
|
|
|
+ NAME = "Hunter"
|
|
|
+ POSITION = (6000, 4000)
|
|
|
+ ALT_POSITIONS = [(3500, 6500), (8000, 1500)]
|
|
|
+
|
|
|
+
|
|
|
+class HunterRight(Hunter):
|
|
|
+ NAME = "Hunter (right)"
|
|
|
+ POSITION = (3500, 6500)
|
|
|
+ ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
|
|
|
+
|
|
|
+
|
|
|
+class HunterLeft(Hunter):
|
|
|
+ NAME = "Hunter (left)"
|
|
|
+ POSITION = (8000, 1500)
|
|
|
+ ALT_POSITIONS = [(9000, 500), (6000, 4000)]
|
|
|
+
|
|
|
+
|
|
|
+class Attacker(Role):
|
|
|
+ NAME = "Attacker"
|
|
|
+ POSITION = (7500, 4000)
|
|
|
+ ALT_POSITIONS = [(5000, 6500), (8000, 2000)]
|
|
|
+
|
|
|
+
|
|
|
+class AttackerRight(Attacker):
|
|
|
+ NAME = "Attacker (right)"
|
|
|
+ POSITION = (5000, 6500)
|
|
|
+ ALT_POSITIONS = [(1500, 7500), (6000, 4000)]
|
|
|
+
|
|
|
+
|
|
|
+class AttackerLeft(Attacker):
|
|
|
+ NAME = "Attacker (left)"
|
|
|
+ POSITION = (8000, 2000)
|
|
|
+ ALT_POSITIONS = [(9000, 500), (6000, 4000)]
|
|
|
+
|
|
|
+
|
|
|
+class Saboteur(Role):
|
|
|
+ NAME = "Saboteur"
|
|
|
+ POSITION = (15000, 6000)
|
|
|
+ ALT_POSITIONS = [(12000, 3500), (10000, 6000)]
|
|
|
+
|
|
|
+
|
|
|
+class Strategy(BaseClass):
|
|
|
+ def __init__(self, roles, default=Defender):
|
|
|
+ self.roles = roles
|
|
|
+ self.pos = 0
|
|
|
+ self.default = default
|
|
|
+
|
|
|
+ def pick(self):
|
|
|
+ """ Get the next role in line for the hero, or defender by default """
|
|
|
+ try:
|
|
|
+ role = self.roles[self.pos]
|
|
|
+ self.pos += 1
|
|
|
+ return role
|
|
|
+ except IndexError:
|
|
|
+ return self.default
|
|
|
+
|
|
|
+
|
|
|
+class StrategyFullDefense(Strategy):
|
|
|
+ NAME = "Full Defense"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([Defender, DefenderLeft, DefenderRight])
|
|
|
+
|
|
|
+
|
|
|
+class StrategyDefensive(Strategy):
|
|
|
+ NAME = "Defensive"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([Attacker, GateKeeper, Defender])
|
|
|
+
|
|
|
+
|
|
|
+class StrategyBalanced(Strategy):
|
|
|
+ NAME = "Balanced"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([Attacker, Hunter, Defender])
|
|
|
+
|
|
|
+
|
|
|
+class StrategyPrudentFarm(Strategy):
|
|
|
+ NAME = "Farm (prudent)"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([HunterLeft, HunterRight, GateKeeper])
|
|
|
+
|
|
|
+
|
|
|
+class StrategyFarm(Strategy):
|
|
|
+ NAME = "Farm"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([Attacker, HunterLeft, HunterRight])
|
|
|
+
|
|
|
+
|
|
|
+class StrategyAggressive(Strategy):
|
|
|
+ NAME = "Aggressive"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__([Saboteur, Attacker, GateKeeper])
|
|
|
+
|
|
|
+
|
|
|
+class Action(BaseClass):
|
|
|
+ def __init__(self, priority=1000):
|
|
|
+ self.priority = priority
|
|
|
+ self.feasibility = 0
|
|
|
+ self.done = False
|
|
|
+ self._pos = None
|
|
|
+
|
|
|
+ @property
|
|
|
+ def pos(self):
|
|
|
+ return self._pos
|
|
|
+
|
|
|
+ def do(self):
|
|
|
+ self.done = True
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"{self.__class__.__name__} [{self.priority}/{self.feasibility}]"
|
|
|
+
|
|
|
+
|
|
|
+class Move(Action):
|
|
|
+ def __init__(self, pos, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ self._pos = pos
|
|
|
+
|
|
|
+ def do(self):
|
|
|
+ super().do()
|
|
|
+ print(f"MOVE {self.pos[0]} {self.pos[1]}")
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"{self.__class__.__name__} {self.pos} [{self.priority}/{self.feasibility}]"
|
|
|
+
|
|
|
+
|
|
|
+class Explore(Move):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class MoveAt(Action):
|
|
|
+ def __init__(self, target, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ self.target = target
|
|
|
+
|
|
|
+ @property
|
|
|
+ def pos(self):
|
|
|
+ return self.target.pos
|
|
|
+
|
|
|
+ def do(self):
|
|
|
+ super().do()
|
|
|
+ print(f"MOVE {self.pos[0]} {self.pos[1]}")
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"{self.__class__.__name__} {self.target.id} [{self.priority}/{self.feasibility}]"
|
|
|
+
|
|
|
+
|
|
|
+class Defend(MoveAt):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class Hunt(MoveAt):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class Reposition(Move):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class Cast(Action):
|
|
|
+ SPELL = ""
|
|
|
+ MANA_COST = 10
|
|
|
+
|
|
|
+ def __init__(self, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ self.params = []
|
|
|
+
|
|
|
+ def do(self):
|
|
|
+ super().do()
|
|
|
+ print(f"SPELL {self.SPELL} {' '.join(map(str, self.params))}")
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"{self.__class__.__name__} {' '.join(map(str, self.params))} [{self.priority}/{self.feasibility}]"
|
|
|
+
|
|
|
+
|
|
|
+class CastWind(Cast):
|
|
|
+ SPELL = "WIND"
|
|
|
+ RADIUS = 1280
|
|
|
+ DEPTH = 2200
|
|
|
+
|
|
|
+ def __init__(self, pos, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ self.params = pos
|
|
|
+
|
|
|
+
|
|
|
+class CastControl(Cast):
|
|
|
+ SPELL = "CONTROL"
|
|
|
+ RANGE = 2200
|
|
|
+
|
|
|
+ def __init__(self, target_id, pos, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ x, y = pos
|
|
|
+ self.params = [target_id, x, y]
|
|
|
+
|
|
|
+
|
|
|
+class CastShield(Cast):
|
|
|
+ SPELL = "SHIELD"
|
|
|
+ RANGE = 2200
|
|
|
+ DURATION = 12
|
|
|
+
|
|
|
+ def __init__(self, target_id, priority=1000):
|
|
|
+ super().__init__(priority)
|
|
|
+ self.params = [target_id]
|
|
|
+
|
|
|
+
|
|
|
+class Wait(Action):
|
|
|
+ def do(self):
|
|
|
+ print(f"WAIT")
|
|
|
+
|
|
|
+
|
|
|
+class Entity(GridItem):
|
|
|
+ TYPE_MONSTER = 0
|
|
|
+ TYPE_HERO = 1
|
|
|
+ TYPE_OPPONENT_HERO = 2
|
|
|
+
|
|
|
+ def __init__(self, id_, pos=(-1, -1)):
|
|
|
+ super().__init__(pos)
|
|
|
+ self.id = id_ # Unique identifier
|
|
|
+ self.shield_life = -1 # Count down until shield spell fades
|
|
|
+ self.health = -1 # Remaining health of this monster
|
|
|
+ self.is_controlled = False
|
|
|
+
|
|
|
+ # Computed properties
|
|
|
+ self.visible = False
|
|
|
+ self.distance_to_hq = None
|
|
|
+ self.moving_time_to_hq = None
|
|
|
+ self.distance_to_opponent_hq = None
|
|
|
+ self.moving_time_to_opponent_hq = None
|
|
|
+ self.distance_from_entity = {}
|
|
|
+ self.moving_time_from_entity = {}
|
|
|
+ self.go_toward_hq = False
|
|
|
+ self.go_toward_opponent_hq = False
|
|
|
+ self.is_on_my_side = False
|
|
|
+ self.is_on_opponent_side = False
|
|
|
+ self.camper = False
|
|
|
+
|
|
|
+ def init_round(self):
|
|
|
+ self.visible = False
|
|
|
+ self.distance_to_hq = None
|
|
|
+ self.moving_time_to_hq = None
|
|
|
+ self.distance_to_opponent_hq = None
|
|
|
+ self.moving_time_to_opponent_hq = None
|
|
|
+ self.distance_from_entity = {}
|
|
|
+ self.moving_time_from_entity = {}
|
|
|
+ self.go_toward_hq = False
|
|
|
+ self.go_toward_opponent_hq = False
|
|
|
+ self.is_on_my_side = False
|
|
|
+ self.is_on_opponent_side = False
|
|
|
+ self.camper = False
|
|
|
+
|
|
|
+
|
|
|
+class Hero(Entity):
|
|
|
+ ATTACK = 2
|
|
|
+ ATTACK_RANGE = 800
|
|
|
+ SPEED = 800
|
|
|
+ SIGHT = 2200
|
|
|
+
|
|
|
+ def __init__(self, id_):
|
|
|
+ super().__init__(id_)
|
|
|
+ self.role = Role
|
|
|
+ self.current_role_pos = None
|
|
|
+
|
|
|
+
|
|
|
+class OpponentHero(Hero):
|
|
|
+ def __init__(self, id_):
|
|
|
+ super().__init__(id_)
|
|
|
+
|
|
|
+
|
|
|
+class Monster(Entity):
|
|
|
+ THREAT_FOR_ME = 1
|
|
|
+ THREAT_FOR_OPPONENT = 2
|
|
|
+ NO_THREAT = 0
|
|
|
+ SPEED = 400
|
|
|
+ THREAT_RANGE = 5000
|
|
|
+ ATTACK_RANGE = 300
|
|
|
+
|
|
|
+ def __init__(self, id_):
|
|
|
+ super().__init__(id_)
|
|
|
+ self.near_base = False
|
|
|
+ self.threat_for = self.NO_THREAT
|
|
|
+ self.vx = 0 # Trajectory of this monster
|
|
|
+ self.vy = 0
|
|
|
+
|
|
|
+ @property
|
|
|
+ def next_pos(self):
|
|
|
+ return self.x + self.vx, self.y + self.vy
|
|
|
+
|
|
|
+ @property
|
|
|
+ def threats_hq(self):
|
|
|
+ return self.threat_for == self.THREAT_FOR_ME
|
|
|
+
|
|
|
+ @property
|
|
|
+ def threats_opponent_hq(self):
|
|
|
+ return self.threat_for == self.THREAT_FOR_OPPONENT
|
|
|
+
|
|
|
+ @property
|
|
|
+ def alive(self):
|
|
|
+ return self.health > 0
|
|
|
+
|
|
|
+
|
|
|
+class Grid(BaseClass):
|
|
|
+ MAX_X = 17630
|
|
|
+ MAX_Y = 9000
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.threat_level = 0
|
|
|
+ self.round = 0
|
|
|
+ self._index = {}
|
|
|
+ self.hq = None
|
|
|
+ self.opponent_hq = None
|
|
|
+ self.strategy = None
|
|
|
+
|
|
|
+ @property
|
|
|
+ def index(self):
|
|
|
+ return self._index
|
|
|
+
|
|
|
+ def get(self, _id):
|
|
|
+ return self._index[_id]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def entities(self):
|
|
|
+ return [e for e in self._index.values() if e.visible]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def heroes(self):
|
|
|
+ return sorted([v for v in self.entities if type(v) is Hero], key=lambda x: x.id)
|
|
|
+
|
|
|
+ @property
|
|
|
+ def monsters(self):
|
|
|
+ return [v for v in self.entities if type(v) is Monster]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def opponent_heroes(self):
|
|
|
+ return [v for v in self.entities if type(v) is OpponentHero]
|
|
|
+
|
|
|
+ def init_game(self):
|
|
|
+ self.hq = HeadQuarter([int(i) for i in input().split()])
|
|
|
+ self.opponent_hq = HeadQuarter((Grid.MAX_X, Grid.MAX_Y) if self.hq.x == 0 else (0, 0))
|
|
|
+ _ = int(input()) # Heroes per player, always 3
|
|
|
+
|
|
|
+ def update_entity(self, entity):
|
|
|
+ entity.distance_to_hq = self.distance(self.hq.pos, entity.pos)
|
|
|
+ entity.moving_time_to_hq = self.moving_time_to(entity, self.hq.pos)
|
|
|
+ entity.distance_to_opponent_hq = self.distance(self.opponent_hq.pos, entity.pos)
|
|
|
+ entity.moving_time_to_opponent_hq = self.moving_time_to(entity, self.opponent_hq.pos)
|
|
|
+ entity.distance_from_entity = {e.id: self.distance(e.pos, entity.pos) for e in self.entities if
|
|
|
+ e.id != entity.id}
|
|
|
+ entity.moving_time_from_entity = {e.id: self.moving_time_to(entity, e.pos) for e in self.entities if
|
|
|
+ e.id != entity.id}
|
|
|
+ if type(entity) is Monster:
|
|
|
+ entity.go_toward_hq = (entity.vx <= 0 and entity.vy <= 0 and self.hq.x == 0 or
|
|
|
+ entity.vx >= 0 and entity.vy >= 0 and self.hq.x != 0)
|
|
|
+ entity.go_toward_opponent_hq = (entity.vx >= 0 and entity.vy >= 0 and self.opponent_hq.x != 0 or
|
|
|
+ entity.vx <= 0 and entity.vy <= 0 and self.opponent_hq.x == 0)
|
|
|
+
|
|
|
+ entity.is_on_my_side = entity.distance_to_hq < entity.distance_to_opponent_hq
|
|
|
+ entity.is_on_opponent_side = not entity.is_on_my_side
|
|
|
+
|
|
|
+ if type(entity) is OpponentHero:
|
|
|
+ if entity.previous_pos and entity.pos == entity.previous_pos and entity.is_on_my_side:
|
|
|
+ entity.camper = True
|
|
|
+
|
|
|
+ def init_round(self):
|
|
|
+ """ get inputs and reinit the state of entities """
|
|
|
+ self.round += 1
|
|
|
+ log(f"Round {self.round}")
|
|
|
+
|
|
|
+ # *** Update HQ status
|
|
|
+ self.hq.health, self.hq.mana = [int(j) for j in input().split()]
|
|
|
+ self.opponent_hq.health, self.opponent_hq.mana = [int(j) for j in input().split()]
|
|
|
+
|
|
|
+ # *** Reinit the entities state
|
|
|
+ for entity in self.entities:
|
|
|
+ entity.init_round()
|
|
|
+
|
|
|
+ # *** Update entities
|
|
|
+ entity_count = int(input()) # Amount of heroes and monsters you can see
|
|
|
+ for i in range(entity_count):
|
|
|
+
|
|
|
+ _id, _type, _x, _y, shield_life, is_controlled, health, vx, vy, near_base, threat_for = [int(j) for j in
|
|
|
+ input().split()]
|
|
|
+ # Entity is not indexed already
|
|
|
+ if _id not in self.index:
|
|
|
+ if _type == Entity.TYPE_MONSTER:
|
|
|
+ entity = Monster(_id)
|
|
|
+
|
|
|
+ elif _type == Entity.TYPE_HERO:
|
|
|
+ entity = Hero(_id)
|
|
|
+
|
|
|
+ elif _type == Entity.TYPE_OPPONENT_HERO:
|
|
|
+ entity = OpponentHero(_id)
|
|
|
+
|
|
|
+ else:
|
|
|
+ log("Error: unknown type ({_type})")
|
|
|
+ break
|
|
|
+ self.index[_id] = entity
|
|
|
+
|
|
|
+ # Update entity
|
|
|
+ entity = self.get(_id)
|
|
|
+ entity.pos = (_x, _y)
|
|
|
+ entity.shield_life = shield_life
|
|
|
+ entity.is_controlled = is_controlled
|
|
|
+ entity.health = health
|
|
|
+ entity.visible = True
|
|
|
+
|
|
|
+ if type(entity) is Monster:
|
|
|
+ entity.near_base = bool(near_base)
|
|
|
+ entity.threat_for = threat_for
|
|
|
+ entity.vx = vx
|
|
|
+ entity.vy = vy
|
|
|
+
|
|
|
+ # purge index
|
|
|
+ self.purge()
|
|
|
+
|
|
|
+ # update entities
|
|
|
+ for entity in self.entities:
|
|
|
+ self.update_entity(entity)
|
|
|
+
|
|
|
+ # *** define strategy and roles
|
|
|
+ self.threat_level = self.compute_threat_level()
|
|
|
+
|
|
|
+ self.strategy = self.define_strategy()
|
|
|
+ for hero in self.heroes:
|
|
|
+ role = self.strategy.pick()
|
|
|
+ if hero.role != role:
|
|
|
+ hero.current_role_pos = None
|
|
|
+ elif hero.pos in self.get_role_positions(hero.role):
|
|
|
+ # remember as the last occupied role position
|
|
|
+ hero.current_role_pos = hero.pos
|
|
|
+ hero.role = role
|
|
|
+
|
|
|
+ log(f'## Threat: {self.threat_level} - Strategy: {self.strategy.NAME} ##')
|
|
|
+ log("Heroes: {}".format(','.join(
|
|
|
+ [f"<{h.id},P:{h.pos},M:{h.moving_time_to_hq},O:{int(h.is_on_opponent_side)}>" for h in self.heroes])))
|
|
|
+ log("Monsters: {}".format(','.join(
|
|
|
+ [f"<{m.id},P:{m.pos},T:{int(m.threats_hq)},V:{int(m.visible)},M:{m.moving_time_to_hq},"
|
|
|
+ f"N:{int(m.go_toward_hq)},O:{int(m.go_toward_opponent_hq)},S:{int(m.shield_life > 0)}>" for m
|
|
|
+ in self.monsters])))
|
|
|
+ log("Opponents: {}".format(','.join(
|
|
|
+ [f"<{o.id},P:{o.pos},S:{o.shield_life},M:{o.moving_time_to_hq},C:{int(o.camper)}>" for o in self.opponent_heroes])))
|
|
|
+
|
|
|
+ def purge(self):
|
|
|
+ """ remove dead entities from the index """
|
|
|
+ self._index = {k: e for k, e in self._index.items() if type}
|
|
|
+
|
|
|
+ index = {}
|
|
|
+ for entity in self.entities:
|
|
|
+ if isinstance(entity, Monster):
|
|
|
+ if self.visible(entity.next_pos) and not entity.visible:
|
|
|
+ # entity should be visible, but has not been seen, presumed dead
|
|
|
+ continue
|
|
|
+
|
|
|
+ index[entity.id] = entity
|
|
|
+
|
|
|
+ self._index = index
|
|
|
+
|
|
|
+ def compute_threat_level(self):
|
|
|
+ level = 0
|
|
|
+ for monster in self.monsters:
|
|
|
+ if monster.threats_hq:
|
|
|
+ level += (monster.health + monster.shield_life) * max(24 - monster.moving_time_to_hq, 0)
|
|
|
+ for enemy in self.opponent_heroes:
|
|
|
+ if enemy.is_on_my_side:
|
|
|
+ level += 50
|
|
|
+
|
|
|
+ return level
|
|
|
+
|
|
|
+ def define_strategy(self):
|
|
|
+ # *** Define strategy
|
|
|
+
|
|
|
+ if self.threat_level > 1400:
|
|
|
+ return StrategyFullDefense()
|
|
|
+ if self.threat_level > 1000:
|
|
|
+ return StrategyDefensive()
|
|
|
+ elif self.threat_level > 800:
|
|
|
+ return StrategyBalanced()
|
|
|
+
|
|
|
+ if self.round < 40:
|
|
|
+ if self.threat_level:
|
|
|
+ return StrategyPrudentFarm()
|
|
|
+ else:
|
|
|
+ return StrategyFarm()
|
|
|
+ elif self.round < 100:
|
|
|
+ return StrategyBalanced()
|
|
|
+ else:
|
|
|
+ return StrategyAggressive()
|
|
|
+
|
|
|
+ def get_action_for(self, hero):
|
|
|
+ actions = []
|
|
|
+
|
|
|
+ k0_defend = 10
|
|
|
+ k0_reposition = 250
|
|
|
+ k0_hunt = 200
|
|
|
+ k0_cast_wind = 150
|
|
|
+ k0_cast_offensive_wind = 220
|
|
|
+ k0_cast_control = 100
|
|
|
+ k0_cast_control_monster = 100
|
|
|
+ k0_cast_offensive_control = 200
|
|
|
+ k0_cast_shield = 150
|
|
|
+ k0_cast_shield_on_monster = 80
|
|
|
+
|
|
|
+ k_attack_time = 5
|
|
|
+ k_hero_nearer = 30
|
|
|
+ k_defend_against_shielded = -20
|
|
|
+ k_defender_priority_bonus = -50
|
|
|
+ k_go_toward_hq = -30
|
|
|
+ k_hunt_distance = 5
|
|
|
+ k_hunt_pv = 2
|
|
|
+ k_position_distance_from_pos = -8
|
|
|
+ k_cast_wind_threats = -1
|
|
|
+ k_cast_wind_enemy = -20
|
|
|
+ k_cast_wind_enemy_on_my_side = -10
|
|
|
+ k_cast_wind_enemy_near_hq = -5
|
|
|
+ k_cast_wind_proximity = 5
|
|
|
+ k_cast_wind_bad_target = 10
|
|
|
+ k_cast_wind_last_defense = -30
|
|
|
+ k_cast_wind_last_defense_limit = 10
|
|
|
+ k_control_health = -2
|
|
|
+ k_control_time = 5
|
|
|
+ k_control_going_toward = -20
|
|
|
+ k_shield_enemy_near = -20
|
|
|
+ k_shield_monster = -1
|
|
|
+ k_shield_monster_distance = 3
|
|
|
+ k_cast_shield_distance = 10
|
|
|
+ k_camper_bonus = -50
|
|
|
+ k_cast_control_on_opponent_defender = 10
|
|
|
+ k_cast_control_on_opponent_defender_threat = -1
|
|
|
+
|
|
|
+ defend_threshold = 18
|
|
|
+ min_control_hp = 15
|
|
|
+ min_shield_distance = max(CastWind.RADIUS, CastControl.RANGE)
|
|
|
+ min_shield_threat = 60
|
|
|
+ offensive_buff = -1
|
|
|
+ offensive_buff_control = -2
|
|
|
+
|
|
|
+ k_feasibility_moving_time = 10
|
|
|
+ k_feasibility_casting_cost = 5
|
|
|
+ k_feasibility_far_from_pos = 5
|
|
|
+ k_feasibility_defender_far_from_pos = 10
|
|
|
+
|
|
|
+ min_mana_priority = 240
|
|
|
+
|
|
|
+ # *** Defend
|
|
|
+ for monster in self.monsters:
|
|
|
+ if monster.threats_hq and monster.moving_time_to_hq < defend_threshold:
|
|
|
+ priority = k0_defend
|
|
|
+
|
|
|
+ # Réduire la priorité en fonction de la distance
|
|
|
+ priority += (k_attack_time * monster.moving_time_to_hq)
|
|
|
+ # TODO: réduire priority pour chaque monstre suppl que l'attaque pourrait toucher
|
|
|
+
|
|
|
+ priority += k_defend_against_shielded * int(monster.shield_life > 0)
|
|
|
+
|
|
|
+ # Réduire la priorité pour chaque héros plus proche du qg
|
|
|
+ for other_hero in [h for h in self.heroes if h is not hero]:
|
|
|
+ if other_hero.moving_time_to_hq < monster.moving_time_to_hq:
|
|
|
+ priority += k_hero_nearer
|
|
|
+
|
|
|
+ if isinstance(hero, (Defender, GateKeeper)):
|
|
|
+ priority += k_defender_priority_bonus
|
|
|
+
|
|
|
+ action = Defend(monster, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Position
|
|
|
+ role_pos = self.get_role_position(hero)
|
|
|
+ if hero.pos != role_pos:
|
|
|
+ priority = k0_reposition
|
|
|
+ priority += k_position_distance_from_pos * self.moving_time_to(hero, role_pos)
|
|
|
+ action = Reposition(role_pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Hunt
|
|
|
+ if hero.is_on_my_side and \
|
|
|
+ not issubclass(hero.role, (Defender, GateKeeper)) and \
|
|
|
+ not (issubclass(hero.role, Saboteur) and hero.is_on_my_side):
|
|
|
+ for monster in self.monsters:
|
|
|
+ if monster.threats_opponent_hq:
|
|
|
+ # it's defense domain or not worth it
|
|
|
+ continue
|
|
|
+ priority = k0_hunt
|
|
|
+ priority += k_hunt_distance * hero.moving_time_from_entity[monster.id]
|
|
|
+ priority += k_go_toward_hq * monster.go_toward_hq
|
|
|
+ priority += k_hunt_pv * monster.health
|
|
|
+ action = Hunt(monster, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Cast WIND on threats
|
|
|
+ if self.hq.mana >= CastWind.MANA_COST:
|
|
|
+ affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
|
|
|
+ if affected:
|
|
|
+ priority = k0_cast_wind
|
|
|
+
|
|
|
+ # Affected entities
|
|
|
+ priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_hq])
|
|
|
+ priority += k_cast_wind_enemy * len([m for m in affected if type(m) is OpponentHero])
|
|
|
+ priority += k_cast_wind_enemy_on_my_side * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side])
|
|
|
+ priority += k_cast_wind_enemy_near_hq * sum([m.moving_time_to_hq for m in affected if type(m) is OpponentHero if m.distance_to_hq < HeadQuarter.SIGHT])
|
|
|
+ priority += k_camper_bonus * len([m for m in affected if type(m) is OpponentHero and m.is_on_my_side and m.camper])
|
|
|
+
|
|
|
+ # Hq average proximity
|
|
|
+ priority += k_cast_wind_proximity * hero.moving_time_to_hq
|
|
|
+
|
|
|
+ # last defence
|
|
|
+ priority += sum([k_cast_wind_last_defense * (k_cast_wind_last_defense_limit - m.moving_time_to_hq) for m in affected if type(m) is Monster and m.moving_time_to_hq < k_cast_wind_last_defense_limit])
|
|
|
+
|
|
|
+ action = CastWind(self.opponent_hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Cast CONTROL on approaching monsters
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
|
|
|
+ possible_targets = [m for m in self.monsters if
|
|
|
+ m.distance_from_entity[hero.id] < CastControl.RANGE and not m.shield_life]
|
|
|
+ for monster in possible_targets:
|
|
|
+ if monster.health < min_control_hp:
|
|
|
+ # too weak
|
|
|
+ continue
|
|
|
+
|
|
|
+ if monster.go_toward_opponent_hq:
|
|
|
+ # already targeting enemy
|
|
|
+ continue
|
|
|
+
|
|
|
+ if monster.distance_to_hq <= (monster.THREAT_RANGE + monster.SPEED):
|
|
|
+ # too late...
|
|
|
+ continue
|
|
|
+
|
|
|
+ if monster.is_controlled:
|
|
|
+ # already controlled
|
|
|
+ continue
|
|
|
+
|
|
|
+ priority = k0_cast_control
|
|
|
+ priority += k_control_health * monster.health
|
|
|
+ priority += k_control_time * min(monster.moving_time_to_opponent_hq, monster.moving_time_to_hq)
|
|
|
+ priority += k_control_going_toward * monster.go_toward_hq
|
|
|
+ action = CastControl(monster.id, self.opponent_hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Cast CONTROL on campers
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST):
|
|
|
+ possible_targets = [e for e in self.opponent_heroes if
|
|
|
+ e.distance_from_entity[hero.id] < CastControl.RANGE
|
|
|
+ and not e.shield_life and e.camper]
|
|
|
+ for enemy in possible_targets:
|
|
|
+ if enemy.is_controlled:
|
|
|
+ # already controlled
|
|
|
+ continue
|
|
|
+ priority = k0_cast_control
|
|
|
+ action = CastControl(enemy.id, self.opponent_hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # *** Cast SHIELD on self
|
|
|
+ opponents_within_casting_range = [o.id for o in self.opponent_heroes if
|
|
|
+ o.distance_from_entity[hero.id] < min_shield_distance]
|
|
|
+ if hero.shield_life == 0 and \
|
|
|
+ self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and \
|
|
|
+ opponents_within_casting_range\
|
|
|
+ and self.threat_level > min_shield_threat\
|
|
|
+ and hero.is_on_my_side:
|
|
|
+ # log(f"{hero.id}: shield against {[(o.id, o.pos, o.distance_from_entity[hero.id]) for o in self.opponent_heroes]}")
|
|
|
+ priority = k0_cast_shield
|
|
|
+ priority += k_shield_enemy_near * len(opponents_within_casting_range)
|
|
|
+ priority += k_cast_shield_distance * min(hero.moving_time_to_hq, hero.moving_time_to_opponent_hq)
|
|
|
+ action = CastShield(hero.id, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ if hero.is_on_opponent_side and not isinstance(self.strategy, (StrategyDefensive, StrategyFullDefense)):
|
|
|
+
|
|
|
+ threat_on_opponent_hq = sum(m.health + m.shield_life for m in self.monsters if m.threats_opponent_hq)
|
|
|
+ threat_on_opponent_hq -= 10 * len([o for o in self.opponent_heroes if o.distance_to_opponent_hq < Monster.THREAT_RANGE])
|
|
|
+ log(f"Offensive buff: {threat_on_opponent_hq}")
|
|
|
+
|
|
|
+ for monster in self.monsters:
|
|
|
+ # cast control on monsters near opponent hq
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and monster.distance_from_entity[hero.id] < CastControl.RANGE:
|
|
|
+ if monster.health < min_control_hp:
|
|
|
+ # too weak
|
|
|
+ continue
|
|
|
+
|
|
|
+ if monster.go_toward_opponent_hq:
|
|
|
+ # already targeting enemy
|
|
|
+ continue
|
|
|
+
|
|
|
+ if monster.shield_life > 0:
|
|
|
+ continue
|
|
|
+
|
|
|
+ priority = k0_cast_control_monster
|
|
|
+ action = CastControl(monster.id, self.opponent_hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ if threat_on_opponent_hq > 50:
|
|
|
+ for monster in self.monsters:
|
|
|
+ # cast shield on monsters threatening opponent hq
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and monster.distance_from_entity[hero.id] < CastShield.RANGE:
|
|
|
+ if not monster.threats_opponent_hq or not monster.go_toward_opponent_hq:
|
|
|
+ # not targeting enemy
|
|
|
+ continue
|
|
|
+ if monster.shield_life > 0:
|
|
|
+ continue
|
|
|
+ priority = k0_cast_shield_on_monster
|
|
|
+ priority += k_shield_monster_distance * monster.moving_time_to_opponent_hq
|
|
|
+ priority += k_shield_monster * monster.health
|
|
|
+ action = CastShield(monster.id, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # cast wind on monsters threatening opponent hq
|
|
|
+ affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
|
|
|
+ priority = k0_cast_offensive_wind
|
|
|
+ priority += k_cast_wind_threats * sum([m.health for m in affected if type(m) is Monster and m.threats_opponent_hq and not m.shield_life])
|
|
|
+ priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
|
|
|
+ priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
|
|
|
+ priority += offensive_buff * threat_on_opponent_hq
|
|
|
+ action = CastWind(self.opponent_hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # cast wind on enemies defending opponent hq
|
|
|
+ affected = [e for e in self.in_wind_area(hero) if not e.shield_life]
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastShield.MANA_COST) and affected:
|
|
|
+ priority = k0_cast_offensive_wind
|
|
|
+ priority += k_cast_wind_enemy * len([e for e in affected if type(e) is OpponentHero and not e.shield_life])
|
|
|
+ priority += k_cast_wind_bad_target * len([e for e in affected if type(e) is Monster and not e.shield_life])
|
|
|
+ priority += k_cast_wind_proximity * hero.moving_time_to_opponent_hq
|
|
|
+ priority += offensive_buff * threat_on_opponent_hq
|
|
|
+ action = CastWind(self.hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ for enemy in self.opponent_heroes:
|
|
|
+ if self.hq.mana >= (CastWind.MANA_COST + CastControl.MANA_COST) and enemy.distance_from_entity[hero.id] < CastControl.RANGE:
|
|
|
+ if enemy.is_controlled:
|
|
|
+ # already controlled
|
|
|
+ continue
|
|
|
+ if enemy.shield_life:
|
|
|
+ continue
|
|
|
+
|
|
|
+ priority = k0_cast_offensive_control
|
|
|
+ priority += k_cast_control_on_opponent_defender * enemy.moving_time_to_opponent_hq
|
|
|
+ priority += k_cast_control_on_opponent_defender_threat * threat_on_opponent_hq
|
|
|
+ priority += offensive_buff_control * threat_on_opponent_hq
|
|
|
+ action = CastControl(enemy.id, self.hq.pos, priority)
|
|
|
+ actions.append(action)
|
|
|
+
|
|
|
+ # Estimate feasibility
|
|
|
+ for action in actions:
|
|
|
+ action.feasibility = -1
|
|
|
+
|
|
|
+ # ignore impossibles actions
|
|
|
+ if isinstance(action, Defend):
|
|
|
+ if action.target.moving_time_to_hq < hero.moving_time_to_hq:
|
|
|
+ # too late...
|
|
|
+ continue
|
|
|
+
|
|
|
+ action.feasibility = 0
|
|
|
+
|
|
|
+ if action.pos and not isinstance(action, Reposition):
|
|
|
+ action.feasibility += k_feasibility_moving_time * self.moving_time_to(hero, action.pos)
|
|
|
+
|
|
|
+ if isinstance(action, Cast):
|
|
|
+ action.feasibility += k_feasibility_casting_cost * action.MANA_COST
|
|
|
+
|
|
|
+ action.feasibility += k_feasibility_far_from_pos * self.moving_time_to(hero, hero.current_role_pos or hero.role.POSITION)
|
|
|
+
|
|
|
+ actions = sorted([a for a in actions if a.feasibility >= 0], key=lambda a: a.priority + a.feasibility)
|
|
|
+
|
|
|
+ try:
|
|
|
+ log(actions)
|
|
|
+ return actions[0]
|
|
|
+ except IndexError:
|
|
|
+ return Wait()
|
|
|
+
|
|
|
+ def update_after_action(self, hero, action):
|
|
|
+ if isinstance(action, (Move, MoveAt)):
|
|
|
+ hero.pos = self.position_after_moving(hero, action.pos, hero.SPEED)
|
|
|
+ # log(f"{hero.id}: move to {action.pos}, new pos: {hero.pos}")
|
|
|
+ for monster in self.in_attack_area(hero):
|
|
|
+ monster.health -= hero.ATTACK
|
|
|
+ if monster.health <= 0:
|
|
|
+ del self._index[monster.id]
|
|
|
+
|
|
|
+ if isinstance(action, Cast):
|
|
|
+ self.hq.mana -= action.MANA_COST
|
|
|
+
|
|
|
+ if isinstance(action, CastWind):
|
|
|
+ affected = self.in_wind_area(hero)
|
|
|
+ for entity in affected:
|
|
|
+ entity.pos = self.position_after_moving(entity, action.params, action.DEPTH)
|
|
|
+ self.update_entity(entity)
|
|
|
+
|
|
|
+ if isinstance(action, CastShield):
|
|
|
+ self.index[action.params[0]].shield_life = CastShield.DURATION
|
|
|
+
|
|
|
+ if isinstance(action, CastControl):
|
|
|
+ target_id, x, y = action.params
|
|
|
+ target = self.index[target_id]
|
|
|
+ target.pos = self.position_after_moving(target, (x, y), target.SPEED)
|
|
|
+ self.update_entity(target)
|
|
|
+ target.is_controlled = True
|
|
|
+ if type(target) is Monster:
|
|
|
+ target.go_toward_opponent_hq = True
|
|
|
+
|
|
|
+ if not isinstance(action, Reposition):
|
|
|
+ # reinit alternative positions if hero has acted elsewhere
|
|
|
+ hero.current_role_pos = None
|
|
|
+
|
|
|
+ def play(self, hero):
|
|
|
+ action = self.get_action_for(hero)
|
|
|
+ log(f"> {action}")
|
|
|
+ action.do()
|
|
|
+ self.update_after_action(hero, action)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def manhattan(from_, to_):
|
|
|
+ xa, ya = from_
|
|
|
+ xb, yb = to_
|
|
|
+ return abs(xa - xb) + abs(ya - yb)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def distance(from_, to_):
|
|
|
+ xa, ya = from_
|
|
|
+ xb, yb = to_
|
|
|
+ return int(math.sqrt((xa - xb) ** 2 + abs(ya - yb) ** 2))
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def moving_time_to(entity, target):
|
|
|
+ distance = Grid.distance(entity.pos, target)
|
|
|
+ return time_to(distance, entity.SPEED)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def position_after_moving(entity, pos, speed):
|
|
|
+ """ get the entity position after it moved toward `pos` from `speed` """
|
|
|
+ x0, y0 = entity.pos
|
|
|
+ x1, y1 = pos
|
|
|
+ dx, dy = (x1 - x0) / (x1 or 1), (y1 - y0) / (y1 or 1)
|
|
|
+
|
|
|
+ return x0 + int(dx * speed), y0 + int(dy * speed)
|
|
|
+
|
|
|
+ def visible(self, pos):
|
|
|
+ return self.distance(self.hq.pos, pos) < self.hq.SIGHT or \
|
|
|
+ any(self.distance(h.pos, pos) < h.SIGHT for h in self.heroes)
|
|
|
+
|
|
|
+ def get_role_positions(self, role):
|
|
|
+ positions = [role.POSITION] + role.ALT_POSITIONS
|
|
|
+ if self.hq.x != 0:
|
|
|
+ return [(self.MAX_X - x, self.MAX_Y - y) for x, y in positions]
|
|
|
+ else:
|
|
|
+ return positions
|
|
|
+
|
|
|
+ def get_role_position(self, hero):
|
|
|
+ """ get the position for the given role """
|
|
|
+ role_positions = self.get_role_positions(hero.role)
|
|
|
+
|
|
|
+ pos = role_positions[0]
|
|
|
+ # log(f"{hero.pos} - {hero.current_role_pos} - {role_positions}")
|
|
|
+ if hero.current_role_pos:
|
|
|
+ try:
|
|
|
+ i = role_positions.index(hero.current_role_pos)
|
|
|
+ pos = role_positions[i + 1]
|
|
|
+ except (ValueError, IndexError):
|
|
|
+ pass
|
|
|
+ return pos
|
|
|
+
|
|
|
+ def in_attack_area(self, hero):
|
|
|
+ return [e for e in self.monsters if e.distance_from_entity[hero.id] <= Hero.ATTACK_RANGE]
|
|
|
+
|
|
|
+ def in_wind_area(self, hero):
|
|
|
+ return [e for e in self.entities if type(e) is not Hero and e.distance_from_entity[hero.id] <= CastWind.RADIUS]
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ # *** init game
|
|
|
+ grid = Grid()
|
|
|
+ grid.init_game()
|
|
|
+
|
|
|
+ while True:
|
|
|
+ # *** get inputs and init round
|
|
|
+ grid.init_round()
|
|
|
+
|
|
|
+ # *** Compute priorities
|
|
|
+ for hero in grid.heroes:
|
|
|
+ grid.play(hero)
|
|
|
+
|
|
|
+
|
|
|
+main()
|