exec($sql); } function now_ms(): int { return (int) floor(microtime(true) * 1000); } function random_token(): string { return bin2hex(random_bytes(8)); } function spawn_points(int $w, int $h): array { return [ ['x' => 1, 'y' => 1], ['x' => $w - 2, 'y' => 1], ['x' => 1, 'y' => $h - 2], ['x' => $w - 2, 'y' => $h - 2], ]; } function generate_map(int $w = 13, int $h = 11): array { $tiles = []; for ($y = 0; $y < $h; $y++) { $row = []; for ($x = 0; $x < $w; $x++) { $tile = 0; if ($x === 0 || $y === 0 || $x === $w - 1 || $y === $h - 1) { $tile = 1; } elseif ($x % 2 === 0 && $y % 2 === 0) { $tile = 1; } elseif (mt_rand(1, 100) <= 60) { $tile = 2; } $row[] = $tile; } $tiles[] = $row; } $spawns = spawn_points($w, $h); foreach ($spawns as $spawn) { for ($dy = -1; $dy <= 1; $dy++) { for ($dx = -1; $dx <= 1; $dx++) { $sx = $spawn['x'] + $dx; $sy = $spawn['y'] + $dy; if ($sx >= 0 && $sy >= 0 && $sx < $w && $sy < $h) { $tiles[$sy][$sx] = 0; } } } } return [ 'w' => $w, 'h' => $h, 'tiles' => $tiles ]; } function default_state(string $playerName, string $token, bool $host = true): array { $map = generate_map(); $spawns = spawn_points($map['w'], $map['h']); $colors = ['#111827', '#374151', '#6b7280', '#9ca3af']; $player = [ 'token' => $token, 'name' => $playerName, 'x' => $spawns[0]['x'], 'y' => $spawns[0]['y'], 'alive' => true, 'is_host' => $host, 'bombs' => 1, 'blast' => 2, 'speed' => 1, 'color' => $colors[0] ]; return [ 'map' => $map, 'players' => [$player], 'bombs' => [], 'explosions' => [], 'powerups' => [], 'started_at' => null, 'last_tick' => now_ms() ]; } function get_room(int $roomId): ?array { $stmt = db()->prepare('SELECT * FROM rooms WHERE id = :id'); $stmt->execute([':id' => $roomId]); $row = $stmt->fetch(); if (!$row) { return null; } $row['state'] = json_decode($row['state_json'], true) ?: []; return $row; } function save_room_state(int $roomId, array $state, string $status): void { $stmt = db()->prepare('UPDATE rooms SET state_json = :state_json, status = :status WHERE id = :id'); $stmt->execute([ ':state_json' => json_encode($state, JSON_UNESCAPED_UNICODE), ':status' => $status, ':id' => $roomId ]); } function create_room(string $roomName, string $playerName, string $token, int $maxPlayers = 4): int { $state = default_state($playerName, $token, true); $stmt = db()->prepare('INSERT INTO rooms (name, status, max_players, state_json) VALUES (:name, :status, :max_players, :state_json)'); $stmt->execute([ ':name' => $roomName, ':status' => 'waiting', ':max_players' => $maxPlayers, ':state_json' => json_encode($state, JSON_UNESCAPED_UNICODE) ]); return (int) db()->lastInsertId(); } function add_player_to_room(array $room, string $playerName, string $token): array { $state = $room['state']; $players = $state['players'] ?? []; $maxPlayers = (int) $room['max_players']; if (count($players) >= $maxPlayers) { throw new RuntimeException('Room is full'); } $spawns = spawn_points($state['map']['w'], $state['map']['h']); $colors = ['#111827', '#374151', '#6b7280', '#9ca3af']; $index = count($players); $spawn = $spawns[$index % count($spawns)]; $player = [ 'token' => $token, 'name' => $playerName, 'x' => $spawn['x'], 'y' => $spawn['y'], 'alive' => true, 'is_host' => false, 'bombs' => 1, 'blast' => 2, 'speed' => 1, 'color' => $colors[$index % count($colors)] ]; $players[] = $player; $state['players'] = $players; return $state; } function remove_player_from_room(array $room, string $token): ?array { $state = $room['state']; $players = $state['players'] ?? []; $newPlayers = array_values(array_filter($players, fn($p) => $p['token'] !== $token)); if (count($newPlayers) === 0) { return null; } $hostExists = false; foreach ($newPlayers as $p) { if (!empty($p['is_host'])) { $hostExists = true; break; } } if (!$hostExists) { $newPlayers[0]['is_host'] = true; } $state['players'] = $newPlayers; $room['state'] = $state; return $room; } function delete_room(int $roomId): void { $stmt = db()->prepare('DELETE FROM rooms WHERE id = :id'); $stmt->execute([':id' => $roomId]); } function cleanup_empty_rooms(): int { $stmt = db()->prepare('SELECT id, state_json FROM rooms'); $stmt->execute(); $rows = $stmt->fetchAll(); $deleted = 0; foreach ($rows as $row) { $state = json_decode($row['state_json'] ?? '', true); $players = is_array($state) ? ($state['players'] ?? null) : null; $isEmpty = !is_array($players) || count($players) === 0; if ($isEmpty) { delete_room((int) $row['id']); $deleted++; } } return $deleted; } function get_session_player(int $roomId): ?array { return $_SESSION['room_players'][$roomId] ?? null; } function set_session_player(int $roomId, string $token, string $name): void { if (!isset($_SESSION['room_players'])) { $_SESSION['room_players'] = []; } $_SESSION['room_players'][$roomId] = ['token' => $token, 'name' => $name]; } function update_room_tick(array $room): array { $state = $room['state']; $now = now_ms(); $state['explosions'] = array_values(array_filter($state['explosions'] ?? [], function ($exp) use ($now) { return ($exp['until'] ?? 0) > $now; })); $bombs = $state['bombs'] ?? []; $remaining = []; $newExplosions = []; foreach ($bombs as $bomb) { if (($bomb['placed_at'] ?? 0) + 2000 <= $now) { $blast = $bomb['blast'] ?? 2; $newExplosions = array_merge($newExplosions, compute_explosion_tiles($state, $bomb['x'], $bomb['y'], $blast)); } else { $remaining[] = $bomb; } } $state['bombs'] = $remaining; if ($newExplosions) { $tiles = $state['map']['tiles']; foreach ($newExplosions as $exp) { $tiles[$exp['y']][$exp['x']] = $exp['tile_after']; if (!empty($exp['powerup'])) { $state['powerups'][] = ['x' => $exp['x'], 'y' => $exp['y'], 'type' => $exp['powerup']]; } $state['explosions'][] = [ 'x' => $exp['x'], 'y' => $exp['y'], 'until' => $now + 550 ]; } $state['map']['tiles'] = $tiles; } $players = $state['players'] ?? []; foreach ($players as &$player) { if (!$player['alive']) { continue; } foreach ($state['explosions'] as $exp) { if ($exp['x'] === $player['x'] && $exp['y'] === $player['y']) { $player['alive'] = false; } } } unset($player); $state['players'] = $players; $alive = array_filter($players, fn($p) => $p['alive']); if ($room['status'] === 'playing' && count($alive) <= 1) { $room['status'] = 'finished'; $state['winner'] = $alive ? array_values($alive)[0]['name'] : null; } $state['last_tick'] = $now; $room['state'] = $state; return $room; } function compute_explosion_tiles(array $state, int $x, int $y, int $blast): array { $tiles = $state['map']['tiles']; $w = $state['map']['w']; $h = $state['map']['h']; $out = []; $pushTile = function (int $tx, int $ty, bool $breakableHit = false) use (&$out, $tiles) { $powerup = null; if ($breakableHit && mt_rand(1, 100) <= 20) { $types = ['blast', 'bombs', 'speed']; $powerup = $types[array_rand($types)]; } $out[] = [ 'x' => $tx, 'y' => $ty, 'tile_after' => $breakableHit ? 0 : $tiles[$ty][$tx], 'powerup' => $powerup ]; }; $pushTile($x, $y); $dirs = [[1,0],[-1,0],[0,1],[0,-1]]; foreach ($dirs as $dir) { for ($i = 1; $i <= $blast; $i++) { $tx = $x + $dir[0] * $i; $ty = $y + $dir[1] * $i; if ($tx < 0 || $ty < 0 || $tx >= $w || $ty >= $h) { break; } $tile = $tiles[$ty][$tx]; if ($tile === 1) { break; } if ($tile === 2) { $pushTile($tx, $ty, true); break; } $pushTile($tx, $ty); } } return $out; } function apply_powerup(array &$player, string $type): void { if ($type === 'blast') { $player['blast'] = min(5, $player['blast'] + 1); } elseif ($type === 'bombs') { $player['bombs'] = min(5, $player['bombs'] + 1); } elseif ($type === 'speed') { $player['speed'] = min(3, $player['speed'] + 1); } }