<?phpnamespace App\Game;/** * The rules of the duel, run by the host only (the authority): two players catch stars for a while, the one with more wins. * Player 0 = the host, 1 = the other one. Positions are the top-left corners of their squares. */final class Duel{ public const W = 320; public const H = 180; public const TOP = 18; public const SIZE = 10; public const STAR_RADIUS = 4; public const STARS = 3; public const SECONDS = 40.0; public const SPEED = 110.0; /** @var list<array{0: float, 1: float}> */ public array $stars = []; /** @var array{0: int, 1: int} */ public array $scores = [0, 0]; public float $left = self::SECONDS; public bool $over = false; public function __construct(int $seed = 0) { $this->reset($seed); } public function reset(int $seed = 0): void { mt_srand($seed !== 0 ? $seed : random_int(1, 1 << 30)); $this->scores = [0, 0]; $this->left = self::SECONDS; $this->over = false; $this->stars = []; for ($i = 0; $i < self::STARS; $i++) { $this->stars[] = $this->newStar(); } } /** @param array{0: float, 1: float} $host @param array{0: float, 1: float} $guest */ public function step(float $dt, array $host, array $guest): void { if ($this->over) { return; } $this->left = max(0.0, $this->left - $dt); foreach ($this->stars as $i => $star) { foreach ([$host, $guest] as $who => $pos) { if (self::touches($pos, $star)) { $this->scores[$who]++; $this->stars[$i] = $this->newStar(); break; } } } $this->over = $this->left <= 0; } /** 0 = the host won, 1 = the other one, -1 = a draw (or not over). */ public function winner(): int { if (!$this->over || $this->scores[0] === $this->scores[1]) { return -1; } return $this->scores[0] > $this->scores[1] ? 0 : 1; } /** What the host sends to the other player ~30 times a second. @param array{0: float, 1: float} $host @param array{0: float, 1: float} $guest */ public function snapshot(array $host, array $guest): array { $r = fn (array $p) => [round($p[0], 1), round($p[1], 1)]; return ['t' => 's', 'h' => $r($host), 'g' => $r($guest), 's' => array_map($r, $this->stars), 'sc' => $this->scores, 'l' => round($this->left, 1), 'o' => $this->over ? 1 : 0]; } /** @param array{0: float, 1: float} $pos @param array{0: float, 1: float} $star */ public static function touches(array $pos, array $star): bool { $nx = max($pos[0], min($star[0], $pos[0] + self::SIZE)); $ny = max($pos[1], min($star[1], $pos[1] + self::SIZE)); return ($star[0] - $nx) ** 2 + ($star[1] - $ny) ** 2 <= (self::STAR_RADIUS + 1) ** 2; } /** A position moved by a direction (-1..1 each) for $dt seconds, kept in the arena. @param array{0: float, 1: float} $pos */ public static function move(array $pos, float $dx, float $dy, float $dt): array { $len = sqrt($dx * $dx + $dy * $dy); if ($len > 1) { $dx /= $len; $dy /= $len; } return [max(0.0, min(self::W - self::SIZE, $pos[0] + $dx * self::SPEED * $dt)), max((float)self::TOP, min(self::H - self::SIZE, $pos[1] + $dy * self::SPEED * $dt))]; } /** The direction to the nearest star (the bot, the demo). @param array{0: float, 1: float} $pos @param list<array{0: float, 1: float}> $stars */ public static function towardNearest(array $pos, array $stars): array { $best = null; $cx = $pos[0] + self::SIZE / 2; $cy = $pos[1] + self::SIZE / 2; foreach ($stars as $s) { $d = ($s[0] - $cx) ** 2 + ($s[1] - $cy) ** 2; if ($best === null || $d < $best[0]) { $best = [$d, $s[0] - $cx, $s[1] - $cy]; } } if ($best === null || $best[0] < 1) { return [0.0, 0.0]; } $len = sqrt($best[0]); return [$best[1] / $len, $best[2] / $len]; } /** @return array{0: float, 1: float} */ private function newStar(): array { $m = 12; return [(float)mt_rand($m, self::W - $m), (float)mt_rand(self::TOP + $m, self::H - $m)]; }}