Download graphwar/shot.py from Foxgrad/graphwar-arena: direct link, hf CLI and curl.
- Browser
- Download file 4.85 kB
-
https://huggingface.co/Foxgrad/graphwar-arena/resolve/main/graphwar/shot.py
- Command line
-
hf download hf://Foxgrad/graphwar-arena/graphwar/shot.py
-
curl -L -o shot.py https://huggingface.co/Foxgrad/graphwar-arena/resolve/main/graphwar/shot.py
4.85 kB
| """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 | |
| 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 ? ») | |
| 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) | |