From a9d2a6b15c24f7bed017347526d5b70dbe5c0c24 Mon Sep 17 00:00:00 2001 From: Dmitry Gammel Date: Sun, 26 Jul 2026 19:31:44 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D1=80=D0=BA=D0=B0=D1=81=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0:=20=D0=A2=D0=97,=20nginx+g?= =?UTF-8?q?ameserver(uWebSockets)=20=D0=B2=20Docker,=20/ws=20echo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend — чистый HTML/CSS/JS без сборки (резюме, проекты, игра-заглушки). Gameserver — C++ на uWebSockets (CMake FetchContent), протокол hello/lobby.list_rooms/error проверен end-to-end. --- .gitignore | 1 + TZ.md | 140 +++++++++++++++++++++++++++++++ docker-compose.yml | 14 ++++ frontend/css/game.css | 1 + frontend/css/projects.css | 1 + frontend/css/resume.css | 1 + frontend/css/theme.css | 38 +++++++++ frontend/game.html | 19 +++++ frontend/index.html | 23 +++++ frontend/js/game/entities.js | 1 + frontend/js/game/lobby.js | 21 +++++ frontend/js/game/main.js | 4 + frontend/js/game/network.js | 38 +++++++++ frontend/js/game/protocol.js | 21 +++++ frontend/js/matrix-rain.js | 11 +++ frontend/js/projects.js | 1 + frontend/projects.html | 22 +++++ frontend/vendor/quintus/.gitkeep | 0 gameserver/CMakeLists.txt | 47 +++++++++++ gameserver/Dockerfile | 15 ++++ gameserver/src/main.cpp | 49 +++++++++++ nginx/Dockerfile | 4 + nginx/nginx.conf | 19 +++++ 23 files changed, 491 insertions(+) create mode 100644 .gitignore create mode 100644 TZ.md create mode 100644 docker-compose.yml create mode 100644 frontend/css/game.css create mode 100644 frontend/css/projects.css create mode 100644 frontend/css/resume.css create mode 100644 frontend/css/theme.css create mode 100644 frontend/game.html create mode 100644 frontend/index.html create mode 100644 frontend/js/game/entities.js create mode 100644 frontend/js/game/lobby.js create mode 100644 frontend/js/game/main.js create mode 100644 frontend/js/game/network.js create mode 100644 frontend/js/game/protocol.js create mode 100644 frontend/js/matrix-rain.js create mode 100644 frontend/js/projects.js create mode 100644 frontend/projects.html create mode 100644 frontend/vendor/quintus/.gitkeep create mode 100644 gameserver/CMakeLists.txt create mode 100644 gameserver/Dockerfile create mode 100644 gameserver/src/main.cpp create mode 100644 nginx/Dockerfile create mode 100644 nginx/nginx.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..619fca6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gameserver/build/ diff --git a/TZ.md b/TZ.md new file mode 100644 index 0000000..8433fd9 --- /dev/null +++ b/TZ.md @@ -0,0 +1,140 @@ +# ТЗ: cactoz.su + +## 1. Общее + +Личный сайт-визитка на домене `cactoz.su`. Три страницы, единая Matrix-стилистика. + +**Тема:** чёрный фон, зелёный и серебро, моноширинный шрифт (JetBrains Mono / Share Tech Mono), строго и технично — без лишних декоративных элементов. + +**Ник `cacto` (кактус) — сквозной мотив:** +- Пиксельный кактус (16×16 или 32×32 спрайт, зелёный/серебро на чёрном) как лого в шапке и favicon. +- ASCII-арт кактуса в hero-блоке главной страницы. +- Терминальные префиксы вместо обычных заголовков/навигации: `cacto@matrix:~$ whoami`, `cacto@matrix:~$ ls projects/`, `cacto@matrix:~$ ./battlecity`. +- Названия комнат в лобби генерируются в формате `cactus-room-01`, `cactus-room-02`, ... +- Характер, а не просто ник: живучесть, минимум ресурсов, "колючий" стиль текста в отдельных местах (например, error-сообщения). + +## 2. Страницы + +| # | Файл | Назначение | +|---|------|------------| +| 1 | `index.html` | Резюме — PHP/Go backend, опыт КИПиА/АСУТП | +| 2 | `projects.html` | Карточки проектов со ссылками на репозитории | +| 3 | `game.html` | BattleCity: лобби + игра | + +## 3. Структура проекта + +``` +cactoz.su/ +├── docker-compose.yml +├── nginx/ +│ ├── Dockerfile +│ └── nginx.conf +├── frontend/ # чистый HTML/CSS/JS, без сборки +│ ├── index.html +│ ├── projects.html +│ ├── game.html +│ ├── favicon.ico +│ ├── css/ +│ │ ├── theme.css # переменные темы, общий сброс +│ │ ├── resume.css +│ │ ├── projects.css +│ │ └── game.css +│ ├── js/ +│ │ ├── matrix-rain.js # canvas-эффект цифрового дождя +│ │ ├── projects.js # рендер карточек проектов из data +│ │ └── game/ +│ │ ├── network.js # обёртка над WebSocket, очередь сообщений +│ │ ├── lobby.js # UI лобби: список комнат, создание/вход +│ │ ├── protocol.js # константы типов сообщений, (де)сериализация +│ │ ├── entities.js # клиентские представления Tank/Bullet/Wall для Quintus +│ │ └── main.js # инициализация Quintus, игровой цикл рендера +│ └── vendor/ +│ └── quintus/ # Quintus.js, подключается напрямую + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..30cc291 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + cacto — Гаммель Дмитрий + + + + + +
+ +

