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 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>valley-blocks — serene p2p tetris</title>
<style>
:root {
--sky-a: #ffe3c8; --sky-b: #e2725b; --ink: #4a3853; --ink-dim: #7d6a86;
--panel: #fff6ec; --panel-2: #fdeee0; --border: #ecd9c6;
--teal: #7fc7c4; --gold: #f2c26b; --plum: #c589bf; --blue: #6f9fd8;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(180deg, var(--sky-a) 0%, var(--sky-b) 130%);
color: var(--ink); min-height: 100vh;
font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 48px 20px 80px;
}
main { max-width: 780px; margin: 0 auto; }
.sun { width: 74px; height: 74px; border-radius: 50%; background: #fff6ec; opacity: .9;
margin-bottom: -30px; box-shadow: 0 0 60px #fff6ecaa; }
h1 { font-size: 40px; font-weight: 800; letter-spacing: 1px; }
h1 .blocks span { display: inline-block; border-radius: 6px; padding: 0 10px; color: #fff; }
h1 .blocks span:nth-child(1) { background: var(--teal); }
h1 .blocks span:nth-child(2) { background: var(--gold); margin-left: 4px; }
.tag { color: var(--ink-dim); margin: 8px 0 28px; font-size: 17px; }
h2 { margin: 44px 0 12px; font-size: 22px; }
h3 { margin: 24px 0 8px; font-size: 17px; }
p, li { color: #5d4b66; }
ul { padding-left: 22px; margin: 10px 0; }
li { margin: 6px 0; }
a { color: #a4574a; }
pre {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 14px 16px; overflow-x: auto; margin: 12px 0; color: var(--ink);
font: 13.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px;
background: #fff6ec99; border-radius: 5px; padding: 1px 6px; }
pre code { background: none; padding: 0; }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
padding: 18px 20px; margin: 14px 0;
}
.btns { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 6px; }
.btn {
display: inline-block; padding: 11px 20px; border-radius: 999px; text-decoration: none;
background: var(--ink); color: #fff6ec; font-weight: 700; font-size: 14px;
letter-spacing: .4px; border: none;
}
.btn.alt { background: var(--panel); color: var(--ink); border: 1px solid var(--border); }
.btn:hover { transform: translateY(-1px); }
footer { margin-top: 60px; color: var(--ink-dim); font-size: 13px; }
.langs {
position: fixed; top: 12px; right: 14px; color: var(--ink-dim); 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(--ink); font-weight: 700; opacity: 1; }
</style>
</head>
<body>
<nav class="langs" id="langs"></nav>
<main data-lang-block lang="en">
<div class="sun"></div>
<h1><span class="blocks"><span>valley</span><span>blocks</span></span></h1>
<p class="tag">a serene tetris with serverless p2p rooms — this page is also the git repo.</p>
<div class="btns">
<a class="btn" href="https://game2.ardegazu.ro/">play</a>
<a class="btn alt" href="browse/index.html">browse the code</a>
<a class="btn alt" 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/game2.git
cd valley-blocks/client
npm install && npm run dev # port 4173</code></pre>
<h2>What it is</h2>
<p>A Monument Valley-inspired Tetris PWA: SRS rotation with wall kicks, 7-bag
randomizer, ghost piece, hold, guideline scoring, five palettes that shift
every two levels, synthesized sound, and a rare steerable 🐍 snake piece.
Solo play works fully offline; sharing the page URL turns it into a private
room for up to 16 players — same pieces for everyone from a shared seed,
race or outlast your friends, round after round.</p>
<h2>How it works</h2>
<ul>
<li><strong>No game server.</strong> Browsers are
<a href="https://libp2p.io">js-libp2p</a> peers: they meet through a
circuit-relay-v2 node (gossipsub peer discovery), upgrade to direct
WebRTC, and speak a room-scoped protocol. The room secret lives in the
URL fragment (<code>#…</code>); the protocol id only ever carries a
one-way HKDF of it.</li>
<li><strong>Everything is end-to-end encrypted.</strong> libp2p's noise
layer encrypts every peer pair end-to-end — even while frames still ride
the relay, it only routes opaque ciphertext. Room membership is proved
pairwise with a sealed AES-256-GCM hello keyed from the room secret and
bound to both authenticated peer ids (<code>client/src/net/</code>,
shared with <a href="https://git.ardegazu.ro/game1/">neon-grid</a>);
TURN fallback still can't read anything.</li>
<li><strong>One peer referees.</strong> The host deals seats and seeds,
judges round outcomes, and hands a snapshot to late joiners (they
spectate, then play the next round). Host conflicts resolve by a shared
precedence rule — an active round outranks an idle claim, a joined player
outranks a parked tab, ties break to lowest peer id — and if the host
vanishes mid-round, the round voids and the lowest surviving id takes
over. Boards themselves are simulated locally — every player broadcasts
their own compact board state, so your controls never wait on the
network.</li>
<li><strong>The site is plain static files.</strong> The game and this repo
page are served with no backend anywhere — deploying is replacing a
folder.</li>
</ul>
<h2>Honest guarantees & limits</h2>
<div class="card">
<p><strong>Guaranteed:</strong> no accounts, no analytics, no database, no
third-party requests. Room contents are end-to-end encrypted; the relay
routes opaque ciphertext and sees connection metadata only (peer ids, IP
addresses, traffic volume — as any relay must; room ids never leave the
encrypted channel). Scores and history stay in your browser.</p>
<p><strong>Not guaranteed:</strong> the host referees, and each player reports
their own board — a modified client could lie about its board. Play with
friends, not strangers with prize money. Rooms are capped at 16 and a full
mesh gets heavy past ~12 players on phones. Anyone with the room link is in
the room — treat links like invitations.</p>
</div>
<h2>Layout</h2>
<pre><code>client/src/game2/net/ libp2p layer (bootstrap, node, peers, room crypto, TURN) — shared with neon-grid
client/src/game2/game/ the game: core/ render/ input/ ui/ audio/ storage/ + frames.cljs (wire) + party.cljs (referee)
client/src/game2/main.cljs room secret → create-net → party → app
client/test/ golden vectors: wire bytes, referee rules, the board sim
site/ this page · deploy/publish-repo.sh builds the git mirror</code></pre>
<footer>MIT licensed. Anonymous by design — the repo's only author is
<code>valley-blocks <valley-blocks@noreply.local></code>.</footer>
</main>
<main data-lang-block lang="ro" hidden>
<div class="sun"></div>
<h1><span class="blocks"><span>valley</span><span>blocks</span></span></h1>
<p class="tag">un tetris senin cu camere p2p fără server — pagina asta e și repo-ul git.</p>
<div class="btns">
<a class="btn" href="https://game2.ardegazu.ro/">joacă</a>
<a class="btn alt" href="browse/index.html">răsfoiește codul</a>
<a class="btn alt" 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/game2.git
cd valley-blocks/client
npm install && npm run dev # port 4173</code></pre>
<h2>Ce este</h2>
<p>Un PWA de Tetris inspirat de Monument Valley: rotație SRS cu wall kicks,
randomizer 7-bag, piesă-fantomă, rezervă, punctaj după guideline, cinci palete
care se schimbă la fiecare două niveluri, sunet sintetizat și o rară piesă-🐍
șarpe pe care o poți cârmi. Jocul solo merge complet offline; trimițând URL-ul
paginii o transformi într-o cameră privată pentru până la 16 jucători —
aceleași piese pentru toți dintr-un seed comun, întrece-ți sau rezistă mai
mult decât prietenii, rundă după rundă.</p>
<h2>Cum funcționează</h2>
<ul>
<li><strong>Fără server de joc.</strong> Browserele sunt peers
<a href="https://libp2p.io">js-libp2p</a>: se întâlnesc printr-un nod
circuit-relay-v2 (descoperire prin gossipsub), trec pe WebRTC direct și
vorbesc un protocol legat de cameră. Secretul camerei stă în fragmentul
URL-ului (<code>#…</code>); id-ul de protocol poartă doar un HKDF
ireversibil al lui.</li>
<li><strong>Totul e criptat cap-la-cap.</strong> Stratul noise al lui libp2p
criptează fiecare pereche de peers de la un capăt la altul — chiar și cât
timp cadrele mai trec prin releu, acesta rutează doar text cifrat opac.
Apartenența la cameră se dovedește pereche cu pereche, cu un hello sigilat
AES-256-GCM derivat din secretul camerei și legat de ambele peer id-uri
autentificate (<code>client/src/net/</code>, împărțit cu
<a href="https://git.ardegazu.ro/game1/">neon-grid</a>); nici fallback-ul
TURN nu poate citi nimic.</li>
<li><strong>Un peer arbitrează.</strong> Gazda împarte locurile și seed-urile,
judecă deznodământul rundelor și dă un snapshot celor care intră târziu
(privesc, apoi joacă runda următoare). Conflictele de gazdă se rezolvă
printr-o regulă de precedență comună — o rundă activă bate o pretenție
inactivă, un jucător intrat bate un tab parcat, egalitățile merg la cel
mai mic peer id — iar dacă gazda dispare în mijlocul rundei, runda se
anulează și preia cel mai mic id supraviețuitor. Tablele în sine se
simulează local — fiecare jucător își difuzează propria stare compactă a
tablei, așa că comenzile tale nu așteaptă niciodată rețeaua.</li>
<li><strong>Situl e doar fișiere statice.</strong> Jocul și pagina asta de
repo sunt servite fără niciun backend nicăieri — deploy-ul înseamnă să
înlocuiești un director.</li>
</ul>
<h2>Garanții și limite, pe bune</h2>
<div class="card">
<p><strong>Garantat:</strong> fără conturi, fără analytics, fără bază de
date, fără cereri către terți. Conținutul camerelor e criptat cap-la-cap;
releul rutează text cifrat opac și vede doar metadate de conexiune (peer
id-uri, adrese IP, volum de trafic — ca orice releu; id-urile camerelor nu
părăsesc niciodată canalul criptat). Scorurile și istoricul rămân în
browserul tău.</p>
<p><strong>Negarantat:</strong> gazda arbitrează, iar fiecare jucător își
raportează singur tabla — un client modificat ar putea minți despre ea.
Joacă cu prieteni, nu cu străini pe bani. Camerele sunt plafonate la 16, iar
un mesh complet devine greu peste ~12 jucători pe telefoane. Oricine are
linkul camerei e în cameră — tratează linkurile ca pe niște invitații.</p>
</div>
<h2>Structura</h2>
<pre><code>client/src/game2/net/ stratul libp2p (bootstrap, node, peers, cripto de cameră, TURN) — împărțit cu neon-grid
client/src/game2/game/ jocul: core/ render/ input/ ui/ audio/ storage/ + frames.cljs (protocol) + party.cljs (arbitru)
client/src/game2/main.cljs secretul camerei → create-net → party → app
client/test/ vectori de referință: octeții de pe fir, regulile arbitrului, simularea tablei
site/ pagina asta · deploy/publish-repo.sh construiește oglinda git</code></pre>
<footer>Licență MIT. Anonim prin construcție — singurul autor al repo-ului e
<code>valley-blocks <valley-blocks@noreply.local></code>.</footer>
</main>
<main data-lang-block lang="hu" hidden>
<div class="sun"></div>
<h1><span class="blocks"><span>valley</span><span>blocks</span></span></h1>
<p class="tag">derűs tetris szerver nélküli p2p szobákkal — ez az oldal egyben a git repó is.</p>
<div class="btns">
<a class="btn" href="https://game2.ardegazu.ro/">játék</a>
<a class="btn alt" href="browse/index.html">böngészd a kódot</a>
<a class="btn alt" 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/game2.git
cd valley-blocks/client
npm install && npm run dev # port 4173</code></pre>
<h2>Mi ez</h2>
<p>Monument Valley ihlette Tetris PWA: SRS-forgatás wall kickekkel, 7-bag
randomizer, szellemelem, tartalék, guideline-pontozás, öt paletta, amely két
szintenként vált, szintetizált hang, és egy ritka, kormányozható 🐍
kígyóelem. A szólójáték teljesen offline is megy; az oldal URL-jét megosztva
privát szobává válik akár 16 játékosnak — közös seedből mindenki ugyanazokat
az elemeket kapja, versenyezz vagy bírd tovább a barátaidnál, körről körre.</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">js-libp2p</a> peerek: egy circuit-relay-v2
node-on át találkoznak (gossipsub peer-felfedezés), közvetlen WebRTC-re
váltanak, és szoba-hatókörű protokollt beszélnek. A szoba titka az URL
fragmentben él (<code>#…</code>); a protokoll-id csak egy egyirányú
HKDF-jét hordozza.</li>
<li><strong>Minden végponttól végpontig titkosított.</strong> A libp2p
noise-rétege minden peer-párt végponttól végpontig titkosít — amíg a
keretek még a relén mennek át, az is csak átlátszatlan titkosított
szöveget továbbít. A szobatagságot páronként a szoba titkából
származtatott, mindkét hitelesített peer-id-hoz kötött, lepecsételt
AES-256-GCM hello bizonyítja (<code>client/src/net/</code>, közös a
<a href="https://git.ardegazu.ro/game1/">neon-griddel</a>); a TURN
tartalék sem tud elolvasni semmit.</li>
<li><strong>Az egyik peer a bíró.</strong> A házigazda osztja a helyeket és
a seedeket, megítéli a körök kimenetelét, és pillanatképet ad a későn
érkezőknek (ők néznek, aztán a következő körben játszanak). A
házigazda-vitákat közös elsőbbségi szabály dönti el — az aktív kör üti a
tétlen igényt, a belépett játékos üti a parkoló fület, döntetlennél a
legkisebb peer id nyer —, és ha a házigazda kör közben tűnik el, a kör
érvénytelen, és a legkisebb megmaradt id veszi át. Maguk a táblák
helyben szimulálódnak — minden játékos a saját kompakt táblaállapotát
sugározza, így az irányításod soha nem vár a hálózatra.</li>
<li><strong>Az oldal sima statikus fájl.</strong> A játékot és ezt a
repó-oldalt sehol semmilyen backend nem szolgálja ki — a telepítés egy
mappa lecserélése.</li>
</ul>
<h2>Garanciák és korlátok, őszintén</h2>
<div class="card">
<p><strong>Garantált:</strong> se fiók, se analitika, se adatbázis, se
harmadik félhez menő kérés. A szobák tartalma végponttól végpontig
titkosított; a relé átlátszatlan titkosított szöveget továbbít, és csak
kapcsolati metaadatokat lát (peer id-k, IP-címek, forgalom mennyisége —
ahogy minden relének muszáj; a szoba-id-k soha nem hagyják el a titkosított
csatornát). A pontszámok és az előzmények a böngésződben maradnak.</p>
<p><strong>Nem garantált:</strong> a házigazda bíráskodik, és minden játékos
maga jelenti a tábláját — egy módosított kliens hazudhat róla. Barátokkal
játssz, ne pénzdíjas idegenekkel. A szobák plafonja 16 fő, és a teljes mesh
~12 játékos fölött telefonon nehézzé válik. Bárki, akinél ott a szoba
linkje, bent van a szobában — kezeld a linkeket meghívóként.</p>
</div>
<h2>Felépítés</h2>
<pre><code>client/src/game2/net/ libp2p réteg (bootstrap, node, peers, szoba-kripto, TURN) — közös a neon-griddel
client/src/game2/game/ a játék: core/ render/ input/ ui/ audio/ storage/ + frames.cljs (protokoll) + party.cljs (bíró)
client/src/game2/main.cljs szoba-titok → create-net → party → app
client/test/ referenciavektorok: a vonalon menő bájtok, a bíró szabályai, a tábla szimulációja
site/ ez az oldal · deploy/publish-repo.sh építi a git tükröt</code></pre>
<footer>MIT licenc. Szándékosan névtelen — a repó egyetlen szerzője
<code>valley-blocks <valley-blocks@noreply.local></code>.</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>
|