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
All checks were successful
Pin to IPFS / pin (push) Successful in 13s
This commit is contained in:
parent
fc81c1108b
commit
31a4c2e3a8
5 changed files with 100 additions and 31 deletions
74
app.js
74
app.js
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
13
crypto.js
13
crypto.js
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
10
protocol.js
10
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.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 ─── */
|
||||||
|
|
|
||||||
25
style.css
25
style.css
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue