player id), players (by id), endsAt (ms). */ public array $board = ['cells' => [], 'players' => [], 'endsAt' => null]; // ---- On the server ------------------------------------------------------------------- // Loaded on mount, after each of this page's moves, and whenever anyone's move changes it. #[Server, Watch, Live('spread')] public static function board(): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('spread', 'board', func_get_args()); } #[Server] public static function enter(#[Validate('required|max:16')] string $name): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('spread', 'enter', func_get_args()); } /** Starts a round, unless one's going: the board is cleared. */ #[Server] public static function begin(): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('spread', 'begin', func_get_args()); } /** Claims cell x,y for the player with `$token`, if the rules allow: whether it did. */ #[Server] public static function take( string $token, #[Validate('integer|between:0,11')] int $x, #[Validate('integer|between:0,11')] int $y, ): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('spread', 'take', func_get_args()); } // ---- In both: the rules, which the browser uses to show your moves ------------------- /** Whether player `$id` may claim x,y on `$cells`. */ private static function allowed(array $cells, int $id, int $x, int $y): bool { $owner = $cells["$x,$y"] ?? null; if ($owner === $id) { return false; } if ($owner === null && !in_array($id, $cells, true)) { return true; // your first cell } $mine = 0; foreach ([[1, 0], [-1, 0], [0, 1], [0, -1]] as [$dx, $dy]) { $mine += ($cells[($x + $dx) . ',' . ($y + $dy)] ?? null) === $id ? 1 : 0; } return $mine >= ($owner === null ? 1 : 2); } private static function running(int|float|null $endsAt): bool { return $endsAt !== null && $endsAt > self::now(); } /** Now, in ms. A float: the browser's PHP is 32-bit, where an int stops at about 2.1 billion * (and a timestamp in ms from JSON arrives as a float). */ private static function now(): float { return microtime(true) * 1000; } // ---- In the browser ------------------------------------------------------------------- protected function mounted(): void { // Re-render for the countdown. Loaders don't run again unless what they watch changed. U::setInterval(fn() => $this->refresh(), 250); } public function join(): Generator { $this->me = yield static::enter($this->name); } public function start(): Generator { yield static::begin(); } public function claim(int $x, int $y): Generator { if (!$this->canClaim($x, $y)) { return; } // Shown at once; the board loaded after the call says whether the server agreed. $this->board['cells']["$x,$y"] = $this->me['id']; yield static::take($this->me['token'], $x, $y); } private function canClaim(int $x, int $y): bool { return $this->me !== null && $this->secondsLeft() > 0 && self::allowed($this->board['cells'], $this->me['id'], $x, $y); } private function secondsLeft(): int { return max(0, (int) ceil((($this->board['endsAt'] ?? 0) - self::now()) / 1000)); } /** Players with their cell counts, most first. */ private function scores(): array { $counts = array_count_values($this->board['cells']); $scores = []; foreach ($this->board['players'] as $id => $player) { $scores[] = [...$player, 'id' => $id, 'cells' => $counts[$id] ?? 0]; } usort($scores, fn($a, $b) => $b['cells'] <=> $a['cells']); return $scores; } }; ?> @php($left = $this->secondsLeft()) @php($scores = $this->scores())
@if ($left > 0) {{ $left }}s @elseif ($board['endsAt'] !== null && $scores !== [] && $scores[0]['cells'] > 0) Round over: {{ $scores[0]['name'] }} wins with {{ $scores[0]['cells'] }} cells @else No round going @endif @if ($left === 0) @endif
@if ($me === null)
@error('name')

{{ $message }}

@enderror @else

You're {{ $me['name'] }}. Outlined cells are the ones you can take.

@endif
@for ($y = 0; $y < self::SIZE; $y++) @for ($x = 0; $x < self::SIZE; $x++) @php($owner = $board['cells']["$x,$y"] ?? null) @endfor @endfor
    @foreach ($scores as $player)
  1. {{ $player['name'] }} {{ $player['cells'] }}
  2. @endforeach