Игровая логика PvP: сервер (C++) + клиент (Quintus.js)
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья, технический проигрыш при дисконнекте. game.ready запускает матч, когда оба игрока в pvp-комнате готовы. Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты, рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space), экран результата матча. Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял Game изнутри его же метода — перенесено на uWS::Loop::defer.
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
#include "Game.hpp"
|
||||
|
||||
#include <App.h>
|
||||
#include <libusockets.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr double kTankHalf = 0.4;
|
||||
constexpr double kTankSpeed = 4.0; // тайлов/сек
|
||||
constexpr double kBulletSpeed = 8.0; // тайлов/сек
|
||||
constexpr double kBulletHalf = 0.08;
|
||||
constexpr int kFireCooldownTicks = 10; // 0.5с при 20 Гц
|
||||
constexpr double kDt = Game::kTickMs / 1000.0;
|
||||
} // namespace
|
||||
|
||||
Game::Game(Room &room, std::function<void()> onFinished) : onFinished_(std::move(onFinished)) {
|
||||
buildMap();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
Tank &t = tanks_[i];
|
||||
t.player_id = room.players[i].player_id;
|
||||
t.ws = room.players[i].ws;
|
||||
if (i == 0) {
|
||||
t.x = 1.5;
|
||||
t.y = kMapHeight - 2.5;
|
||||
t.dir = Dir::Up;
|
||||
} else {
|
||||
t.x = kMapWidth - 2.5;
|
||||
t.y = 1.5;
|
||||
t.dir = Dir::Down;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Game::~Game() {
|
||||
if (timer_) {
|
||||
us_timer_close(timer_);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::buildMap() {
|
||||
map_.assign(kMapHeight, std::vector<int>(kMapWidth, kEmpty));
|
||||
for (int y = 0; y < kMapHeight; y++) {
|
||||
for (int x = 0; x < kMapWidth; x++) {
|
||||
if (x == 0 || y == 0 || x == kMapWidth - 1 || y == kMapHeight - 1) {
|
||||
map_[y][x] = kSteel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Симметричные (180°) кластеры кирпича/стали в интерьере карты.
|
||||
auto placeMirrored = [this](int x, int y, int w, int h, int tile) {
|
||||
for (int dy = 0; dy < h; dy++) {
|
||||
for (int dx = 0; dx < w; dx++) {
|
||||
map_[y + dy][x + dx] = tile;
|
||||
map_[kMapHeight - 1 - (y + dy)][kMapWidth - 1 - (x + dx)] = tile;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
placeMirrored(3, 3, 2, 2, kBrick);
|
||||
placeMirrored(6, 3, 1, 2, kBrick);
|
||||
placeMirrored(3, 6, 2, 1, kSteel);
|
||||
placeMirrored(6, 6, 3, 3, kBrick);
|
||||
}
|
||||
|
||||
void Game::start() {
|
||||
timer_ = us_create_timer((us_loop_t *)uWS::Loop::get(), 0, sizeof(Game *));
|
||||
*(Game **)us_timer_ext(timer_) = this;
|
||||
us_timer_set(
|
||||
timer_,
|
||||
[](us_timer_t *t) {
|
||||
Game *self = *(Game **)us_timer_ext(t);
|
||||
self->tick();
|
||||
},
|
||||
kTickMs, kTickMs);
|
||||
}
|
||||
|
||||
void Game::handleInput(const std::string &player_id, const json &payload) {
|
||||
for (auto &t : tanks_) {
|
||||
if (t.player_id == player_id) {
|
||||
t.up = payload.value("up", false);
|
||||
t.down = payload.value("down", false);
|
||||
t.left = payload.value("left", false);
|
||||
t.right = payload.value("right", false);
|
||||
t.fire = payload.value("fire", false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Game::tileSolid(int tx, int ty) const {
|
||||
if (tx < 0 || ty < 0 || ty >= kMapHeight || tx >= kMapWidth) {
|
||||
return true;
|
||||
}
|
||||
return map_[ty][tx] != kEmpty;
|
||||
}
|
||||
|
||||
bool Game::rectHitsSolid(double x, double y) const {
|
||||
int minX = (int)std::floor(x - kTankHalf);
|
||||
int maxX = (int)std::floor(x + kTankHalf - 1e-6);
|
||||
int minY = (int)std::floor(y - kTankHalf);
|
||||
int maxY = (int)std::floor(y + kTankHalf - 1e-6);
|
||||
for (int ty = minY; ty <= maxY; ty++) {
|
||||
for (int tx = minX; tx <= maxX; tx++) {
|
||||
if (tileSolid(tx, ty)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Game::rectHitsTank(double x, double y, const Tank &self) const {
|
||||
for (const auto &t : tanks_) {
|
||||
if (&t == &self || !t.alive) {
|
||||
continue;
|
||||
}
|
||||
if (std::abs(x - t.x) < 2 * kTankHalf && std::abs(y - t.y) < 2 * kTankHalf) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Game::applyInput(Tank &tank) {
|
||||
if (!tank.alive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tank.fire_cooldown > 0) {
|
||||
tank.fire_cooldown--;
|
||||
}
|
||||
|
||||
Dir moveDir = tank.dir;
|
||||
bool moving = false;
|
||||
if (tank.up) {
|
||||
moveDir = Dir::Up;
|
||||
moving = true;
|
||||
} else if (tank.down) {
|
||||
moveDir = Dir::Down;
|
||||
moving = true;
|
||||
} else if (tank.left) {
|
||||
moveDir = Dir::Left;
|
||||
moving = true;
|
||||
} else if (tank.right) {
|
||||
moveDir = Dir::Right;
|
||||
moving = true;
|
||||
}
|
||||
|
||||
tank.dir = moveDir;
|
||||
|
||||
if (moving) {
|
||||
double nx = tank.x, ny = tank.y;
|
||||
double step = kTankSpeed * kDt;
|
||||
switch (moveDir) {
|
||||
case Dir::Up: ny -= step; break;
|
||||
case Dir::Down: ny += step; break;
|
||||
case Dir::Left: nx -= step; break;
|
||||
case Dir::Right: nx += step; break;
|
||||
}
|
||||
if (!rectHitsSolid(nx, ny) && !rectHitsTank(nx, ny, tank)) {
|
||||
tank.x = nx;
|
||||
tank.y = ny;
|
||||
}
|
||||
}
|
||||
|
||||
if (tank.fire) {
|
||||
fireBullet(tank);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::fireBullet(Tank &tank) {
|
||||
if (tank.fire_cooldown > 0) {
|
||||
return;
|
||||
}
|
||||
bool hasOwnBullet = std::any_of(bullets_.begin(), bullets_.end(),
|
||||
[&](const Bullet &b) { return b.owner_id == tank.player_id; });
|
||||
if (hasOwnBullet) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bullet b;
|
||||
b.id = next_bullet_id_++;
|
||||
b.owner_id = tank.player_id;
|
||||
b.dir = tank.dir;
|
||||
double offset = kTankHalf + kBulletHalf + 0.01;
|
||||
b.x = tank.x;
|
||||
b.y = tank.y;
|
||||
switch (tank.dir) {
|
||||
case Dir::Up: b.y -= offset; break;
|
||||
case Dir::Down: b.y += offset; break;
|
||||
case Dir::Left: b.x -= offset; break;
|
||||
case Dir::Right: b.x += offset; break;
|
||||
}
|
||||
bullets_.push_back(b);
|
||||
tank.fire_cooldown = kFireCooldownTicks;
|
||||
}
|
||||
|
||||
void Game::updateBullets() {
|
||||
double step = kBulletSpeed * kDt;
|
||||
|
||||
for (auto &b : bullets_) {
|
||||
switch (b.dir) {
|
||||
case Dir::Up: b.y -= step; break;
|
||||
case Dir::Down: b.y += step; break;
|
||||
case Dir::Left: b.x -= step; break;
|
||||
case Dir::Right: b.x += step; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Пуля против пули: встречные уничтожают друг друга.
|
||||
std::vector<bool> dead(bullets_.size(), false);
|
||||
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||
if (dead[i]) continue;
|
||||
for (size_t j = i + 1; j < bullets_.size(); j++) {
|
||||
if (dead[j] || bullets_[i].owner_id == bullets_[j].owner_id) continue;
|
||||
if (std::abs(bullets_[i].x - bullets_[j].x) < 2 * kBulletHalf &&
|
||||
std::abs(bullets_[i].y - bullets_[j].y) < 2 * kBulletHalf) {
|
||||
dead[i] = dead[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int loserIndex = -1;
|
||||
bool anyoneHit = false;
|
||||
|
||||
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||
if (dead[i]) continue;
|
||||
Bullet &b = bullets_[i];
|
||||
|
||||
int tx = (int)std::floor(b.x);
|
||||
int ty = (int)std::floor(b.y);
|
||||
if (tx < 0 || ty < 0 || tx >= kMapWidth || ty >= kMapHeight) {
|
||||
dead[i] = true;
|
||||
continue;
|
||||
}
|
||||
if (map_[ty][tx] == kSteel) {
|
||||
dead[i] = true;
|
||||
continue;
|
||||
}
|
||||
if (map_[ty][tx] == kBrick) {
|
||||
map_[ty][tx] = kEmpty;
|
||||
dead[i] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int ti = 0; ti < 2; ti++) {
|
||||
Tank &t = tanks_[ti];
|
||||
if (!t.alive || t.player_id == b.owner_id) continue;
|
||||
if (std::abs(b.x - t.x) < kTankHalf + kBulletHalf && std::abs(b.y - t.y) < kTankHalf + kBulletHalf) {
|
||||
dead[i] = true;
|
||||
t.lives--;
|
||||
anyoneHit = true;
|
||||
if (t.lives <= 0) {
|
||||
t.alive = false;
|
||||
loserIndex = ti;
|
||||
} else {
|
||||
// респаун на стартовой позиции
|
||||
t.x = (ti == 0) ? 1.5 : kMapWidth - 2.5;
|
||||
t.y = (ti == 0) ? kMapHeight - 2.5 : 1.5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Bullet> alive;
|
||||
for (size_t i = 0; i < bullets_.size(); i++) {
|
||||
if (!dead[i]) alive.push_back(bullets_[i]);
|
||||
}
|
||||
bullets_ = std::move(alive);
|
||||
|
||||
(void)anyoneHit;
|
||||
if (loserIndex != -1) {
|
||||
endGame(1 - loserIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::tick() {
|
||||
if (finished_) {
|
||||
return;
|
||||
}
|
||||
tick_count_++;
|
||||
|
||||
for (auto &t : tanks_) {
|
||||
applyInput(t);
|
||||
}
|
||||
updateBullets();
|
||||
|
||||
if (!finished_) {
|
||||
broadcastState();
|
||||
}
|
||||
}
|
||||
|
||||
json Game::dirToJson(Dir d) {
|
||||
switch (d) {
|
||||
case Dir::Up: return "up";
|
||||
case Dir::Down: return "down";
|
||||
case Dir::Left: return "left";
|
||||
case Dir::Right: return "right";
|
||||
}
|
||||
return "up";
|
||||
}
|
||||
|
||||
json Game::serializeTank(const Tank &t) const {
|
||||
return {
|
||||
{"id", t.player_id},
|
||||
{"x", t.x},
|
||||
{"y", t.y},
|
||||
{"direction", dirToJson(t.dir)},
|
||||
{"lives", t.lives},
|
||||
{"alive", t.alive},
|
||||
};
|
||||
}
|
||||
|
||||
json Game::serializeBullet(const Bullet &b) const {
|
||||
return {
|
||||
{"id", b.id},
|
||||
{"owner_id", b.owner_id},
|
||||
{"x", b.x},
|
||||
{"y", b.y},
|
||||
{"direction", dirToJson(b.dir)},
|
||||
};
|
||||
}
|
||||
|
||||
void Game::sendTo(WS *ws, const std::string &type, json payload) {
|
||||
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||
}
|
||||
|
||||
void Game::broadcastState() {
|
||||
json tanks = json::array();
|
||||
for (const auto &t : tanks_) tanks.push_back(serializeTank(t));
|
||||
json bullets = json::array();
|
||||
for (const auto &b : bullets_) bullets.push_back(serializeBullet(b));
|
||||
|
||||
json payload = {{"tick", tick_count_}, {"tanks", tanks}, {"bullets", bullets}};
|
||||
for (const auto &t : tanks_) {
|
||||
sendTo(t.ws, "game.state", payload);
|
||||
}
|
||||
}
|
||||
|
||||
json Game::buildStartPayload(const std::string &mode) const {
|
||||
json mapJson = json::array();
|
||||
for (const auto &row : map_) {
|
||||
mapJson.push_back(row);
|
||||
}
|
||||
json players = json::array();
|
||||
for (const auto &t : tanks_) {
|
||||
players.push_back({{"id", t.player_id}});
|
||||
}
|
||||
return {
|
||||
{"map", mapJson},
|
||||
{"mode", mode},
|
||||
{"players", players},
|
||||
{"tick_rate", 1000 / kTickMs},
|
||||
};
|
||||
}
|
||||
|
||||
void Game::handleDisconnect(WS *ws) {
|
||||
if (finished_) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (tanks_[i].ws == ws) {
|
||||
int winnerIndex = 1 - i;
|
||||
finished_ = true;
|
||||
if (timer_) {
|
||||
us_timer_close(timer_);
|
||||
timer_ = nullptr;
|
||||
}
|
||||
sendTo(tanks_[winnerIndex].ws, "game.over", {{"result", "win"}, {"reason", "соперник отключился"}});
|
||||
if (onFinished_) {
|
||||
onFinished_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Game::endGame(int winnerIndex) {
|
||||
finished_ = true;
|
||||
if (timer_) {
|
||||
us_timer_close(timer_);
|
||||
timer_ = nullptr;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
std::string result = (winnerIndex == -1) ? "draw" : (i == winnerIndex ? "win" : "lose");
|
||||
std::string reason = (winnerIndex == -1) ? "оба танка уничтожены одновременно"
|
||||
: (i == winnerIndex) ? "противник уничтожен"
|
||||
: "танк уничтожен";
|
||||
sendTo(tanks_[i].ws, "game.over", {{"result", result}, {"reason", reason}});
|
||||
}
|
||||
|
||||
if (onFinished_) {
|
||||
onFinished_();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "Lobby.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct us_timer_t;
|
||||
|
||||
// Authoritative-игра для одной комнаты в режиме pvp (этап 4 из TZ.md).
|
||||
// Coop с AI — этап 5, наследует эту же тик-логику.
|
||||
class Game {
|
||||
public:
|
||||
static constexpr int kTickMs = 50; // 20 Гц
|
||||
static constexpr int kMapWidth = 15;
|
||||
static constexpr int kMapHeight = 15;
|
||||
static constexpr int kStartLives = 3;
|
||||
|
||||
enum Tile { kEmpty = 0, kBrick = 1, kSteel = 2 };
|
||||
enum class Dir { Up, Right, Down, Left };
|
||||
|
||||
struct Tank {
|
||||
std::string player_id;
|
||||
WS *ws;
|
||||
double x, y;
|
||||
Dir dir = Dir::Up;
|
||||
int lives = kStartLives;
|
||||
int fire_cooldown = 0;
|
||||
bool up = false, down = false, left = false, right = false, fire = false;
|
||||
bool alive = true;
|
||||
};
|
||||
|
||||
struct Bullet {
|
||||
int id;
|
||||
std::string owner_id;
|
||||
double x, y;
|
||||
Dir dir;
|
||||
};
|
||||
|
||||
// room.players.size() должен быть == 2 на момент создания.
|
||||
Game(Room &room, std::function<void()> onFinished);
|
||||
~Game();
|
||||
|
||||
void start();
|
||||
void handleInput(const std::string &player_id, const json &payload);
|
||||
// Технический проигрыш отключившегося — второй игрок побеждает.
|
||||
void handleDisconnect(WS *ws);
|
||||
json buildStartPayload(const std::string &mode) const;
|
||||
|
||||
private:
|
||||
std::vector<std::vector<int>> map_;
|
||||
std::array<Tank, 2> tanks_;
|
||||
std::vector<Bullet> bullets_;
|
||||
int next_bullet_id_ = 1;
|
||||
int tick_count_ = 0;
|
||||
bool finished_ = false;
|
||||
us_timer_t *timer_ = nullptr;
|
||||
std::function<void()> onFinished_;
|
||||
|
||||
void buildMap();
|
||||
void tick();
|
||||
void applyInput(Tank &tank);
|
||||
void updateBullets();
|
||||
bool tileSolid(int tx, int ty) const;
|
||||
bool rectHitsSolid(double x, double y) const;
|
||||
bool rectHitsTank(double x, double y, const Tank &self) const;
|
||||
void fireBullet(Tank &tank);
|
||||
void endGame(int winnerIndex); // -1 = ничья
|
||||
void broadcastState();
|
||||
void sendTo(WS *ws, const std::string &type, json payload);
|
||||
|
||||
static json dirToJson(Dir d);
|
||||
json serializeTank(const Tank &t) const;
|
||||
json serializeBullet(const Bullet &b) const;
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "Lobby.hpp"
|
||||
#include "Game.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
Lobby::Lobby() = default;
|
||||
Lobby::~Lobby() = default;
|
||||
|
||||
void Lobby::send(WS *ws, const std::string &type, json payload) {
|
||||
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||
@@ -41,6 +45,7 @@ json Lobby::roomPlayers(const Room &room) const {
|
||||
{"id", room.players[i].player_id},
|
||||
{"nickname", room.players[i].nickname},
|
||||
{"slot", i},
|
||||
{"ready", room.players[i].ready},
|
||||
});
|
||||
}
|
||||
return players;
|
||||
@@ -69,6 +74,7 @@ void Lobby::onOpen(WS *ws) {
|
||||
}
|
||||
|
||||
void Lobby::onClose(WS *ws) {
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
sockets_.erase(ws);
|
||||
}
|
||||
@@ -96,6 +102,10 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
||||
handleJoinRoom(ws, payload);
|
||||
} else if (type == "lobby.leave_room") {
|
||||
handleLeaveRoom(ws);
|
||||
} else if (type == "game.ready") {
|
||||
handleGameReady(ws);
|
||||
} else if (type == "game.input") {
|
||||
handleGameInput(ws, payload);
|
||||
} else {
|
||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||
}
|
||||
@@ -197,10 +207,98 @@ void Lobby::handleLeaveRoom(WS *ws) {
|
||||
sendError(ws, "not_in_room", "you are not in a room");
|
||||
return;
|
||||
}
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
broadcastRoomsToLobby();
|
||||
}
|
||||
|
||||
void Lobby::handleGameReady(WS *ws) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
sendError(ws, "not_in_room", "you are not in a room");
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = rooms_.find(data->room_id);
|
||||
if (it == rooms_.end()) {
|
||||
return;
|
||||
}
|
||||
for (auto &p : it->second.players) {
|
||||
if (p.ws == ws) {
|
||||
p.ready = true;
|
||||
}
|
||||
}
|
||||
broadcastRoomUpdated(it->second);
|
||||
maybeStartGame(data->room_id);
|
||||
}
|
||||
|
||||
void Lobby::handleGameInput(WS *ws, const json &payload) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
return;
|
||||
}
|
||||
auto it = games_.find(data->room_id);
|
||||
if (it == games_.end()) {
|
||||
return;
|
||||
}
|
||||
it->second->handleInput(data->player_id, payload);
|
||||
}
|
||||
|
||||
void Lobby::maybeStartGame(const std::string &room_id) {
|
||||
auto it = rooms_.find(room_id);
|
||||
if (it == rooms_.end()) {
|
||||
return;
|
||||
}
|
||||
Room &room = it->second;
|
||||
if (room.mode != "pvp" || room.state != "waiting" || room.players.size() != kMaxPlayersPerRoom) {
|
||||
return;
|
||||
}
|
||||
bool allReady = std::all_of(room.players.begin(), room.players.end(),
|
||||
[](const RoomPlayer &p) { return p.ready; });
|
||||
if (!allReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
room.state = "playing";
|
||||
|
||||
auto game = std::make_unique<Game>(room, [this, room_id]() {
|
||||
auto rit = rooms_.find(room_id);
|
||||
if (rit != rooms_.end()) {
|
||||
rit->second.state = "waiting";
|
||||
for (auto &p : rit->second.players) {
|
||||
p.ready = false;
|
||||
}
|
||||
broadcastRoomUpdated(rit->second);
|
||||
}
|
||||
broadcastRoomsToLobby();
|
||||
// Мы всё ещё внутри вызова метода Game (tick/handleDisconnect), который
|
||||
// и вызвал этот колбэк — удалять объект прямо сейчас было бы use-after-free
|
||||
// на возврате из этого вызова. Откладываем erase на следующий тик луп'а.
|
||||
uWS::Loop::get()->defer([this, room_id]() { games_.erase(room_id); });
|
||||
});
|
||||
|
||||
json startPayload = game->buildStartPayload(room.mode);
|
||||
for (const auto &p : room.players) {
|
||||
send(p.ws, "game.start", startPayload);
|
||||
}
|
||||
game->start();
|
||||
games_[room_id] = std::move(game);
|
||||
|
||||
broadcastRoomsToLobby();
|
||||
}
|
||||
|
||||
void Lobby::endActiveGameIfAny(const std::string &room_id, WS *leavingWs) {
|
||||
if (room_id.empty()) {
|
||||
return;
|
||||
}
|
||||
auto it = games_.find(room_id);
|
||||
if (it == games_.end()) {
|
||||
return;
|
||||
}
|
||||
it->second->handleDisconnect(leavingWs);
|
||||
games_.erase(it);
|
||||
}
|
||||
|
||||
void Lobby::removePlayerFromRoom(WS *ws) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <App.h>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -17,10 +18,13 @@ struct PerSocketData {
|
||||
|
||||
using WS = uWS::WebSocket<false, true, PerSocketData>;
|
||||
|
||||
class Game;
|
||||
|
||||
struct RoomPlayer {
|
||||
std::string player_id;
|
||||
std::string nickname;
|
||||
WS *ws;
|
||||
bool ready = false;
|
||||
};
|
||||
|
||||
struct Room {
|
||||
@@ -33,6 +37,9 @@ struct Room {
|
||||
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
|
||||
class Lobby {
|
||||
public:
|
||||
Lobby();
|
||||
~Lobby();
|
||||
|
||||
void onOpen(WS *ws);
|
||||
void onMessage(WS *ws, std::string_view message);
|
||||
void onClose(WS *ws);
|
||||
@@ -41,6 +48,7 @@ private:
|
||||
static constexpr size_t kMaxPlayersPerRoom = 2;
|
||||
|
||||
std::unordered_map<std::string, Room> rooms_;
|
||||
std::unordered_map<std::string, std::unique_ptr<Game>> games_;
|
||||
std::unordered_set<WS *> sockets_;
|
||||
int next_player_id_ = 1;
|
||||
int next_room_id_ = 1;
|
||||
@@ -50,7 +58,11 @@ private:
|
||||
void handleCreateRoom(WS *ws, const json &payload);
|
||||
void handleJoinRoom(WS *ws, const json &payload);
|
||||
void handleLeaveRoom(WS *ws);
|
||||
void handleGameReady(WS *ws);
|
||||
void handleGameInput(WS *ws, const json &payload);
|
||||
void removePlayerFromRoom(WS *ws);
|
||||
void maybeStartGame(const std::string &room_id);
|
||||
void endActiveGameIfAny(const std::string &room_id, WS *leavingWs);
|
||||
|
||||
void broadcastRoomsToLobby();
|
||||
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
|
||||
|
||||
Reference in New Issue
Block a user