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 ─── */
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
13
crypto.js
13
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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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.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 ─── */
|
||||
|
|
|
|||
25
style.css
25
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue