Pick a short seed below and press Expand. A tiny deterministic generator stretches your few starting bits into a long stream, and three simple randomness tests check whether the output looks like fair coin flips: roughly half ones, no obvious short-run bias, and no easy correlation between neighboring bits.
<p class="hint">A pseudorandom generator stretches a short <b>seed</b> into a long
stream of bits. Pick a seed, press <b>Expand</b>, and watch three simple randomness
tests judge the output. The same seed always gives the same stream — but flip one bit
of the seed and everything changes.</p>
<div class="row">
<label for="seed">Seed (digits):</label>
<input id="seed" type="text" value="42" maxlength="12" inputmode="numeric">
<button id="flip" type="button" class="ghost">Flip a seed bit</button>
</div>
<div class="btns">
<button id="expand" type="button">Expand</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="stream" id="stream">Press Expand to generate a stream.</div>
<div class="tests" id="tests"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .9rem; font-weight: 600; color: #1d3557; }
input { font: 600 14px ui-monospace, monospace; padding: .4rem .6rem; width: 9rem;
border: 1px solid #cdd9e3; border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stream { font: 600 13px ui-monospace, monospace; line-height: 1.7; letter-spacing: 1px;
background: #f4f7fa; border: 1px solid #e1e8ef; border-radius: 8px; padding: .7rem;
word-break: break-all; color: #1d3557; min-height: 3.5em; }
.stream b { color: #e63946; }
.tests { margin-top: .7rem; display: grid; gap: .4rem; }
.test { display: flex; align-items: center; gap: .5rem; font-size: .9rem; }
.badge { font: 700 12px system-ui, sans-serif; padding: .15rem .5rem; border-radius: 999px; min-width: 4.2rem; text-align: center; }
.badge.pass { background: #d7f0df; color: #0a7d33; }
.badge.fail { background: #fcdcdf; color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A pseudorandom generator (PRG): a deterministic function that stretches a short
// seed into a long bit stream that looks random to anyone who doesn't know the seed.
// Here we use a small mixing function (a SplitMix64-style hash) in counter mode:
// for i = 0,1,2,... output_block = mix(seed + i)
// Crucially it is DETERMINISTIC: same seed -> same stream. Secure real-world PRGs
// (stream ciphers) replace this toy mixer with a hardness-based one-way function.
const STREAM_BITS = 256;
// Murmur3-style 32-bit finalizer: a strong mixer that avalanches its input, so
// flipping ANY single input bit flips about half of the 32 output bits.
function mix(x){
x = (x + 0x9e3779b9) >>> 0;
x = Math.imul(x ^ (x >>> 16), 0x85ebca6b) >>> 0;
x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35) >>> 0;
x = (x ^ (x >>> 16)) >>> 0;
return x >>> 0;
}
// Stretch a numeric seed into n pseudorandom bits via counter mode:
// block i = mix(seed XOR (i * golden-ratio constant)), 32 fresh bits each.
function expand(seed, n){
const bits = [];
let counter = 0;
while (bits.length < n){
const block = mix(((seed ^ Math.imul(counter + 1, 0x9e3779b9)) >>> 0));
for (let b = 31; b >= 0 && bits.length < n; b--) bits.push((block >>> b) & 1);
counter++;
}
return bits;
}
// ── Three simple randomness tests ───────────────────────────────────────────
// 1) Frequency: the fraction of ones should be close to 1/2.
function freqTest(bits){
const ones = bits.reduce((a,b)=>a+b,0);
const frac = ones / bits.length;
return { ok: Math.abs(frac - 0.5) < 0.08, detail: fmt('%v%% ones', { v: (frac*100).toFixed(1) }) };
}
// 2) Runs: count maximal runs of equal bits; for random bits it's ~n/2.
function runsTest(bits){
let runs = 1;
for (let i=1;i<bits.length;i++) if (bits[i]!==bits[i-1]) runs++;
const expected = bits.length/2;
return { ok: Math.abs(runs - expected) < expected*0.25,
detail: fmt('%runs% runs (≈%expected% expected)', { runs: runs, expected: expected }) };
}
// 3) Serial correlation: neighboring bits shouldn't predict each other.
function corrTest(bits){
let agree = 0;
for (let i=1;i<bits.length;i++) if (bits[i]===bits[i-1]) agree++;
const frac = agree/(bits.length-1);
return { ok: Math.abs(frac - 0.5) < 0.08, detail: fmt('%v%% adjacent pairs match', { v: (frac*100).toFixed(1) }) };
}
let prevBits = null;
function readSeed(){
const raw = document.getElementById('seed').value.replace(/\D/g,'') || '0';
return (parseInt(raw,10) >>> 0);
}
function render(bits, prev){
const el = document.getElementById('stream'); el.innerHTML = '';
const shown = bits.slice(0,128);
shown.forEach((bit,i)=>{
const changed = prev && prev[i] !== undefined && prev[i] !== bit;
if (changed){ const b=document.createElement('b'); b.textContent=bit; el.appendChild(b); }
else el.appendChild(document.createTextNode(String(bit)));
});
el.appendChild(document.createTextNode(' …'));
}
function renderTests(bits){
const defs = [
['Frequency', freqTest(bits)],
['Runs', runsTest(bits)],
['Correlation', corrTest(bits)]
];
const c = document.getElementById('tests'); c.innerHTML='';
defs.forEach(([name,res])=>{
const row=document.createElement('div'); row.className='test';
const badge=document.createElement('span');
badge.className='badge '+(res.ok?'pass':'fail');
badge.textContent=res.ok?'PASS':'FAIL';
const txt=document.createElement('span');
txt.innerHTML='<b style="color:#1d3557">'+name+'</b> — '+res.detail;
row.appendChild(badge); row.appendChild(txt); c.appendChild(row);
});
}
function run(){
const seed = readSeed();
const bits = expand(seed, STREAM_BITS);
render(bits, prevBits);
renderTests(bits);
console.log('seed', seed, '→ first 32 bits', bits.slice(0,32).join(''));
prevBits = bits;
}
function flipBit(){
const seed = readSeed();
const flipped = (seed ^ (1 << (Math.floor(Math.random()*8)))) >>> 0;
document.getElementById('seed').value = String(flipped);
run();
}
document.getElementById('expand').addEventListener('click', run);
document.getElementById('flip').addEventListener('click', flipBit);
document.getElementById('reset').addEventListener('click', ()=>{
document.getElementById('seed').value='42'; prevBits=null; run();
});
run();
<p class="hint">A pseudorandom generator stretches a short <b>seed</b> into a long
stream of bits. Pick a seed, press <b>Expand</b>, and watch three simple randomness
tests judge the output. The same seed always gives the same stream — but flip one bit
of the seed and everything changes.</p>
<div class="row">
<label for="seed">Seed (digits):</label>
<input id="seed" type="text" value="42" maxlength="12" inputmode="numeric">
<button id="flip" type="button" class="ghost">Flip a seed bit</button>
</div>
<div class="btns">
<button id="expand" type="button">Expand</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="stream" id="stream">Press Expand to generate a stream.</div>
<div class="tests" id="tests"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .9rem; font-weight: 600; color: #1d3557; }
input { font: 600 14px ui-monospace, monospace; padding: .4rem .6rem; width: 9rem;
border: 1px solid #cdd9e3; border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stream { font: 600 13px ui-monospace, monospace; line-height: 1.7; letter-spacing: 1px;
background: #f4f7fa; border: 1px solid #e1e8ef; border-radius: 8px; padding: .7rem;
word-break: break-all; color: #1d3557; min-height: 3.5em; }
.stream b { color: #e63946; }
.tests { margin-top: .7rem; display: grid; gap: .4rem; }
.test { display: flex; align-items: center; gap: .5rem; font-size: .9rem; }
.badge { font: 700 12px system-ui, sans-serif; padding: .15rem .5rem; border-radius: 999px; min-width: 4.2rem; text-align: center; }
.badge.pass { background: #d7f0df; color: #0a7d33; }
.badge.fail { background: #fcdcdf; color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A pseudorandom generator (PRG): a deterministic function that stretches a short
// seed into a long bit stream that looks random to anyone who doesn't know the seed.
// Here we use a small mixing function (a SplitMix64-style hash) in counter mode:
// for i = 0,1,2,... output_block = mix(seed + i)
// Crucially it is DETERMINISTIC: same seed -> same stream. Secure real-world PRGs
// (stream ciphers) replace this toy mixer with a hardness-based one-way function.
const STREAM_BITS = 256;
// Murmur3-style 32-bit finalizer: a strong mixer that avalanches its input, so
// flipping ANY single input bit flips about half of the 32 output bits.
function mix(x){
x = (x + 0x9e3779b9) >>> 0;
x = Math.imul(x ^ (x >>> 16), 0x85ebca6b) >>> 0;
x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35) >>> 0;
x = (x ^ (x >>> 16)) >>> 0;
return x >>> 0;
}
// Stretch a numeric seed into n pseudorandom bits via counter mode:
// block i = mix(seed XOR (i * golden-ratio constant)), 32 fresh bits each.
function expand(seed, n){
const bits = [];
let counter = 0;
while (bits.length < n){
const block = mix(((seed ^ Math.imul(counter + 1, 0x9e3779b9)) >>> 0));
for (let b = 31; b >= 0 && bits.length < n; b--) bits.push((block >>> b) & 1);
counter++;
}
return bits;
}
// ── Three simple randomness tests ───────────────────────────────────────────
// 1) Frequency: the fraction of ones should be close to 1/2.
function freqTest(bits){
const ones = bits.reduce((a,b)=>a+b,0);
const frac = ones / bits.length;
return { ok: Math.abs(frac - 0.5) < 0.08, detail: fmt('%v%% ones', { v: (frac*100).toFixed(1) }) };
}
// 2) Runs: count maximal runs of equal bits; for random bits it's ~n/2.
function runsTest(bits){
let runs = 1;
for (let i=1;i<bits.length;i++) if (bits[i]!==bits[i-1]) runs++;
const expected = bits.length/2;
return { ok: Math.abs(runs - expected) < expected*0.25,
detail: fmt('%runs% runs (≈%expected% expected)', { runs: runs, expected: expected }) };
}
// 3) Serial correlation: neighboring bits shouldn't predict each other.
function corrTest(bits){
let agree = 0;
for (let i=1;i<bits.length;i++) if (bits[i]===bits[i-1]) agree++;
const frac = agree/(bits.length-1);
return { ok: Math.abs(frac - 0.5) < 0.08, detail: fmt('%v%% adjacent pairs match', { v: (frac*100).toFixed(1) }) };
}
let prevBits = null;
function readSeed(){
const raw = document.getElementById('seed').value.replace(/\D/g,'') || '0';
return (parseInt(raw,10) >>> 0);
}
function render(bits, prev){
const el = document.getElementById('stream'); el.innerHTML = '';
const shown = bits.slice(0,128);
shown.forEach((bit,i)=>{
const changed = prev && prev[i] !== undefined && prev[i] !== bit;
if (changed){ const b=document.createElement('b'); b.textContent=bit; el.appendChild(b); }
else el.appendChild(document.createTextNode(String(bit)));
});
el.appendChild(document.createTextNode(' …'));
}
function renderTests(bits){
const defs = [
['Frequency', freqTest(bits)],
['Runs', runsTest(bits)],
['Correlation', corrTest(bits)]
];
const c = document.getElementById('tests'); c.innerHTML='';
defs.forEach(([name,res])=>{
const row=document.createElement('div'); row.className='test';
const badge=document.createElement('span');
badge.className='badge '+(res.ok?'pass':'fail');
badge.textContent=res.ok?'PASS':'FAIL';
const txt=document.createElement('span');
txt.innerHTML='<b style="color:#1d3557">'+name+'</b> — '+res.detail;
row.appendChild(badge); row.appendChild(txt); c.appendChild(row);
});
}
function run(){
const seed = readSeed();
const bits = expand(seed, STREAM_BITS);
render(bits, prevBits);
renderTests(bits);
console.log('seed', seed, '→ first 32 bits', bits.slice(0,32).join(''));
prevBits = bits;
}
function flipBit(){
const seed = readSeed();
const flipped = (seed ^ (1 << (Math.floor(Math.random()*8)))) >>> 0;
document.getElementById('seed').value = String(flipped);
run();
}
document.getElementById('expand').addEventListener('click', run);
document.getElementById('flip').addEventListener('click', flipBit);
document.getElementById('reset').addEventListener('click', ()=>{
document.getElementById('seed').value='42'; prevBits=null; run();
});
run();
Two things to notice. First, the same seed always produces the same stream — that's what lets two people who share a seed generate the identical keystream to encrypt and decrypt. Second, flip a single bit of the seed and the entire stream changes unrecognizably, yet it still passes the tests. The output is fully determined, but it behaves like randomness — and that is exactly what "pseudorandom" means.
Comments
Loading comments...