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())
{{ $message }}
@enderror @elseYou're {{ $me['name'] }}. Outlined cells are the ones you can take.
@endif