whoami

+

Гаммель Дмитрий Викторович — PHP/Go backend, опыт КИПиА/АСУТП

+ +
+ + + diff --git a/frontend/js/game/entities.js b/frontend/js/game/entities.js new file mode 100644 index 0000000..8e904d4 --- /dev/null +++ b/frontend/js/game/entities.js @@ -0,0 +1 @@ +// TODO: клиентские представления Tank/Bullet/Wall для Quintus — этапы 4-5 diff --git a/frontend/js/game/lobby.js b/frontend/js/game/lobby.js new file mode 100644 index 0000000..f90c003 --- /dev/null +++ b/frontend/js/game/lobby.js @@ -0,0 +1,21 @@ +// TODO: полноценный UI лобби (список/создание/присоединение комнат) — этап 3. +// Пока — проверка связки клиент <-> gameserver (hello -> lobby.list_rooms). +import { Network, ServerMessage } from './network.js'; + +export function connectAndProbe() { + const net = new Network(); + + net.on('_open', () => net.hello('cacto')); + net.on(ServerMessage.HELLO_ACK, () => { + console.log('[gameserver] hello.ack received'); + net.listRooms(); + }); + net.on(ServerMessage.LOBBY_ROOMS, (payload) => { + console.log('[gameserver] rooms:', payload.rooms); + }); + net.on(ServerMessage.ERROR, (payload) => { + console.error('[gameserver] error:', payload); + }); + + return net; +} diff --git a/frontend/js/game/main.js b/frontend/js/game/main.js new file mode 100644 index 0000000..107031b --- /dev/null +++ b/frontend/js/game/main.js @@ -0,0 +1,4 @@ +// TODO: инициализация Quintus и игрового цикла рендера — этапы 4-5 +import { connectAndProbe } from './lobby.js'; + +connectAndProbe(); diff --git a/frontend/js/game/network.js b/frontend/js/game/network.js new file mode 100644 index 0000000..fed83f2 --- /dev/null +++ b/frontend/js/game/network.js @@ -0,0 +1,38 @@ +import { ClientMessage, ServerMessage } from './protocol.js'; + +export class Network { + constructor() { + const proto = location.protocol === 'https:' ? 'wss' : 'ws'; + this.socket = new WebSocket(`${proto}://${location.host}/ws`); + this.handlers = new Map(); + + this.socket.addEventListener('open', () => this._emit('_open')); + this.socket.addEventListener('close', () => this._emit('_close')); + this.socket.addEventListener('message', (event) => { + const msg = JSON.parse(event.data); + this._emit(msg.type, msg.payload); + }); + } + + on(type, handler) { + this.handlers.set(type, handler); + } + + _emit(type, payload) { + this.handlers.get(type)?.(payload); + } + + send(type, payload = {}) { + this.socket.send(JSON.stringify({ type, payload })); + } + + hello(nickname) { + this.send(ClientMessage.HELLO, { nickname }); + } + + listRooms() { + this.send(ClientMessage.LOBBY_LIST_ROOMS); + } +} + +export { ServerMessage }; diff --git a/frontend/js/game/protocol.js b/frontend/js/game/protocol.js new file mode 100644 index 0000000..bc7ebcf --- /dev/null +++ b/frontend/js/game/protocol.js @@ -0,0 +1,21 @@ +// Типы сообщений из TZ.md, раздел 5. +export const ClientMessage = { + HELLO: 'hello', + LOBBY_LIST_ROOMS: 'lobby.list_rooms', + LOBBY_CREATE_ROOM: 'lobby.create_room', + LOBBY_JOIN_ROOM: 'lobby.join_room', + LOBBY_LEAVE_ROOM: 'lobby.leave_room', + GAME_READY: 'game.ready', + GAME_INPUT: 'game.input', +}; + +export const ServerMessage = { + HELLO_ACK: 'hello.ack', + ERROR: 'error', + LOBBY_ROOMS: 'lobby.rooms', + LOBBY_ROOM_JOINED: 'lobby.room_joined', + LOBBY_ROOM_UPDATED: 'lobby.room_updated', + GAME_START: 'game.start', + GAME_STATE: 'game.state', + GAME_OVER: 'game.over', +}; diff --git a/frontend/js/matrix-rain.js b/frontend/js/matrix-rain.js new file mode 100644 index 0000000..39ce165 --- /dev/null +++ b/frontend/js/matrix-rain.js @@ -0,0 +1,11 @@ +// TODO: полноценный canvas-эффект цифрового дождя — этап 2 +const canvas = document.getElementById('matrix-rain'); +if (canvas) { + const ctx = canvas.getContext('2d'); + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + resize(); + window.addEventListener('resize', resize); +} diff --git a/frontend/js/projects.js b/frontend/js/projects.js new file mode 100644 index 0000000..b439d30 --- /dev/null +++ b/frontend/js/projects.js @@ -0,0 +1 @@ +// TODO: рендер карточек проектов из данных — этап 2 diff --git a/frontend/projects.html b/frontend/projects.html new file mode 100644 index 0000000..378c96d --- /dev/null +++ b/frontend/projects.html @@ -0,0 +1,22 @@ + + + + + + ls projects/ — cacto + + + + +
+ +

