game1 / site / index.html
  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 &amp; 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>

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game1.git