Jede Person wirft eigene Münze: sharedSeed + per-peer Result via SHA-256
All checks were successful
Pin to IPFS / pin (push) Successful in 13s

This commit is contained in:
Ole 2026-06-14 16:47:31 +02:00
parent fc81c1108b
commit 31a4c2e3a8
5 changed files with 100 additions and 31 deletions

74
app.js
View file

@ -122,20 +122,57 @@
} }
/* ─── Coin animation ─── */ /* ─── Coin animation ─── */
function flipCoin(resultBit, areaId) { function flipCoinEl(coinEl, resultBit) {
const area = $(areaId); coinEl.classList.remove('flipping','result-kopf','result-zahl');
if (!area) return; void coinEl.offsetWidth;
area.hidden = false; coinEl.classList.add('flipping');
const coin = area.querySelector('.coin');
coin.classList.remove('flipping','result-kopf','result-zahl');
void coin.offsetWidth;
coin.classList.add('flipping');
setTimeout(() => { setTimeout(() => {
coin.classList.remove('flipping'); coinEl.classList.remove('flipping');
coin.classList.add(resultBit === 0 ? 'result-kopf' : 'result-zahl'); coinEl.classList.add(resultBit === 0 ? 'result-kopf' : 'result-zahl');
}, 1500); }, 1500);
} }
function renderResults(results) {
const container = $('results-container');
container.innerHTML = '';
container.hidden = false;
const all = [mesh.myPeerId, ...mesh.getPeerIds()];
const seen = new Set();
all.forEach(id => {
if (seen.has(id)) return;
seen.add(id);
const r = results.get(id);
if (r === undefined) return;
const isMe = id === mesh.myPeerId;
const name = isMe ? 'Du' : id.slice(0, 8);
const item = document.createElement('div');
item.className = 'result-item';
item.dataset.result = r;
item.innerHTML = `
<div class="coin mini">
<div class="coin-inner">
<div class="coin-face front">K</div>
<div class="coin-face back">Z</div>
</div>
</div>
<div class="result-label">${name}</div>
`;
container.appendChild(item);
});
requestAnimationFrame(() => {
const coins = container.querySelectorAll('.coin');
coins.forEach((coin, i) => {
const r = parseInt(coin.closest('.result-item').dataset.result);
setTimeout(() => flipCoinEl(coin, r), i * 400);
});
});
}
/* ─── Protocol phases UI ─── */ /* ─── Protocol phases UI ─── */
function updatePhases(state) { function updatePhases(state) {
const map = { IDLE:'ready', STARTING:'ready', COMMITTING:'commit', const map = { IDLE:'ready', STARTING:'ready', COMMITTING:'commit',
@ -158,7 +195,7 @@
show('game'); show('game');
renderPlayers('game-player-list'); renderPlayers('game-player-list');
$('btn-start-protocol').hidden = autostart || role !== 'host'; $('btn-start-protocol').hidden = autostart || role !== 'host';
$('coin-area').hidden = true; $('results-container').hidden = true;
$('result-area').hidden = true; $('result-area').hidden = true;
$('protocol-text').textContent = autostart ? 'Starte Protokoll...' : 'Bereit zum Münzwurf'; $('protocol-text').textContent = autostart ? 'Starte Protokoll...' : 'Bereit zum Münzwurf';
updatePhases('IDLE'); updatePhases('IDLE');
@ -170,12 +207,11 @@
$('protocol-text').textContent = $('protocol-text').textContent =
phase === 'commit' ? `Commits: ${rcvd}/${total}` : `Reveals: ${rcvd}/${total}`; phase === 'commit' ? `Commits: ${rcvd}/${total}` : `Reveals: ${rcvd}/${total}`;
}; };
protocol.oncomplete = result => { protocol.oncomplete = results => {
const text = result === 0 ? 'KOPF' : 'ZAHL'; $('protocol-text').textContent = 'Ergebnisse';
$('protocol-text').textContent = `Ergebnis: ${text}!`; renderResults(results);
flipCoin(result, 'coin-area');
$('result-area').hidden = false; $('result-area').hidden = false;
$('result-text').textContent = text; $('result-text').textContent = 'Fertig';
$('btn-start-protocol').hidden = true; $('btn-start-protocol').hidden = true;
}; };
protocol.onerror = err => { protocol.onerror = err => {
@ -188,7 +224,7 @@
mesh.ondata = (peerId, msg) => { mesh.ondata = (peerId, msg) => {
if (msg.type === 'protocol_start') { if (msg.type === 'protocol_start') {
if (protocol) protocol.reset(); if (protocol) protocol.reset();
$('coin-area').hidden = true; $('results-container').hidden = true;
$('result-area').hidden = true; $('result-area').hidden = true;
$('btn-start-protocol').hidden = true; $('btn-start-protocol').hidden = true;
updatePhases('IDLE'); updatePhases('IDLE');
@ -335,7 +371,7 @@
/* ─── Retry / again ─── */ /* ─── Retry / again ─── */
$('btn-retry').addEventListener('click', () => { $('btn-retry').addEventListener('click', () => {
if (protocol) protocol.reset(); if (protocol) protocol.reset();
$('coin-area').hidden = true; $('results-container').hidden = true;
$('result-area').hidden = true; $('result-area').hidden = true;
$('btn-start-protocol').hidden = role !== 'host'; $('btn-start-protocol').hidden = role !== 'host';
$('protocol-text').textContent = 'Bereit zum Münzwurf'; $('protocol-text').textContent = 'Bereit zum Münzwurf';
@ -345,7 +381,7 @@
/* ─── Result screen → play again ─── */ /* ─── Result screen → play again ─── */
$('btn-play-again').addEventListener('click', () => { $('btn-play-again').addEventListener('click', () => {
if (protocol) protocol.reset(); if (protocol) protocol.reset();
$('coin-area').hidden = true; $('results-container').hidden = true;
$('result-area').hidden = true; $('result-area').hidden = true;
setupGame(false); setupGame(false);
}); });

View file

@ -24,14 +24,23 @@ function arraysEqual(a, b) {
return diff === 0; return diff === 0;
} }
function combine(secrets) { function combineAll(secrets) {
const result = new Uint8Array(32); const result = new Uint8Array(32);
for (const secret of secrets) { for (const secret of secrets) {
for (let i = 0; i < 32; i++) { for (let i = 0; i < 32; i++) {
result[i] ^= secret[i]; result[i] ^= secret[i];
} }
} }
return result[0] & 1; return result;
}
async function computePeerResult(sharedSeed, peerId) {
const data = new TextEncoder().encode(peerId);
const combined = new Uint8Array(sharedSeed.length + data.length);
combined.set(sharedSeed);
combined.set(data, sharedSeed.length);
const hash = await crypto.subtle.digest('SHA-256', combined);
return new Uint8Array(hash)[0] & 1;
} }
function bufToBase64(buf) { function bufToBase64(buf) {

View file

@ -93,14 +93,7 @@
<div class="phase" id="phase-done">Ergebnis</div> <div class="phase" id="phase-done">Ergebnis</div>
</div> </div>
<div id="protocol-text" class="protocol-text">Warte auf Start...</div> <div id="protocol-text" class="protocol-text">Warte auf Start...</div>
<div id="coin-area" hidden> <div id="results-container" class="results-container" hidden></div>
<div id="coin" class="coin">
<div class="coin-inner">
<div class="coin-face front">K</div>
<div class="coin-face back">Z</div>
</div>
</div>
</div>
<div id="result-area" hidden> <div id="result-area" hidden>
<h3 id="result-text"></h3> <h3 id="result-text"></h3>
<button id="btn-retry" class="btn-primary">Nochmal</button> <button id="btn-retry" class="btn-primary">Nochmal</button>

View file

@ -149,10 +149,16 @@ class CoinFlipProtocol {
} }
} }
const sharedSeed = combineAll(Array.from(this.reveals.values()));
const results = new Map();
for (const pid of this.expectedPeers) {
results.set(pid, await computePeerResult(sharedSeed, pid));
}
this.state = 'COMPLETE'; this.state = 'COMPLETE';
this.result = combine(Array.from(this.reveals.values())); this.results = results;
if (this.onstatechange) this.onstatechange('COMPLETE'); if (this.onstatechange) this.onstatechange('COMPLETE');
if (this.oncomplete) this.oncomplete(this.result); if (this.oncomplete) this.oncomplete(results);
} }
/* ─── Reset ─── */ /* ─── Reset ─── */

View file

@ -268,6 +268,31 @@ h3 {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
/* Results container (per-peer coins) */
.results-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin: 20px 0;
}
.result-item {
text-align: center;
}
.result-item .coin.mini {
width: 80px;
height: 80px;
margin: 0 auto 8px;
perspective: 400px;
}
.result-item .coin.mini .coin-face {
font-size: 1.6rem;
}
.result-label {
font-size: 0.85rem;
color: var(--text-dim);
}
/* Result area */ /* Result area */
#result-area { #result-area {
margin-top: 24px; margin-top: 24px;