1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>neon-grid — p2p light-cycle arena</title>
<style>
:root {
--bg: #05060f; --panel: #0b0e1f; --panel-2: #10142a; --border: #1a2140;
--text: #d8e6ff; --muted: #6b7aa8; --cyan: #00f0ff; --pink: #ff3ec8;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--text);
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 40px 20px 80px;
}
main { max-width: 780px; margin: 0 auto; }
h1 { font-size: 34px; letter-spacing: 4px; color: var(--cyan); text-shadow: 0 0 18px var(--cyan); }
h1 .p { color: var(--pink); text-shadow: 0 0 18px var(--pink); }
.tag { color: var(--muted); margin: 6px 0 30px; font-size: 17px; }
h2 { margin: 44px 0 12px; font-size: 22px; color: var(--cyan); }
h3 { margin: 24px 0 8px; font-size: 17px; }
p, li { color: #b9c6dd; }
ul { padding-left: 22px; margin: 10px 0; }
li { margin: 5px 0; }
a { color: var(--cyan); }
pre {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 14px 16px; overflow-x: auto; margin: 12px 0;
font: 13.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px;
background: var(--panel-2); border-radius: 5px; padding: 1px 6px; }
pre code { background: none; padding: 0; }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 18px 20px; margin: 14px 0;
}
.btns { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 6px; }
.btn {
display: inline-block; padding: 10px 18px; border-radius: 8px; text-decoration: none;
background: #0a1428; border: 1px solid #14406a; color: var(--cyan);
letter-spacing: 1px; font-size: 14px;
}
.btn:hover { background: #10305a; box-shadow: 0 0 14px #00f0ff44; }
footer { margin-top: 60px; color: var(--muted); font-size: 13px; }
.langs {
position: fixed; top: 12px; right: 14px; color: var(--muted); font-size: 13px;
letter-spacing: .06em; user-select: none;
}
.langs button {
font: inherit; color: inherit; background: none; border: 0; padding: 3px 4px;
cursor: pointer; opacity: .55;
}
.langs button.on { color: var(--cyan); opacity: 1; }
</style>
</head>
<body>
<nav class="langs" id="langs"></nav>
<main data-lang-block lang="en">
<h1>NEON<span class="p">GRID</span></h1>
<p class="tag">a serverless p2p light-cycle arena — this page is also the git repo.</p>
<div class="btns">
<a class="btn" href="https://game1.ardegazu.ro/">ride the grid</a>
<a class="btn" href="browse/index.html">browse the code</a>
<a class="btn" href="#clone">clone the repo</a>
</div>
<h2 id="clone">Clone</h2>
<p>This site serves the repository over git's dumb-HTTP protocol as plain static
files (plain files — there is no git server here, or any server at all):</p>
<pre><code>git clone https://git.ardegazu.ro/game1.git
cd neon-grid</code></pre>
<h2>What it is</h2>
<p>Tron-style multiplayer for up to 16 riders. Everyone rides a light cycle on a
shared 128×96 grid, leaves a solid trail, and the last rider alive wins the
round; first to 5 rounds takes the match. Installable PWA — works saved to an
iOS home screen, safe-area aware, tap-to-steer on phones.</p>
<h2>How it works</h2>
<ul>
<li><strong>No game server.</strong> Browsers are
<a href="https://libp2p.io">libp2p</a> peers: they meet through a
circuit-relay-v2 node, discover each other via gossipsub, and upgrade to
direct WebRTC. The room secret lives in the URL fragment (<code>#…</code>)
and never leaves the browser.</li>
<li><strong>Everything is end-to-end encrypted.</strong> Every peer pair is
noise-encrypted end-to-end (even while frames still cross the relay), and
room membership is proved with an AES-256-GCM sealed hello derived from
the room secret — an undecryptable hello means you're not in the room.
TURN with ephemeral credentials covers the stubborn NATs.</li>
<li><strong>One peer is the referee.</strong> The lowest peer id runs a fixed-rate
authoritative grid simulation and broadcasts ~40-byte tick deltas; everyone
else renders from them. Host loss mid-round voids the round and the next
lowest id takes over; late joiners get a run-length-encoded snapshot and
spectate until the next round.</li>
<li><strong>Hosting without an origin server.</strong> The client is a static
Vite build served as plain files — there is no backend anywhere. This page
you're reading works the same way.</li>
</ul>
<h2>What's in the repo</h2>
<div class="card">
<h3>client/src/game/ — the game</h3>
<p>TypeScript, no framework: typed wire protocol, host simulation, canvas
renderer (neon trails, derez, particles), WebAudio bleeps, lobby UI.</p>
</div>
<div class="card">
<h3>client/src/net/ — the p2p layer</h3>
<p>A thin js-libp2p assembly: relay bootstrap from
<code>/.well-known/ap2p</code>, a room-scoped protocol with a sealed-hello
membership handshake, reconnection, and the six-method surface the game
consumes. Swap the protocol prefix and it drives any turn/tick game.</p>
</div>
<div class="card">
<h3>deploy/ — the publish pipeline</h3>
<p><code>publish-repo.sh</code> builds this very site: landing page + bare
mirror exploded to loose objects (static file servers and git's dumb-HTTP protocol
agree on "every path must exist"), with an anonymity gate that refuses to
publish identity-bearing bytes.</p>
</div>
<h2>Run it yourself</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# open two tabs on the same #room URL and play yourself</code></pre>
<p>Deploy: <code>npm run build</code>, then publish <code>client/dist/</code> as
static files anywhere — IPFS, a CDN, a folder behind nginx. It needs nothing but
a libp2p circuit relay it's allowed to talk to (see <code>src/game/config.ts</code>).</p>
<h2>Guarantees & limits, honestly</h2>
<ul>
<li>The relay operator sees connection metadata, opaque room ids, and frame
sizes/timing. Not content, not nicknames, not who won.</li>
<li>Anyone with the room link can join and play — capability model. The
practical ceiling is ~12 riders (full mesh).</li>
<li>The host referee is one of the players; a cheating host could lie about
the simulation. Between friends that's the same trust as hosting a LAN
party. Inputs from non-host players can't forge anyone else's moves.</li>
<li>MIT licensed. Built on WebRTC, WebCrypto and canvas.</li>
</ul>
<footer>
Live game: <a href="https://game1.ardegazu.ro/">game1.ardegazu.ro</a> ·
this repo: <code>git clone https://git.ardegazu.ro/game1.git</code> ·
built on <a href="https://libp2p.io">js-libp2p</a>
</footer>
</main>
<main data-lang-block lang="ro" hidden>
<h1>NEON<span class="p">GRID</span></h1>
<p class="tag">o arenă p2p de motociclete de lumină, fără server — pagina asta e și repo-ul git.</p>
<div class="btns">
<a class="btn" href="https://game1.ardegazu.ro/">intră în grilă</a>
<a class="btn" href="browse/index.html">răsfoiește codul</a>
<a class="btn" href="#clone-ro">clonează repo-ul</a>
</div>
<h2 id="clone-ro">Clonare</h2>
<p>Situl servește repository-ul prin protocolul dumb-HTTP al lui git, ca simple
fișiere statice (chiar simple fișiere — nu există aici niciun server git, de fapt
niciun server):</p>
<pre><code>git clone https://git.ardegazu.ro/game1.git
cd neon-grid</code></pre>
<h2>Ce este</h2>
<p>Multiplayer în stil Tron pentru până la 16 piloți. Fiecare conduce o motocicletă
de lumină pe o grilă comună de 128×96, lasă în urmă un zid solid, iar ultimul rămas
în viață câștigă runda; primul la 5 runde ia meciul. PWA instalabil — merge salvat
pe ecranul de start al unui iPhone, respectă safe-area, se cârmește prin atingere
pe telefon.</p>
<h2>Cum funcționează</h2>
<ul>
<li><strong>Fără server de joc.</strong> Browserele sunt peers
<a href="https://libp2p.io">libp2p</a>: se întâlnesc printr-un nod
circuit-relay-v2, se descoperă prin gossipsub și trec pe WebRTC direct.
Secretul camerei stă în fragmentul URL-ului (<code>#…</code>) și nu pleacă
niciodată din browser.</li>
<li><strong>Totul e criptat cap-la-cap.</strong> Fiecare pereche de peers e
criptată noise de la un capăt la altul (chiar și cât timp cadrele mai trec
prin releu), iar apartenența la cameră se dovedește cu un hello sigilat
AES-256-GCM derivat din secretul camerei — un hello care nu se poate
decripta înseamnă că nu ești în cameră. TURN cu credențiale efemere acoperă
NAT-urile încăpățânate.</li>
<li><strong>Un peer e arbitrul.</strong> Cel mai mic peer id rulează simularea
autoritativă a grilei la rată fixă și difuzează delte de ~40 de octeți per
tick; restul doar randează. Dacă gazda pică în mijlocul rundei, runda se
anulează și preia următorul id; cine intră târziu primește un snapshot
comprimat RLE și privește până la runda următoare.</li>
<li><strong>Găzduire fără server de origine.</strong> Clientul e un build Vite
static servit ca simple fișiere — nu există backend nicăieri. Pagina pe care
o citești funcționează la fel.</li>
</ul>
<h2>Ce e în repo</h2>
<div class="card">
<h3>client/src/game/ — jocul</h3>
<p>TypeScript, fără framework: protocol de rețea tipat, simulare la gazdă,
renderer pe canvas (dâre de neon, derez, particule), bipuri WebAudio, UI de
lobby.</p>
</div>
<div class="card">
<h3>client/src/net/ — stratul p2p</h3>
<p>Un asamblaj subțire de js-libp2p: bootstrap de releu din
<code>/.well-known/ap2p</code>, un protocol per cameră cu handshake de
apartenență prin hello sigilat, reconectare, și cele șase metode pe care le
consumă jocul. Schimbi prefixul de protocol și duce orice joc pe tick-uri.</p>
</div>
<div class="card">
<h3>deploy/ — conducta de publicare</h3>
<p><code>publish-repo.sh</code> construiește chiar situl ăsta: pagină de
prezentare + oglindă bare explodată în loose objects (serverele de fișiere
statice și dumb-HTTP-ul lui git cad de acord că „orice cale trebuie să
existe"), cu o poartă de anonimat care refuză să publice octeți care poartă
identitate.</p>
</div>
<h2>Rulează-l singur</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# deschide două taburi pe același URL #cameră și joacă cu tine însuți</code></pre>
<p>Deploy: <code>npm run build</code>, apoi publică <code>client/dist/</code> ca
fișiere statice oriunde — IPFS, un CDN, un director în spatele lui nginx. N-are
nevoie decât de un releu libp2p cu care are voie să vorbească (vezi
<code>src/game/config.ts</code>).</p>
<h2>Garanții și limite, pe bune</h2>
<ul>
<li>Operatorul releului vede metadate de conexiune, id-uri opace de cameră și
dimensiuni/ritm de cadre. Nu conținut, nu nickname-uri, nu cine a câștigat.</li>
<li>Oricine are linkul camerei poate intra și juca — model de capabilitate.
Plafonul practic e ~12 piloți (mesh complet).</li>
<li>Arbitrul-gazdă e unul dintre jucători; o gazdă care trișează poate minți
despre simulare. Între prieteni e aceeași încredere ca la o petrecere LAN.
Inputurile celorlalți nu pot falsifica mișcările nimănui.</li>
<li>Licență MIT. Construit pe WebRTC, WebCrypto și canvas.</li>
</ul>
<footer>
Jocul live: <a href="https://game1.ardegazu.ro/">game1.ardegazu.ro</a> ·
repo-ul: <code>git clone https://git.ardegazu.ro/game1.git</code> ·
construit pe <a href="https://libp2p.io">js-libp2p</a>
</footer>
</main>
<main data-lang-block lang="hu" hidden>
<h1>NEON<span class="p">GRID</span></h1>
<p class="tag">szerver nélküli p2p fénymotor-aréna — ez az oldal egyben a git repó is.</p>
<div class="btns">
<a class="btn" href="https://game1.ardegazu.ro/">fel a rácsra</a>
<a class="btn" href="browse/index.html">böngészd a kódot</a>
<a class="btn" href="#clone-hu">repó klónozása</a>
</div>
<h2 id="clone-hu">Klónozás</h2>
<p>Az oldal a repót a git dumb-HTTP protokollján át szolgálja ki, sima statikus
fájlokként (tényleg sima fájlok — nincs itt git szerver, sőt semmilyen szerver):</p>
<pre><code>git clone https://git.ardegazu.ro/game1.git
cd neon-grid</code></pre>
<h2>Mi ez</h2>
<p>Tron-stílusú többjátékos akár 16 versenyzőnek. Mindenki egy fénymotort vezet
a közös 128×96-os rácson, tömör falat húz maga után, és az utolsó életben maradt
nyeri a kört; aki előbb ér 5 körig, viszi a meccset. Telepíthető PWA — iOS
kezdőképernyőre mentve is megy, safe-area-tudatos, telefonon koppintással
kanyarodsz.</p>
<h2>Hogyan működik</h2>
<ul>
<li><strong>Nincs játékszerver.</strong> A böngészők
<a href="https://libp2p.io">libp2p</a> peerek: egy circuit-relay-v2 node-on
át találkoznak, gossipsubbal fedezik fel egymást, majd közvetlen WebRTC-re
váltanak. A szoba titka az URL fragmentben él (<code>#…</code>), és soha
nem hagyja el a böngészőt.</li>
<li><strong>Minden végponttól végpontig titkosított.</strong> Minden peer-pár
noise-titkosított végponttól végpontig (akkor is, amíg a keretek még a
relén mennek át), a szobatagságot pedig a szoba titkából származtatott
AES-256-GCM lepecsételt hello bizonyítja — a megfejthetetlen hello annyit
tesz: nem vagy a szobában. A makacs NAT-okat efemer hitelesítésű TURN
fedi le.</li>
<li><strong>Az egyik peer a bíró.</strong> A legkisebb peer id futtatja a
mérvadó rácsszimulációt fix ütemben, és ~40 bájtos tick-deltákat sugároz;
a többiek ezekből rajzolnak. Ha a házigazda kör közben esik ki, a kör
érvénytelen, és a következő legkisebb id veszi át; a későn érkezők
RLE-tömörített pillanatképet kapnak, és a következő körig nézők.</li>
<li><strong>Hosztolás origin-szerver nélkül.</strong> A kliens statikus Vite
build, sima fájlokként kiszolgálva — nincs sehol backend. Ez az oldal is
pontosan így működik.</li>
</ul>
<h2>Mi van a repóban</h2>
<div class="card">
<h3>client/src/game/ — a játék</h3>
<p>TypeScript, framework nélkül: típusos wire-protokoll, hoszt-szimuláció,
canvas renderer (neoncsíkok, derez, részecskék), WebAudio pittyegés,
lobbi UI.</p>
</div>
<div class="card">
<h3>client/src/net/ — a p2p réteg</h3>
<p>Vékony js-libp2p összeállítás: relé-bootstrap a
<code>/.well-known/ap2p</code>-ből, szoba-hatókörű protokoll lepecsételt
hello tagsági kézfogással, újracsatlakozás, és az a hat metódus, amit a
játék fogyaszt. Cseréld ki a protokoll-előtagot, és bármilyen tick-alapú
játékot elvisz.</p>
</div>
<div class="card">
<h3>deploy/ — a publikáló futószalag</h3>
<p>A <code>publish-repo.sh</code> építi ezt az oldalt: nyitólap + bare tükör
loose objectekre bontva (a statikus fájlszerverek és a git dumb-HTTP-je
egyetértenek abban, hogy „minden útvonalnak léteznie kell"), és egy
anonimitás-kapu, amely nem enged identitáshordozó bájtokat publikálni.</p>
</div>
<h2>Futtasd magad</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# nyiss két fület ugyanarra a #szoba URL-re, és játssz magad ellen</code></pre>
<p>Telepítés: <code>npm run build</code>, aztán tedd ki a
<code>client/dist/</code>-et statikus fájlokként bárhová — IPFS, CDN, egy mappa
nginx mögött. Semmi másra nincs szüksége, csak egy libp2p relére, amivel
beszélhet (lásd <code>src/game/config.ts</code>).</p>
<h2>Garanciák és korlátok, őszintén</h2>
<ul>
<li>A relé üzemeltetője kapcsolati metaadatokat, átlátszatlan szoba-id-kat és
keretméretet/ütemet lát. Tartalmat, beceneveket, győztest nem.</li>
<li>Bárki, akinél ott a szoba linkje, beléphet és játszhat — capability
modell. A gyakorlati plafon ~12 versenyző (teljes mesh).</li>
<li>A bíró-házigazda az egyik játékos; egy csaló házigazda hazudhat a
szimulációról. Barátok közt ez ugyanaz a bizalom, mint egy LAN-partin.
A többiek inputjai senki más lépéseit nem hamisíthatják.</li>
<li>MIT licenc. WebRTC-re, WebCryptóra és canvasra épül.</li>
</ul>
<footer>
Az élő játék: <a href="https://game1.ardegazu.ro/">game1.ardegazu.ro</a> ·
a repó: <code>git clone https://git.ardegazu.ro/game1.git</code> ·
<a href="https://libp2p.io">js-libp2p</a>-re építve
</footer>
</main>
<script>
(function () {
var LANGS = ["en", "ro", "hu"];
var KEY = "ardegazu-site:lang";
var lang = null;
try { lang = localStorage.getItem(KEY); } catch (e) {}
if (LANGS.indexOf(lang) < 0) {
var cands = navigator.languages || [navigator.language];
for (var i = 0; i < cands.length; i++) {
var p = (cands[i] || "").slice(0, 2).toLowerCase();
if (LANGS.indexOf(p) >= 0) { lang = p; break; }
}
}
if (LANGS.indexOf(lang) < 0) lang = "en";
var nav = document.getElementById("langs");
function apply(l) {
lang = l;
document.documentElement.lang = l;
try { localStorage.setItem(KEY, l); } catch (e) {}
var blocks = document.querySelectorAll("[data-lang-block]");
for (var i = 0; i < blocks.length; i++) blocks[i].hidden = blocks[i].getAttribute("lang") !== l;
var btns = nav.querySelectorAll("button");
for (var j = 0; j < btns.length; j++) btns[j].className = btns[j].textContent === l ? "on" : "";
}
LANGS.forEach(function (l, i) {
if (i) nav.appendChild(document.createTextNode(" · "));
var b = document.createElement("button");
b.type = "button";
b.textContent = l;
b.addEventListener("click", function () { apply(l); });
nav.appendChild(b);
});
apply(lang);
})();
</script>
</body>
</html>
|