ls projects/

+ + +
+ + + diff --git a/frontend/vendor/quintus/.gitkeep b/frontend/vendor/quintus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gameserver/CMakeLists.txt b/gameserver/CMakeLists.txt new file mode 100644 index 0000000..d2566eb --- /dev/null +++ b/gameserver/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.16) +project(cactoz_gameserver CXX C) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +FetchContent_Declare( + uwebsockets + GIT_REPOSITORY https://github.com/uNetworking/uWebSockets.git + GIT_TAG v20.79.0 +) +FetchContent_MakeAvailable(uwebsockets) + +# uWebSockets pulls uSockets in via a git submodule, which plain FetchContent +# does not check out. Fetch the exact commit uWebSockets v20.79.0 pins instead. +FetchContent_Declare( + usockets_src + GIT_REPOSITORY https://github.com/uNetworking/uSockets.git + GIT_TAG 86097c490263ab662d62e8e7b541390bdec7d149 +) +FetchContent_MakeAvailable(usockets_src) + +FetchContent_Declare( + json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 +) +FetchContent_MakeAvailable(json) + +file(GLOB USOCKETS_SOURCES + ${usockets_src_SOURCE_DIR}/src/*.c + ${usockets_src_SOURCE_DIR}/src/eventing/*.c +) +add_library(usockets STATIC ${USOCKETS_SOURCES}) +target_include_directories(usockets PUBLIC ${usockets_src_SOURCE_DIR}/src) +target_compile_definitions(usockets PUBLIC LIBUS_NO_SSL LIBUS_USE_EPOLL) + +add_executable(gameserver + src/main.cpp +) +target_include_directories(gameserver PRIVATE + ${uwebsockets_SOURCE_DIR}/src + src +) +target_link_libraries(gameserver PRIVATE usockets nlohmann_json::nlohmann_json pthread z) diff --git a/gameserver/Dockerfile b/gameserver/Dockerfile new file mode 100644 index 0000000..701566d --- /dev/null +++ b/gameserver/Dockerfile @@ -0,0 +1,15 @@ +FROM debian:bookworm AS build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake git ca-certificates zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY CMakeLists.txt ./ +COPY src ./src +RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j"$(nproc)" + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends zlib1g \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/build/gameserver /usr/local/bin/gameserver +EXPOSE 9001 +CMD ["gameserver"] diff --git a/gameserver/src/main.cpp b/gameserver/src/main.cpp new file mode 100644 index 0000000..c9990bb --- /dev/null +++ b/gameserver/src/main.cpp @@ -0,0 +1,49 @@ +// Каркас (этап 1 из TZ.md): проверка связки nginx <-ws-> gameserver. +// Лобби/комнаты/игровая логика — следующие этапы. + +#include +#include +#include + +using json = nlohmann::json; + +struct PerSocketData {}; + +int main() { + uWS::App() + .ws("/ws", { + .open = [](auto *ws) { + std::cout << "client connected\n"; + }, + .message = [](auto *ws, std::string_view message, uWS::OpCode) { + json response; + try { + json request = json::parse(message); + std::string type = request.value("type", ""); + + if (type == "hello") { + response = {{"type", "hello.ack"}, {"payload", {{"player_id", "stub"}}}}; + } else if (type == "lobby.list_rooms") { + response = {{"type", "lobby.rooms"}, {"payload", {{"rooms", json::array()}}}}; + } else { + response = {{"type", "error"}, + {"payload", {{"code", "unknown_type"}, {"message", "unknown message type: " + type}}}}; + } + } catch (const std::exception &e) { + response = {{"type", "error"}, {"payload", {{"code", "bad_json"}, {"message", e.what()}}}}; + } + ws->send(response.dump(), uWS::OpCode::TEXT); + }, + .close = [](auto *ws, int, std::string_view) { + std::cout << "client disconnected\n"; + }, + }) + .listen(9001, [](auto *token) { + if (token) { + std::cout << "gameserver listening on :9001\n"; + } else { + std::cerr << "failed to listen on :9001\n"; + } + }) + .run(); +} diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..525bde2 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.27-alpine +COPY frontend/ /usr/share/nginx/html/ +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..8657086 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name cactoz.su www.cactoz.su; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + location /ws { + proxy_pass http://gameserver:9001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + } +}