<p class="hint">A <b>pseudorandom generator (PRG)</b> stretches a short seed into many bits that look random to any small algorithm. The "hard function" here is a nonlinear mixing circuit (multiplication + XOR-folding) — computationally hard to invert. Toggle seed bits, then click <b>Generate</b> to expand them and run two statistical tests on the output.</p>
<div class="controls">
<label>Seed length: <span id="seedLen">8</span> bits</label>
<input type="range" id="slider" min="4" max="16" value="8" step="1">
</div>
<div class="seed-row" id="seedRow"></div>
<div class="btns">
<button id="genBtn" type="button">Generate</button>
<button id="randBtn" type="button" class="ghost">Random seed</button>
</div>
<div class="output-label">PRG output (<span id="outLen">0</span> bits):</div>
<div class="bits" id="bits"></div>
<div class="stats" id="stats"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls label { font-size: .9rem; font-weight: 600; min-width: 140px; }
input[type=range] { flex: 1; min-width: 140px; max-width: 260px; }
.seed-row { display: flex; gap: 4px; flex-wrap: wrap; margin: .4rem 0 .6rem; min-height: 38px; }
.sbit { width: 34px; height: 34px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 15px ui-monospace, monospace; cursor: pointer;
user-select: none; transition: background .12s; border: 1px solid #adb1b8; }
.sbit.zero { background: #e8eef3; color: #1d3557; }
.sbit.one { background: #1d3557; color: #fff; border-color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.output-label { font-size: .82rem; color: #555; margin-bottom: .3rem; }
.bits { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .7rem; min-height: 28px; }
.obit { width: 14px; height: 20px; border-radius: 3px; display: flex; align-items: center;
justify-content: center; font: 600 11px ui-monospace, monospace; }
.obit.zero { background: #dde5ed; color: #1d3557; }
.obit.one { background: #1d3557; color: #fff; }
.stats { font-size: .9rem; line-height: 1.6; background: #f4f7fa; border-radius: 8px;
padding: .6rem .8rem; border: 1px solid #cdd9e3; }
.stats .ok { color: #0a7d33; font-weight: 700; }
.stats .warn { color: #b45309; font-weight: 700; }
.bar-bg { background: #dde5ed; border-radius: 4px; height: 10px; width: 180px;
display: inline-block; vertical-align: middle; margin: 0 .4rem; }
.bar-fill { background: #1d3557; height: 10px; border-radius: 4px; transition: width .3s; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hardness vs Randomness — PRG demo
//
// The core idea: a SHORT seed + a HARD function = LONG pseudorandom output.
//
// Hard function: two nonlinear mixing steps using integer multiplication
// (a degree->infinity operation over the integers, hard to invert over GF(2)).
// step 1: multiply by the golden-ratio prime 0x9e3779b9
// step 2: XOR-fold the upper/lower halves, multiply again
// This is computationally hard to invert for the same reason hash preimages are
// hard: the nonlinearity (multiply) mixes all input bits together.
//
// PRG: pack the seed into a 32-bit integer, then iterate a nonlinear state evolution, extracting one output bit per step. The expansion ratio is (k-1)x.
function imul32(a, b) {
// 32-bit integer multiplication (Math.imul polyfill-style)
return Math.imul(a >>> 0, b >>> 0) >>> 0;
}
// The "hard function": maps a 32-bit state to 1 output bit.
// Two rounds of multiply-XOR-shift make this nonlinear and hard to invert.
function hardFunc(state, k) {
let x = state >>> 0;
x = imul32(x, 0x9e3779b9);
x ^= x >>> 13;
x = imul32(x, 0x6c62272e);
x ^= x >>> 7;
// Which bit to extract varies with seed length k (adds variety across k values)
return (x >>> (k % 17)) & 1;
}
// State evolution: LCG step with nonlinear XOR feedback.
// Each step is a bijection on 32-bit integers — no state is ever "stuck".
function nextState(state) {
let s = imul32(state >>> 0, 0x19660d) + 0x3c6ef35f;
s = s >>> 0;
s ^= imul32(s >>> 16, 0xb5a4bcae) >>> 0;
return s >>> 0;
}
// Pack an array of bits into a 32-bit unsigned integer.
function packBits(bits) {
let v = 0;
for (let i = 0; i < Math.min(bits.length, 32); i++) v |= (bits[i] << i);
return v >>> 0;
}
// PRG: seed (array of 0/1) -> output bits of length k*(k-1).
// Expansion ratio = (k-1)x: 8-bit seed -> 56 bits, 16-bit seed -> 240 bits.
function prg(seed) {
const k = seed.length;
const numOut = k * (k - 1);
let state = packBits(seed);
// Avoid degenerate zero state: mix in seed length
if (state === 0) state = imul32(k, 0x12345678) ^ 0xdeadbeef;
const output = [];
for (let step = 0; step < numOut; step++) {
output.push(hardFunc(state, k));
state = nextState(state);
}
return output;
}
// Statistical tests -------------------------------------------------------
// Bias test: fraction of ones should be within 15% of 0.5.
function biasTest(bits) {
const ones = bits.reduce((s, b) => s + b, 0);
const ratio = ones / bits.length;
return { ones, zeros: bits.length - ones, ratio, pass: Math.abs(ratio - 0.5) < 0.15 };
}
// Runs test: number of maximal runs of identical bits.
// Expected under uniform randomness: 1 + 2*n*p*(1-p).
function runsTest(bits) {
if (bits.length < 2) return { runs: 1, expected: 1, pass: true };
let runs = 1;
for (let i = 1; i < bits.length; i++) if (bits[i] !== bits[i - 1]) runs++;
const n = bits.length;
const p = bits.filter(b => b).length / n;
const expected = 1 + 2 * n * p * (1 - p);
return { runs, expected: Math.round(expected), pass: runs >= expected * 0.5 };
}
// UI ----------------------------------------------------------------------
const slider = document.getElementById('slider');
const seedLenLabel = document.getElementById('seedLen');
const seedRow = document.getElementById('seedRow');
const outLenLabel = document.getElementById('outLen');
const bitsDiv = document.getElementById('bits');
const statsDiv = document.getElementById('stats');
let seedBits = Array(8).fill(0);
seedBits[1] = 1; seedBits[3] = 1; seedBits[5] = 1; // default non-zero seed
function buildSeedRow() {
seedRow.innerHTML = '';
seedBits.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'sbit ' + (b ? 'one' : 'zero');
d.textContent = b;
d.title = fmt('Seed bit %i% — click to flip', { i: i });
d.addEventListener('click', () => {
seedBits[i] ^= 1;
buildSeedRow();
});
seedRow.appendChild(d);
});
}
function generate() {
const output = prg(seedBits);
outLenLabel.textContent = output.length;
bitsDiv.innerHTML = '';
output.forEach(b => {
const d = document.createElement('div');
d.className = 'obit ' + (b ? 'one' : 'zero');
d.textContent = b;
bitsDiv.appendChild(d);
});
const bias = biasTest(output);
const runs = runsTest(output);
const pct = (bias.ratio * 100).toFixed(1);
const barW = Math.round(bias.ratio * 180);
statsDiv.innerHTML =
fmt('<strong>Bias test</strong>: ones=%ones%, zeros=%zeros% (%pct%% ones) <span class="bar-bg"><span class="bar-fill" style="width:%barW%px"></span></span> ideal: 50%<br>%v%<br><br><strong>Runs test</strong>: %runs% runs (expected ≈ ≈ %expected%)<br>%v2%<br><br><em>Expansion: %length% seed bits → %length2% output bits (%v3%× stretch).</em> Derandomization replaces %length2% random coin flips with just %length% truly random bits + deterministic hard-function evaluation.', { ones: bias.ones, zeros: bias.zeros, pct: pct, barW: barW, v: bias.pass
? '<span class="ok">✓ Bias OK — output is well-balanced.</span>'
: '<span class="warn">⚠ Bias skewed — try a different seed.</span>', runs: runs.runs, expected: runs.expected, v2: runs.pass
? '<span class="ok">✓ Runs OK — bits alternate naturally.</span>'
: '<span class="warn">⚠ Too few runs — bits are too uniform.</span>', length: seedBits.length, length2: output.length, v3: (output.length / seedBits.length).toFixed(0) });
console.log(
'seed:', seedBits.join(''),
'| output:', output.length, 'bits',
'| ones:', bias.ones, '| zeros:', bias.zeros,
'| runs:', runs.runs, '| expected:', runs.expected
);
}
slider.addEventListener('input', () => {
const k = parseInt(slider.value);
seedLenLabel.textContent = k;
seedBits = Array(k).fill(0);
// Set alternating default
for (let i = 0; i < k; i++) seedBits[i] = i % 2;
buildSeedRow();
bitsDiv.innerHTML = '';
statsDiv.innerHTML = '';
outLenLabel.textContent = '0';
});
document.getElementById('genBtn').addEventListener('click', generate);
document.getElementById('randBtn').addEventListener('click', () => {
seedBits = seedBits.map(() => Math.random() < 0.5 ? 0 : 1);
buildSeedRow();
});
buildSeedRow();
<p class="hint">A <b>pseudorandom generator (PRG)</b> stretches a short seed into many bits that look random to any small algorithm. The "hard function" here is a nonlinear mixing circuit (multiplication + XOR-folding) — computationally hard to invert. Toggle seed bits, then click <b>Generate</b> to expand them and run two statistical tests on the output.</p>
<div class="controls">
<label>Seed length: <span id="seedLen">8</span> bits</label>
<input type="range" id="slider" min="4" max="16" value="8" step="1">
</div>
<div class="seed-row" id="seedRow"></div>
<div class="btns">
<button id="genBtn" type="button">Generate</button>
<button id="randBtn" type="button" class="ghost">Random seed</button>
</div>
<div class="output-label">PRG output (<span id="outLen">0</span> bits):</div>
<div class="bits" id="bits"></div>
<div class="stats" id="stats"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls label { font-size: .9rem; font-weight: 600; min-width: 140px; }
input[type=range] { flex: 1; min-width: 140px; max-width: 260px; }
.seed-row { display: flex; gap: 4px; flex-wrap: wrap; margin: .4rem 0 .6rem; min-height: 38px; }
.sbit { width: 34px; height: 34px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 15px ui-monospace, monospace; cursor: pointer;
user-select: none; transition: background .12s; border: 1px solid #adb1b8; }
.sbit.zero { background: #e8eef3; color: #1d3557; }
.sbit.one { background: #1d3557; color: #fff; border-color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.output-label { font-size: .82rem; color: #555; margin-bottom: .3rem; }
.bits { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .7rem; min-height: 28px; }
.obit { width: 14px; height: 20px; border-radius: 3px; display: flex; align-items: center;
justify-content: center; font: 600 11px ui-monospace, monospace; }
.obit.zero { background: #dde5ed; color: #1d3557; }
.obit.one { background: #1d3557; color: #fff; }
.stats { font-size: .9rem; line-height: 1.6; background: #f4f7fa; border-radius: 8px;
padding: .6rem .8rem; border: 1px solid #cdd9e3; }
.stats .ok { color: #0a7d33; font-weight: 700; }
.stats .warn { color: #b45309; font-weight: 700; }
.bar-bg { background: #dde5ed; border-radius: 4px; height: 10px; width: 180px;
display: inline-block; vertical-align: middle; margin: 0 .4rem; }
.bar-fill { background: #1d3557; height: 10px; border-radius: 4px; transition: width .3s; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hardness vs Randomness — PRG demo
//
// The core idea: a SHORT seed + a HARD function = LONG pseudorandom output.
//
// Hard function: two nonlinear mixing steps using integer multiplication
// (a degree->infinity operation over the integers, hard to invert over GF(2)).
// step 1: multiply by the golden-ratio prime 0x9e3779b9
// step 2: XOR-fold the upper/lower halves, multiply again
// This is computationally hard to invert for the same reason hash preimages are
// hard: the nonlinearity (multiply) mixes all input bits together.
//
// PRG: pack the seed into a 32-bit integer, then iterate a nonlinear state evolution, extracting one output bit per step. The expansion ratio is (k-1)x.
function imul32(a, b) {
// 32-bit integer multiplication (Math.imul polyfill-style)
return Math.imul(a >>> 0, b >>> 0) >>> 0;
}
// The "hard function": maps a 32-bit state to 1 output bit.
// Two rounds of multiply-XOR-shift make this nonlinear and hard to invert.
function hardFunc(state, k) {
let x = state >>> 0;
x = imul32(x, 0x9e3779b9);
x ^= x >>> 13;
x = imul32(x, 0x6c62272e);
x ^= x >>> 7;
// Which bit to extract varies with seed length k (adds variety across k values)
return (x >>> (k % 17)) & 1;
}
// State evolution: LCG step with nonlinear XOR feedback.
// Each step is a bijection on 32-bit integers — no state is ever "stuck".
function nextState(state) {
let s = imul32(state >>> 0, 0x19660d) + 0x3c6ef35f;
s = s >>> 0;
s ^= imul32(s >>> 16, 0xb5a4bcae) >>> 0;
return s >>> 0;
}
// Pack an array of bits into a 32-bit unsigned integer.
function packBits(bits) {
let v = 0;
for (let i = 0; i < Math.min(bits.length, 32); i++) v |= (bits[i] << i);
return v >>> 0;
}
// PRG: seed (array of 0/1) -> output bits of length k*(k-1).
// Expansion ratio = (k-1)x: 8-bit seed -> 56 bits, 16-bit seed -> 240 bits.
function prg(seed) {
const k = seed.length;
const numOut = k * (k - 1);
let state = packBits(seed);
// Avoid degenerate zero state: mix in seed length
if (state === 0) state = imul32(k, 0x12345678) ^ 0xdeadbeef;
const output = [];
for (let step = 0; step < numOut; step++) {
output.push(hardFunc(state, k));
state = nextState(state);
}
return output;
}
// Statistical tests -------------------------------------------------------
// Bias test: fraction of ones should be within 15% of 0.5.
function biasTest(bits) {
const ones = bits.reduce((s, b) => s + b, 0);
const ratio = ones / bits.length;
return { ones, zeros: bits.length - ones, ratio, pass: Math.abs(ratio - 0.5) < 0.15 };
}
// Runs test: number of maximal runs of identical bits.
// Expected under uniform randomness: 1 + 2*n*p*(1-p).
function runsTest(bits) {
if (bits.length < 2) return { runs: 1, expected: 1, pass: true };
let runs = 1;
for (let i = 1; i < bits.length; i++) if (bits[i] !== bits[i - 1]) runs++;
const n = bits.length;
const p = bits.filter(b => b).length / n;
const expected = 1 + 2 * n * p * (1 - p);
return { runs, expected: Math.round(expected), pass: runs >= expected * 0.5 };
}
// UI ----------------------------------------------------------------------
const slider = document.getElementById('slider');
const seedLenLabel = document.getElementById('seedLen');
const seedRow = document.getElementById('seedRow');
const outLenLabel = document.getElementById('outLen');
const bitsDiv = document.getElementById('bits');
const statsDiv = document.getElementById('stats');
let seedBits = Array(8).fill(0);
seedBits[1] = 1; seedBits[3] = 1; seedBits[5] = 1; // default non-zero seed
function buildSeedRow() {
seedRow.innerHTML = '';
seedBits.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'sbit ' + (b ? 'one' : 'zero');
d.textContent = b;
d.title = fmt('Seed bit %i% — click to flip', { i: i });
d.addEventListener('click', () => {
seedBits[i] ^= 1;
buildSeedRow();
});
seedRow.appendChild(d);
});
}
function generate() {
const output = prg(seedBits);
outLenLabel.textContent = output.length;
bitsDiv.innerHTML = '';
output.forEach(b => {
const d = document.createElement('div');
d.className = 'obit ' + (b ? 'one' : 'zero');
d.textContent = b;
bitsDiv.appendChild(d);
});
const bias = biasTest(output);
const runs = runsTest(output);
const pct = (bias.ratio * 100).toFixed(1);
const barW = Math.round(bias.ratio * 180);
statsDiv.innerHTML =
fmt('<strong>Bias test</strong>: ones=%ones%, zeros=%zeros% (%pct%% ones) <span class="bar-bg"><span class="bar-fill" style="width:%barW%px"></span></span> ideal: 50%<br>%v%<br><br><strong>Runs test</strong>: %runs% runs (expected ≈ ≈ %expected%)<br>%v2%<br><br><em>Expansion: %length% seed bits → %length2% output bits (%v3%× stretch).</em> Derandomization replaces %length2% random coin flips with just %length% truly random bits + deterministic hard-function evaluation.', { ones: bias.ones, zeros: bias.zeros, pct: pct, barW: barW, v: bias.pass
? '<span class="ok">✓ Bias OK — output is well-balanced.</span>'
: '<span class="warn">⚠ Bias skewed — try a different seed.</span>', runs: runs.runs, expected: runs.expected, v2: runs.pass
? '<span class="ok">✓ Runs OK — bits alternate naturally.</span>'
: '<span class="warn">⚠ Too few runs — bits are too uniform.</span>', length: seedBits.length, length2: output.length, v3: (output.length / seedBits.length).toFixed(0) });
console.log(
'seed:', seedBits.join(''),
'| output:', output.length, 'bits',
'| ones:', bias.ones, '| zeros:', bias.zeros,
'| runs:', runs.runs, '| expected:', runs.expected
);
}
slider.addEventListener('input', () => {
const k = parseInt(slider.value);
seedLenLabel.textContent = k;
seedBits = Array(k).fill(0);
// Set alternating default
for (let i = 0; i < k; i++) seedBits[i] = i % 2;
buildSeedRow();
bitsDiv.innerHTML = '';
statsDiv.innerHTML = '';
outLenLabel.textContent = '0';
});
document.getElementById('genBtn').addEventListener('click', generate);
document.getElementById('randBtn').addEventListener('click', () => {
seedBits = seedBits.map(() => Math.random() < 0.5 ? 0 : 1);
buildSeedRow();
});
buildSeedRow();
The demo above shows a toy PRG built from a hard function. Pick a seed length and watch the generator stretch it into a longer bit string. The bias test checks whether the output bits are suspiciously skewed — a true PRG passes it even though the bits were generated deterministically from a short seed. This is exactly how derandomization works: replace the coin tosses in a BPP algorithm with PRG output seeded by a short truly-random seed, then enumerate all 2seed length seeds deterministically.
Comments
Loading comments...