diff --git a/app.js b/app.js
index 2e8ba22..025369d 100644
--- a/app.js
+++ b/app.js
@@ -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 = `
+
+ ${name}
+ `;
+ 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);
});
diff --git a/crypto.js b/crypto.js
index 6a70c34..8e110fb 100644
--- a/crypto.js
+++ b/crypto.js
@@ -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) {
diff --git a/index.html b/index.html
index 2854f1c..126b2c5 100644
--- a/index.html
+++ b/index.html
@@ -93,14 +93,7 @@
Ergebnis
Warte auf Start...
-
+
diff --git a/protocol.js b/protocol.js
index 8f6baa4..ca0ceb2 100644
--- a/protocol.js
+++ b/protocol.js
@@ -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 ─── */
diff --git a/style.css b/style.css
index e613aa3..1307de8 100644
--- a/style.css
+++ b/style.css
@@ -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;