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 ─── */
function flipCoin(resultBit, areaId) {
const area = $(areaId);
if (!area) return;
area.hidden = false;
const coin = area.querySelector('.coin');
coin.classList.remove('flipping','result-kopf','result-zahl');
void coin.offsetWidth;
coin.classList.add('flipping');
function flipCoinEl(coinEl, resultBit) {
coinEl.classList.remove('flipping','result-kopf','result-zahl');
void coinEl.offsetWidth;
coinEl.classList.add('flipping');
setTimeout(() => {
coin.classList.remove('flipping');
coin.classList.add(resultBit === 0 ? 'result-kopf' : 'result-zahl');
coinEl.classList.remove('flipping');
coinEl.classList.add(resultBit === 0 ? 'result-kopf' : 'result-zahl');
}, 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 ─── */
function updatePhases(state) {
const map = { IDLE:'ready', STARTING:'ready', COMMITTING:'commit',
@ -158,7 +195,7 @@
show('game');
renderPlayers('game-player-list');
$('btn-start-protocol').hidden = autostart || role !== 'host';
$('coin-area').hidden = true;
$('results-container').hidden = true;
$('result-area').hidden = true;
$('protocol-text').textContent = autostart ? 'Starte Protokoll...' : 'Bereit zum Münzwurf';
updatePhases('IDLE');
@ -170,12 +207,11 @@
$('protocol-text').textContent =
phase === 'commit' ? `Commits: ${rcvd}/${total}` : `Reveals: ${rcvd}/${total}`;
};
protocol.oncomplete = result => {
const text = result === 0 ? 'KOPF' : 'ZAHL';
$('protocol-text').textContent = `Ergebnis: ${text}!`;
flipCoin(result, 'coin-area');
protocol.oncomplete = results => {
$('protocol-text').textContent = 'Ergebnisse';
renderResults(results);
$('result-area').hidden = false;
$('result-text').textContent = text;
$('result-text').textContent = 'Fertig';
$('btn-start-protocol').hidden = true;
};
protocol.onerror = err => {
@ -188,7 +224,7 @@
mesh.ondata = (peerId, msg) => {
if (msg.type === 'protocol_start') {
if (protocol) protocol.reset();
$('coin-area').hidden = true;
$('results-container').hidden = true;
$('result-area').hidden = true;
$('btn-start-protocol').hidden = true;
updatePhases('IDLE');
@ -335,7 +371,7 @@
/* ─── Retry / again ─── */
$('btn-retry').addEventListener('click', () => {
if (protocol) protocol.reset();
$('coin-area').hidden = true;
$('results-container').hidden = true;
$('result-area').hidden = true;
$('btn-start-protocol').hidden = role !== 'host';
$('protocol-text').textContent = 'Bereit zum Münzwurf';
@ -345,7 +381,7 @@
/* ─── Result screen → play again ─── */
$('btn-play-again').addEventListener('click', () => {
if (protocol) protocol.reset();
$('coin-area').hidden = true;
$('results-container').hidden = true;
$('result-area').hidden = true;
setupGame(false);
});

View file

@ -24,14 +24,23 @@ function arraysEqual(a, b) {
return diff === 0;
}
function combine(secrets) {
function combineAll(secrets) {
const result = new Uint8Array(32);
for (const secret of secrets) {
for (let i = 0; i < 32; 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) {

View file

@ -93,14 +93,7 @@
<div class="phase" id="phase-done">Ergebnis</div>
</div>
<div id="protocol-text" class="protocol-text">Warte auf Start...</div>
<div id="coin-area" hidden>
<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="results-container" class="results-container" hidden></div>
<div id="result-area" hidden>
<h3 id="result-text"></h3>
<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.result = combine(Array.from(this.reveals.values()));
this.results = results;
if (this.onstatechange) this.onstatechange('COMPLETE');
if (this.oncomplete) this.oncomplete(this.result);
if (this.oncomplete) this.oncomplete(results);
}
/* ─── Reset ─── */

View file

@ -268,6 +268,31 @@ h3 {
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 {
margin-top: 24px;