"""Simulation d'un tir : la courbe y = f(x) (et z(x) pour le bot 3D) part du soldat et avance jusqu'à toucher un obstacle, un soldat, ou sortir du plateau. Convention : u = distance parcourue en x depuis le tireur (toujours positive, on « retourne » le plateau pour l'équipe de droite). La courbe est décalée pour passer par le soldat : y(u) = y_tireur + g(u) - g(0), comme dans Graphwar. """ from __future__ import annotations from dataclasses import dataclass import numpy as np from . import config as C from .world import World @dataclass class ShotResult: xs: np.ndarray # trajectoire parcourue (jusqu'à l'impact) ys: np.ndarray zs: np.ndarray event: str # "soldier", "obstacle", "out" hit_index: int = -1 # indice du soldat touché min_dist: float = 99.0 # plus petite distance au soldat visé (pour la récompense) free_dist: float = 99.0 # pareil mais en ignorant les obstacles (« la visée était-elle bonne ? ») @property def end(self): return float(self.xs[-1]), float(self.ys[-1]), float(self.zs[-1]) def _densify(px, py, pz): """Ajoute des points là où la courbe est raide (pour ne pas « sauter » un obstacle).""" seg = np.sqrt(np.diff(px) ** 2 + np.diff(py) ** 2 + np.diff(pz) ** 2) n = np.maximum(1, np.ceil(seg / C.STEP).astype(np.int64)) total = int(n.sum()) if total > C.MAX_POINTS: # coupe la courbe trop longue keep = int(np.searchsorted(np.cumsum(n), C.MAX_POINTS)) keep = max(keep, 1) px, py, pz, n = px[: keep + 1], py[: keep + 1], pz[: keep + 1], n[:keep] total = int(n.sum()) if total == len(n): # rien à densifier return px, py, pz idx = np.repeat(np.arange(len(n)), n) t = (np.arange(total) - np.repeat(np.cumsum(n) - n, n)) / np.repeat(n, n) ox = px[idx] + t * (px[idx + 1] - px[idx]) oy = py[idx] + t * (py[idx + 1] - py[idx]) oz = pz[idx] + t * (pz[idx + 1] - pz[idx]) return (np.append(ox, px[-1]), np.append(oy, py[-1]), np.append(oz, pz[-1])) def simulate(world: World, shooter: int, g, z=None, target: int = -1) -> ShotResult: """g(u) -> décalage vertical (tableau numpy). z(u) -> hauteur au-dessus du sol (optionnel).""" s = world.soldiers[shooter] u = np.arange(0.0, C.U_MAX, C.STEP) with np.errstate(all="ignore"): gy = np.asarray(g(u), dtype=np.float64) py = s.y + gy - gy[0] pz = np.full_like(u, C.SHOT_Z) if z is None else C.SHOT_Z + np.asarray(z(u), dtype=np.float64) px = s.x + s.dir * u bad = ~np.isfinite(py) | ~np.isfinite(pz) | (np.abs(px) > C.BOARD_W) | (np.abs(py) > C.BOARD_H) \ | (pz > C.CEIL_Z) | (pz < 0) out_at = int(np.argmax(bad)) if bad.any() else len(u) event = "out" free = 99.0 if target >= 0: t = world.soldiers[target] k = max(out_at, 1) free = float(np.min(np.hypot(px[:k] - t.x, py[:k] - t.y) + np.maximum(0.0, pz[:k] - C.SOLDIER_H))) px, py, pz = px[:max(out_at, 1)], py[:max(out_at, 1)], pz[:max(out_at, 1)] if len(px) > 1: px, py, pz = _densify(px, py, pz) first = len(px) hit_index = -1 # --- obstacles (cylindres), sauf là où un cratère a creusé o = world.obstacles if len(o): d2 = (px[:, None] - o[:, 0]) ** 2 + (py[:, None] - o[:, 1]) ** 2 inside = ((d2 < o[:, 2] ** 2) & (pz[:, None] < o[:, 3])).any(axis=1) if inside.any() and len(world.craters): c = world.craters dc = (px[:, None] - c[:, 0]) ** 2 + (py[:, None] - c[:, 1]) ** 2 inside &= ~(dc < c[:, 2] ** 2).any(axis=1) if inside.any(): k = int(np.argmax(inside)) if k < first: first, event = k, "obstacle" # --- soldiers for i, sd in enumerate(world.soldiers): if i == shooter or not sd.alive: continue d2 = (px[:first] - sd.x) ** 2 + (py[:first] - sd.y) ** 2 hit = (d2 < C.SOLDIER_R ** 2) & (pz[:first] < C.SOLDIER_H) if hit.any(): k = int(np.argmax(hit)) if k < first: first, event, hit_index = k, "soldier", i end = min(first + 1, len(px)) xs, ys, zs = px[:end], py[:end], pz[:end] md = 99.0 if target >= 0: t = world.soldiers[target] dxy = np.hypot(xs - t.x, ys - t.y) md = float(np.min(dxy + np.maximum(0.0, zs - C.SOLDIER_H))) return ShotResult(xs, ys, zs, event, hit_index, md, free) def apply(world: World, res: ShotResult) -> None: """Applique les conséquences d'un tir : soldat tué ou cratère creusé.""" if res.event == "soldier": world.soldiers[res.hit_index].alive = False elif res.event == "obstacle": x, y, _ = res.end world.add_crater(x, y)