337 lines
9.8 KiB
PHP
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);
|
|
}
|
|
}
|