|
|
@@ -130,6 +130,17 @@ PLAYERS_INDEX = {p.id: p for p in [ME, OPPONENT]}
|
|
|
PLAYERS_ORDER = sorted([ME, OPPONENT], key=lambda p: p.id)
|
|
|
|
|
|
|
|
|
+class Threat(BaseClass):
|
|
|
+ def __init__(self, unit, damages, target, line):
|
|
|
+ self.unit = unit
|
|
|
+ self.damages = damages
|
|
|
+ self.target = target
|
|
|
+ self.line = line if line else []
|
|
|
+ self.fatal = damages >= target.hp
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return f"<Threat (unit: {self.unit.id}, damages={self.damages}, target={self.target.id}, fatal={self.fatal}, line={self.line})>"
|
|
|
+
|
|
|
class Unit(BaseClass):
|
|
|
TYPE_CULTIST = 0
|
|
|
TYPE_CULT_LEADER = 1
|
|
|
@@ -148,6 +159,9 @@ class Unit(BaseClass):
|
|
|
self.y = None
|
|
|
self.owner = None
|
|
|
|
|
|
+ self.threats = []
|
|
|
+ self.threaten_by = []
|
|
|
+
|
|
|
@property
|
|
|
def pos(self):
|
|
|
return self.x, self.y
|
|
|
@@ -314,6 +328,34 @@ class Grid(BaseClass):
|
|
|
if threat > self.threat[(x, y)]:
|
|
|
self.threat[(x, y)] = threat
|
|
|
|
|
|
+ for u in self.opponent_units:
|
|
|
+ u.threat = []
|
|
|
+ for a in self.owned_units:
|
|
|
+ line_of_sight = self.line_of_sight(u.pos, a.pos)
|
|
|
+ if not line_of_sight:
|
|
|
+ continue
|
|
|
+
|
|
|
+ dist = self.manhattan(u.pos, a.pos)
|
|
|
+ if dist <= u.SHOOTING_RANGE:
|
|
|
+ damages = u.SHOOTING_MAX_DAMAGE - dist
|
|
|
+ threat = Threat(u, damages, a, line_of_sight)
|
|
|
+ u.threats.append(threat)
|
|
|
+ a.threaten_by.append(threat)
|
|
|
+
|
|
|
+ for a in self.owned_units:
|
|
|
+ a.threat = []
|
|
|
+ for u in self.opponent_units:
|
|
|
+ line_of_sight = self.line_of_sight(a.pos, u.pos)
|
|
|
+ if not line_of_sight:
|
|
|
+ continue
|
|
|
+
|
|
|
+ dist = self.manhattan(u.pos, a.pos)
|
|
|
+ if dist <= u.SHOOTING_RANGE:
|
|
|
+ damages = u.SHOOTING_MAX_DAMAGE - dist
|
|
|
+ threat = Threat(a, damages, u, line_of_sight)
|
|
|
+ a.threats.append(threat)
|
|
|
+ u.threaten_by.append(threat)
|
|
|
+
|
|
|
def compute_conversion_path(self):
|
|
|
conversion_path = ConversionPath()
|
|
|
|
|
|
@@ -331,6 +373,7 @@ class Grid(BaseClass):
|
|
|
self.update_index()
|
|
|
self.update_threat_map()
|
|
|
self.compute_conversion_path()
|
|
|
+ log('indexes updated')
|
|
|
|
|
|
# log(self.obstacles + [u.pos for u in self.allied_cultists])
|
|
|
# log([n.pos for n in self.neutrals])
|
|
|
@@ -338,10 +381,19 @@ class Grid(BaseClass):
|
|
|
def build_actions(self):
|
|
|
actions = Queue()
|
|
|
|
|
|
+ advantage = len(self.owned_units) > len(self.opponent_units)
|
|
|
+ sure_advantage = len(self.owned_units) > (len(self.opponent_units) + len(self.neutrals))
|
|
|
+
|
|
|
+ for u in self.allied_cultists:
|
|
|
+ if u.threats:
|
|
|
+ log(f"Threats : {u.id} - {u.threats}")
|
|
|
+ if u.threaten_by:
|
|
|
+ log(f"Threaten : {u.id} - {u.threaten_by}")
|
|
|
+
|
|
|
# Leader take cover
|
|
|
k0_protect_cult_leader = 30
|
|
|
k_protect_threat_level = -5
|
|
|
- k_cover_threat = 10
|
|
|
+ k_protect_worth_risk = 3
|
|
|
k_cover_interest = -10
|
|
|
|
|
|
if self.cult_leader:
|
|
|
@@ -351,19 +403,19 @@ class Grid(BaseClass):
|
|
|
covers = [n for n in self.neighbors(*self.cult_leader.pos) if self.can_move_on(self.cult_leader, n)]
|
|
|
|
|
|
for pos in covers:
|
|
|
- action = ActionMove(self.cult_leader, pos, f'take cover (t: {current_threat})')
|
|
|
-
|
|
|
interest = bool(self.conversion_path and self.conversion_path.steps and pos in [s.pos for s in
|
|
|
- self.conversion_path.steps])
|
|
|
+ self.conversion_path.steps])
|
|
|
|
|
|
priority = k0_protect_cult_leader
|
|
|
- priority += k_protect_threat_level * current_threat
|
|
|
- priority += k_cover_threat * self.threat[pos]
|
|
|
+ priority += k_protect_threat_level * (current_threat - self.threat[pos])
|
|
|
priority += k_cover_interest * interest
|
|
|
+ priority += k_protect_worth_risk * (self.cult_leader.hp - (current_threat + 1))
|
|
|
|
|
|
+ action = ActionMove(self.cult_leader, pos, f'take cover (current: {current_threat}, new: {self.threat[pos]})')
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
# Convert
|
|
|
+ k0_convert = 15
|
|
|
k_convert_number = -10
|
|
|
k_convert_distance = 10
|
|
|
k_convert_danger = 20
|
|
|
@@ -373,7 +425,7 @@ class Grid(BaseClass):
|
|
|
if path:
|
|
|
targets = [self.index[c] for c in path[-1].candidates]
|
|
|
|
|
|
- priority = 0
|
|
|
+ priority = k0_convert
|
|
|
priority += k_convert_number * len(targets)
|
|
|
priority += k_convert_distance * len(path)
|
|
|
priority += k_convert_danger * sum([self.threat[s.pos] for s in path])
|
|
|
@@ -386,104 +438,87 @@ class Grid(BaseClass):
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
# Shoot opponent units
|
|
|
- k_shoot_opponent_cultist = 8
|
|
|
- k_shoot_opponent_leader = 4
|
|
|
- k_shoot_movement_needed = 15
|
|
|
- k0_shoot_in_danger = -10
|
|
|
- k_shoot_sacrifice = 50
|
|
|
+ k0_shoot = 0
|
|
|
+ k_shoot_opponent_cultist = 6
|
|
|
+ k_shoot_opponent_leader = 5
|
|
|
+ k_shoot_kill_before_he_kills = -5
|
|
|
+ k_shoot_sacrifice = 30
|
|
|
k0_runaway = 10
|
|
|
k_runaway_threat = 10
|
|
|
|
|
|
for a in self.allied_cultists:
|
|
|
- targets = {}
|
|
|
- for u in self.opponent_units:
|
|
|
- line_of_sight = self.line_of_sight(a.pos, u.pos)
|
|
|
- if not len(line_of_sight):
|
|
|
- continue
|
|
|
-
|
|
|
- if len(line_of_sight) <= u.SHOOTING_RANGE:
|
|
|
- targets[u] = line_of_sight
|
|
|
- else:
|
|
|
- # la cible est hors de portée, la poursuivre si plus faible et sans soutien?
|
|
|
- # TODO: implémenter
|
|
|
- pass
|
|
|
-
|
|
|
- if not targets:
|
|
|
+ if not a.threats:
|
|
|
continue
|
|
|
|
|
|
- # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
|
|
|
- dist_to_nearest_target = min([len(line) for line in targets.values()])
|
|
|
- damages_received = Unit.SHOOTING_MAX_DAMAGE - dist_to_nearest_target
|
|
|
- unit_in_danger = damages_received >= a.hp or (len(targets) > 1 and (damages_received + 3) >= a.hp)
|
|
|
+ for threat in a.threats:
|
|
|
+ priority = k0_shoot
|
|
|
|
|
|
- for u, line in targets.items():
|
|
|
- dist = len(line)
|
|
|
- damages = Unit.SHOOTING_MAX_DAMAGE - dist
|
|
|
- target_die = damages >= u.hp
|
|
|
+ # Il est probable que l'unité se fera tirer dessus par l'ennemi le plus proche: quel résultat?
|
|
|
+ can_be_killed_by = [t for t in a.threaten_by if t.fatal]
|
|
|
|
|
|
- priority = 0
|
|
|
+ if can_be_killed_by and not threat.fatal:
|
|
|
+ # run away! (si possible)
|
|
|
+ worth_sacrificing = any(t.target is self.cult_leader or (t.fatal and t.target is not a) for t in threat.target.threats)
|
|
|
+ covers = [n for n in self.neighbors(*a.pos) if self.can_move_on(a, n)]
|
|
|
|
|
|
- if unit_in_danger and not target_die:
|
|
|
- # run away!
|
|
|
- covers = [n for n in self.neighbors(*a.pos)
|
|
|
- if self.threat[n] < self.threat[a.pos] and self.can_move_on(a, n)]
|
|
|
+ if covers and not worth_sacrificing:
|
|
|
+ for cover in covers:
|
|
|
+ if self.threat[cover] < a.hp and not any(self.line_of_sight(u.unit.pos, cover) for u in can_be_killed_by):
|
|
|
+ action = ActionMove(a, cover, f'run away!')
|
|
|
+ priority += k0_runaway
|
|
|
+ priority += k_runaway_threat * self.threat[cover]
|
|
|
+ actions.put(priority, action)
|
|
|
+ continue
|
|
|
|
|
|
- for pos in covers:
|
|
|
- action = ActionMove(a, pos, f'run away!')
|
|
|
-
|
|
|
- priority += k0_runaway
|
|
|
- priority += k_runaway_threat * self.threat[pos]
|
|
|
-
|
|
|
- actions.put(priority, action)
|
|
|
- continue
|
|
|
+ log(f'<!> {a.id} seems lost')
|
|
|
|
|
|
- elif unit_in_danger and target_die and len(targets) > 1:
|
|
|
- # bof, un échange, pas sûr que ça puisse être intéressant...
|
|
|
+ elif any(t for t in can_be_killed_by if t.unit != threat.target) and threat.fatal:
|
|
|
+ # l'unité peut tuer la cible, mais sera probablement tuée par une autre unité ennemie
|
|
|
priority += k_shoot_sacrifice
|
|
|
|
|
|
- k = k_shoot_opponent_leader if u is self.opponent_cult_leader else k_shoot_opponent_cultist
|
|
|
- priority += k * dist
|
|
|
- priority += k0_shoot_in_danger * unit_in_danger
|
|
|
+ if not threat.fatal:
|
|
|
+ k = k_shoot_opponent_leader if threat.target is self.opponent_cult_leader else k_shoot_opponent_cultist
|
|
|
+ priority += k * len(threat.line)
|
|
|
+
|
|
|
+ # peut tuer un adversaire avant qu'il ne tue un allié
|
|
|
+ priority += k_shoot_kill_before_he_kills * any(t.fatal and t.target is not a for t in threat.target.threats)
|
|
|
|
|
|
- action = ActionShoot(a, u, f'shoot from {a.pos} ({line})')
|
|
|
+ action = ActionShoot(a, threat.target, f'shoot from {a.pos} (damages: {threat.damages}, fatal: {threat.fatal}, line: {threat.line})')
|
|
|
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
- # Position
|
|
|
- k0_position = 30
|
|
|
- k_advantage = 40
|
|
|
+ # Position: go to front line
|
|
|
+ k0_position = 50
|
|
|
+ k_position_advantage = 40
|
|
|
k_position_distance = 3 # on veut privilégier les plus près, mais pas non plus décourager ceux de l'arrière...
|
|
|
- hp_min = 5
|
|
|
-
|
|
|
- advantage = len(self.owned_units) > len(self.opponent_units) + len(self.neutrals)
|
|
|
+ position_hp_min = 5
|
|
|
|
|
|
for a in self.allied_cultists:
|
|
|
-
|
|
|
- if a.hp < hp_min:
|
|
|
+ if a.hp < position_hp_min:
|
|
|
continue
|
|
|
-
|
|
|
if self.threat[a.pos]:
|
|
|
- # l'unité est déjà dans une zone à risque
|
|
|
- # TODO: envisager un retrait
|
|
|
+ # unit already under threat, let shoot action manage this
|
|
|
+ continue
|
|
|
+ if any(self.threat[n] for n in self.neighbors(*a.pos)):
|
|
|
+ # unit already on frontline
|
|
|
continue
|
|
|
|
|
|
- else:
|
|
|
- # l'unité semble en sécurité, go to front-line
|
|
|
- nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.threat[x] == 1, limit=1)
|
|
|
- if not nearest_frontline:
|
|
|
- log(f"<!> {a.id} can not join nearest frontline")
|
|
|
- continue
|
|
|
+ # l'unité semble en sécurité, go to front-line
|
|
|
+ nearest_frontline = self.discover(a, key=lambda x: self.can_move_on(a, x) and self.threat[x] == 1,
|
|
|
+ limit=1)
|
|
|
+ if not nearest_frontline:
|
|
|
+ log(f"<!> {a.id} can not join nearest frontline")
|
|
|
+ continue
|
|
|
|
|
|
- path, target = nearest_frontline[0]
|
|
|
- if path:
|
|
|
- # log(f"{a.id} - {path} - {target}")
|
|
|
- # already in place
|
|
|
- priority = k0_position
|
|
|
- priority += k_position_distance * len(path)
|
|
|
- priority += k_advantage * advantage
|
|
|
+ path, target = nearest_frontline[0]
|
|
|
+ if path:
|
|
|
+ # log(f"{a.id} - {path} - {target}")
|
|
|
+ priority = k0_position
|
|
|
+ priority += k_position_distance * len(path)
|
|
|
+ priority += k_position_advantage * advantage
|
|
|
|
|
|
- action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
|
|
|
- actions.put(priority, action)
|
|
|
+ action = ActionMove(a, path[0], f'go to frontline {target} by {path}')
|
|
|
+ actions.put(priority, action)
|
|
|
|
|
|
# Shoot neutral units:
|
|
|
k_shoot_dangerous_neutral_threat = 2
|
|
|
@@ -492,7 +527,8 @@ class Grid(BaseClass):
|
|
|
|
|
|
if self.opponent_cult_leader:
|
|
|
|
|
|
- discovered_by_opponent = self.discover(self.opponent_cult_leader, key=lambda x: x in self.index and self.index[x].neutral, limit=3)
|
|
|
+ discovered_by_opponent = self.discover(self.opponent_cult_leader,
|
|
|
+ key=lambda x: x in self.index and self.index[x].neutral, limit=3)
|
|
|
neutrals_threaten = {}
|
|
|
for path, target_pos in discovered_by_opponent:
|
|
|
if len(path) > dangerous_neutral_distance_limit:
|
|
|
@@ -504,7 +540,8 @@ class Grid(BaseClass):
|
|
|
if self.cult_leader:
|
|
|
discovered_by_leader = self.discover(self.cult_leader, key=lambda x: x in neutrals_threaten, limit=3)
|
|
|
for path, target_pos in discovered_by_leader:
|
|
|
- if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target_pos] < len(path):
|
|
|
+ if target_pos not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[
|
|
|
+ target_pos] < len(path):
|
|
|
threaten_neutrals_from_leader[target_pos] = len(path)
|
|
|
|
|
|
log(f"Nearest from opp. leader: {neutrals_threaten}")
|
|
|
@@ -512,9 +549,11 @@ class Grid(BaseClass):
|
|
|
|
|
|
lost_causes = {}
|
|
|
for target, dist in neutrals_threaten.items():
|
|
|
- if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= neutrals_threaten[target]:
|
|
|
- lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target, 0)) # la distance retenue est la différence entre la distance entre la cible
|
|
|
- # et le leader ennemi, et celle entre la cible et le leader allié
|
|
|
+ if target not in threaten_neutrals_from_leader or threaten_neutrals_from_leader[target] <= \
|
|
|
+ neutrals_threaten[target]:
|
|
|
+ lost_causes[target] = (dist - threaten_neutrals_from_leader.get(target,
|
|
|
+ 0)) # la distance retenue est la différence entre la distance entre la cible
|
|
|
+ # et le leader ennemi, et celle entre la cible et le leader allié
|
|
|
|
|
|
for a in self.allied_cultists:
|
|
|
for pos, dist in lost_causes.items():
|
|
|
@@ -538,10 +577,49 @@ class Grid(BaseClass):
|
|
|
# log(self.line_of_sight(a.pos, u.pos))
|
|
|
actions.put(priority, action)
|
|
|
|
|
|
- # TODO: action 'take cover' pour les unités aussi
|
|
|
- # TODO: action 'peace-keeping': tirer sur les neutres qu'on ne pourra pas convertir avant l'ennemi
|
|
|
- # TODO: action 'intercept': une unité se place entre un tireur ennemi et le leader; en dernier recours
|
|
|
- # TODO: action 'do nothing' : parfois, c'est la meilleure chose à faire
|
|
|
+ # Last hope: take risks
|
|
|
+ if not advantage and not actions:
|
|
|
+ k0_last_chance = 80
|
|
|
+ k_last_chance_danger = 5
|
|
|
+ last_chance_hp_min = 5
|
|
|
+
|
|
|
+ for a in self.allied_cultists:
|
|
|
+ if a.hp < last_chance_hp_min:
|
|
|
+ continue
|
|
|
+
|
|
|
+ for n in self.neighbors(*a.pos):
|
|
|
+ if not self.threat[n]:
|
|
|
+ continue
|
|
|
+
|
|
|
+ ongoing_fights = []
|
|
|
+ for u in self.opponent_cultists:
|
|
|
+ line_of_sight = self.line_of_sight(u.pos, n)
|
|
|
+ if not line_of_sight:
|
|
|
+ continue
|
|
|
+
|
|
|
+ dist = self.manhattan(u.pos, n)
|
|
|
+ if dist <= u.SHOOTING_RANGE:
|
|
|
+ damages = u.SHOOTING_MAX_DAMAGE - dist
|
|
|
+
|
|
|
+ ongoing_fights.append((u, damages))
|
|
|
+
|
|
|
+ if any(d >= a.hp for _, d in ongoing_fights):
|
|
|
+ # c'est du suicide: sortir revient à se prendre un coup fatal
|
|
|
+ continue
|
|
|
+
|
|
|
+ action = ActionMove(a, n, f"last hope, moving")
|
|
|
+
|
|
|
+ priority = k0_last_chance
|
|
|
+ priority += k_last_chance_danger * max(d for _, d in ongoing_fights)
|
|
|
+ actions.put(priority, action)
|
|
|
+
|
|
|
+ # TODO: si les deux leaders sont de chaque côté d'un neutre, même si notre leader pourrait convertir le premier, la menace l'emporte et il se barre... repenser le mécanisme de 'take cover'
|
|
|
+ # TODO: ne pas aller convertir les neutres qui sont sous le feu ennemi, ou au moins baisser leur priorité (surtout s'ils sont la seule chose entre le leader et les ennemis)
|
|
|
+ # TODO: augmenter la prise de risque pour la conversion, parfois ça peut changer le résultat si le leader a tous ses pvs
|
|
|
+ # TODO: cf Capture d’écran 2022-09-24 014757.png -> même si pas de ligne entre deux pos, la position peut se retrouver sur une ligne de mire avec une autre cible... snif
|
|
|
+ # TODO: l'algo actuel va privilégier un tir sur un neutre lointain voisin du leader ennemi, plutôt que sur un ennemi en train de nous tirer dessus
|
|
|
+ # TODO: toujours le pbm des tirs supposés possibles mais non, ça coince quand le tir est en diagonale entre deux obstables; et autres pbm avec cet algo :(
|
|
|
+ # TODO: abandonner l'idée d'aller convertir si la menace est mortelle...
|
|
|
|
|
|
return actions
|
|
|
|
|
|
@@ -754,7 +832,7 @@ class Grid(BaseClass):
|
|
|
if not self.can_move_on(unit, pos):
|
|
|
continue
|
|
|
|
|
|
- moving_cost = 1
|
|
|
+ moving_cost = 1 + self.threat[pos]
|
|
|
|
|
|
matches = []
|
|
|
for n in self.neighbors(*pos):
|