39001-vm/includes/rooms.php
2026-03-05 10:39:31 +00:00

337 lines
9.8 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
function ensure_rooms_schema(): void {
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS rooms (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'waiting',
max_players INT NOT NULL DEFAULT 4,
state_json LONGTEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL;
db()->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);
}
}