Foxgrad's picture
Add game engine and bots
3304d77 verified
Raw History Blame Contribute Delete
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
@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)