Игровая логика 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:
2026-07-28 12:05:01 +05:00
parent a9e4828dd9
commit 2d0be9c287
17 changed files with 6632 additions and 5 deletions
+77
View File
@@ -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;
};