Skip to content

Instantly share code, notes, and snippets.

@shaal
Last active April 16, 2026 14:05
Show Gist options
  • Select an option

  • Save shaal/51dbafb8a7af28274ab2e77f910aef3a to your computer and use it in GitHub Desktop.

Select an option

Save shaal/51dbafb8a7af28274ab2e77f910aef3a to your computer and use it in GitHub Desktop.
Miller-Rabin demo (quick prime finder)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Miller-Rabin Primality Test</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;700&family=Space+Grotesk:wght@400;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0a0f;
--surface: #12121a;
--border: #1e1e2e;
--text: #e0e0e8;
--dim: #6a6a80;
--accent: #7c6aff;
--accent-glow: #7c6aff66;
--green: #3dffa0;
--green-glow: #3dffa044;
--red: #ff4a6a;
--red-glow: #ff4a6a44;
--amber: #ffb347;
--amber-glow: #ffb34744;
--cyan: #4af0ff;
--cyan-glow: #4af0ff33;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Space Grotesk', sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* Starfield background */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(1px 1px at 10% 20%, rgba(124,106,255,0.3), transparent),
radial-gradient(1px 1px at 30% 60%, rgba(61,255,160,0.2), transparent),
radial-gradient(1px 1px at 50% 10%, rgba(74,240,255,0.3), transparent),
radial-gradient(1px 1px at 70% 80%, rgba(255,179,71,0.2), transparent),
radial-gradient(1px 1px at 90% 40%, rgba(124,106,255,0.3), transparent),
radial-gradient(1px 1px at 15% 85%, rgba(61,255,160,0.2), transparent),
radial-gradient(1px 1px at 80% 15%, rgba(74,240,255,0.2), transparent),
radial-gradient(1px 1px at 45% 45%, rgba(255,74,106,0.2), transparent),
radial-gradient(1px 1px at 60% 90%, rgba(124,106,255,0.2), transparent),
radial-gradient(1px 1px at 25% 35%, rgba(255,179,71,0.15), transparent);
pointer-events: none;
z-index: 0;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: 3rem 1.5rem;
position: relative;
z-index: 1;
}
header {
text-align: center;
margin-bottom: 3rem;
}
h1 {
font-size: 2.6rem;
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--accent), var(--cyan), var(--green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
header p {
color: var(--dim);
font-size: 1.05rem;
max-width: 520px;
margin: 0 auto;
line-height: 1.6;
}
/* Input section */
.input-section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
}
.input-section label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.6rem;
}
.input-row {
display: flex;
gap: 1rem;
align-items: stretch;
}
.input-row input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.9rem 1.2rem;
color: var(--text);
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.input-row input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.input-row input::placeholder { color: #333348; }
.btn {
padding: 0.9rem 1.8rem;
border: none;
border-radius: 10px;
font-family: 'Space Grotesk', sans-serif;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
box-shadow: 0 0 20px var(--accent-glow), 0 4px 15px rgba(0,0,0,0.3);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.presets {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.preset-btn {
background: var(--bg);
border: 1px solid var(--border);
color: var(--dim);
padding: 0.4rem 0.9rem;
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.preset-btn:hover {
border-color: var(--accent);
color: var(--text);
}
/* Decomposition */
.decomposition {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
display: none;
}
.decomposition.visible { display: block; animation: fadeSlideIn 0.4s ease; }
.section-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 1.2rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title .step-num {
background: var(--accent);
color: #fff;
width: 22px;
height: 22px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
}
.math-display {
font-family: 'JetBrains Mono', monospace;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.2rem 1.5rem;
font-size: 0.9rem;
line-height: 1.8;
overflow-x: auto;
}
.math-display .label {
color: var(--dim);
font-size: 0.75rem;
}
.math-display .value {
color: var(--cyan);
word-break: break-all;
}
.math-display .operator { color: var(--dim); }
.math-display .highlight-s { color: var(--amber); }
.math-display .highlight-d { color: var(--green); }
.big-number {
color: var(--cyan);
word-break: break-all;
line-height: 1.6;
position: relative;
}
.big-number .digit-group {
display: inline;
transition: opacity 0.3s;
}
.big-number .digit-group:nth-child(even) { color: #3ac8e8; }
.big-number .digit-group:nth-child(odd) { color: var(--cyan); }
.digit-count {
color: var(--dim);
font-size: 0.75rem;
margin-top: 0.4rem;
}
/* Witness rounds */
.rounds-container {
display: none;
}
.rounds-container.visible { display: block; }
.witness-round {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
margin-bottom: 1.5rem;
overflow: hidden;
animation: fadeSlideIn 0.5s ease;
}
.round-header {
padding: 1.2rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.round-header:hover { background: #16162080; }
.round-header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.round-number {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
font-weight: 600;
color: var(--dim);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.35rem 0.65rem;
min-width: 60px;
text-align: center;
}
.witness-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text);
}
.witness-value span { color: var(--accent); }
.round-verdict {
font-size: 0.8rem;
font-weight: 600;
padding: 0.3rem 0.8rem;
border-radius: 20px;
}
.verdict-pass {
background: var(--green-glow);
color: var(--green);
border: 1px solid var(--green);
}
.verdict-fail {
background: var(--red-glow);
color: var(--red);
border: 1px solid var(--red);
}
.round-body {
border-top: 1px solid var(--border);
padding: 1.5rem;
display: none;
}
.round-body.open { display: block; }
/* Steps pipeline */
.step-pipeline {
position: relative;
padding-left: 1.5rem;
}
.step-pipeline::before {
content: '';
position: absolute;
left: 5px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--border);
}
.pipeline-step {
position: relative;
margin-bottom: 1rem;
padding-left: 1.2rem;
animation: fadeSlideIn 0.3s ease;
}
.pipeline-step:last-child { margin-bottom: 0; }
.pipeline-step::before {
content: '';
position: absolute;
left: -1.5rem;
top: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--border);
border: 2px solid var(--bg);
}
.pipeline-step.step-active::before { background: var(--amber); box-shadow: 0 0 8px var(--amber-glow); }
.pipeline-step.step-pass::before { background: var(--green); box-shadow: 0 0 8px var(--green-glow); }
.pipeline-step.step-fail::before { background: var(--red); box-shadow: 0 0 8px var(--red-glow); }
.pipeline-step.step-neutral::before { background: var(--dim); }
.step-label {
font-size: 0.78rem;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.3rem;
}
.step-math {
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
color: var(--text);
word-break: break-all;
line-height: 1.6;
}
.step-math .eq { color: var(--dim); }
.step-math .val { color: var(--cyan); }
.step-math .cond { color: var(--amber); font-style: italic; }
.step-math .yes { color: var(--green); }
.step-math .no { color: var(--red); }
/* Final result */
.final-result {
text-align: center;
padding: 2.5rem 2rem;
border-radius: 16px;
margin-top: 2rem;
display: none;
animation: fadeSlideIn 0.5s ease;
}
.final-result.visible { display: block; }
.final-result.is-prime {
background: linear-gradient(135deg, #0d2218, #0a1a20);
border: 1px solid var(--green);
box-shadow: 0 0 40px var(--green-glow), inset 0 0 40px var(--green-glow);
}
.final-result.is-composite {
background: linear-gradient(135deg, #220d15, #1a0a0e);
border: 1px solid var(--red);
box-shadow: 0 0 40px var(--red-glow), inset 0 0 40px var(--red-glow);
}
.result-icon {
font-size: 3rem;
margin-bottom: 0.8rem;
}
.result-text {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 0.4rem;
}
.is-prime .result-text { color: var(--green); }
.is-composite .result-text { color: var(--red); }
.result-sub {
color: var(--dim);
font-size: 0.9rem;
max-width: 440px;
margin: 0 auto;
line-height: 1.5;
}
.confidence {
margin-top: 1.2rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
}
.confidence-bar {
width: 200px;
height: 6px;
background: var(--border);
border-radius: 3px;
margin: 0.5rem auto 0;
overflow: hidden;
}
.confidence-fill {
height: 100%;
background: var(--green);
border-radius: 3px;
transition: width 0.6s ease;
}
/* Running indicator */
.running-indicator {
display: none;
align-items: center;
justify-content: center;
gap: 0.6rem;
padding: 1rem;
color: var(--amber);
font-size: 0.9rem;
}
.running-indicator.visible { display: flex; }
.spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--amber);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Squaring grid */
.squaring-grid {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.2rem 1rem;
align-items: baseline;
margin-top: 0.5rem;
}
.sq-iter {
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
color: var(--dim);
}
.sq-val {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: var(--text);
word-break: break-all;
}
.sq-check {
font-size: 0.78rem;
font-weight: 600;
}
.sq-check.hit { color: var(--green); }
.sq-check.miss { color: var(--dim); }
/* ── Pi Hunt Section ─────────────────────── */
.pi-divider {
text-align: center;
margin: 4rem 0 3rem;
position: relative;
}
.pi-divider::before {
content: '';
position: absolute;
left: 0; right: 0; top: 50%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent);
}
.pi-divider-label {
position: relative;
background: var(--bg);
padding: 0 1.5rem;
color: var(--dim);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.pi-header {
text-align: center;
margin-bottom: 2rem;
}
.pi-symbol {
font-size: 5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--amber), #ff6b6b, var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
filter: drop-shadow(0 0 30px var(--amber-glow));
}
.pi-header h2 {
font-size: 1.6rem;
font-weight: 700;
color: var(--text);
margin-bottom: 0.4rem;
}
.pi-header p {
color: var(--dim);
font-size: 0.95rem;
max-width: 560px;
margin: 0 auto;
line-height: 1.6;
}
/* Pi digit display */
.pi-digit-display {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
}
.pi-digit-display .section-title { margin-bottom: 0.8rem; }
.pi-digits-scroll {
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
line-height: 1.9;
max-height: 140px;
overflow-y: auto;
word-break: break-all;
padding: 0.8rem;
background: var(--bg);
border-radius: 10px;
border: 1px solid var(--border);
}
.pi-digits-scroll .d-consumed { color: var(--dim); opacity: 0.4; }
.pi-digits-scroll .d-current { color: var(--amber); font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.pi-digits-scroll .d-upcoming { color: #333348; }
.pi-digits-scroll .d-prime { color: var(--green); font-weight: 700; }
.pi-digits-scroll .d-decimal { color: var(--dim); font-weight: 400; }
/* Controls */
.pi-controls {
display: flex;
gap: 0.8rem;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.pi-controls .btn { font-size: 0.85rem; padding: 0.7rem 1.4rem; }
.btn-stop {
background: var(--red);
color: #fff;
}
.btn-stop:hover {
box-shadow: 0 0 20px var(--red-glow);
}
.speed-control {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
font-size: 0.8rem;
color: var(--dim);
}
.speed-control select {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
padding: 0.4rem 0.6rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
outline: none;
cursor: pointer;
}
.pi-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 0.8rem;
margin-bottom: 1.5rem;
}
.pi-stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1rem 1.2rem;
text-align: center;
}
.pi-stat-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.6rem;
font-weight: 700;
color: var(--text);
}
.pi-stat-label {
font-size: 0.72rem;
color: var(--dim);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 0.2rem;
}
.pi-stat.stat-primes .pi-stat-value { color: var(--green); }
.pi-stat.stat-composites .pi-stat-value { color: var(--red); }
.pi-stat.stat-skipped .pi-stat-value { color: var(--dim); }
.pi-stat.stat-position .pi-stat-value { color: var(--amber); }
/* Results log */
.pi-results-log {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
}
.pi-results-log .section-title {
padding: 1.2rem 1.5rem 0;
margin-bottom: 0;
}
.pi-log-list {
max-height: 500px;
overflow-y: auto;
padding: 0.8rem;
}
.pi-log-entry {
display: grid;
grid-template-columns: 55px 1fr auto;
gap: 0.8rem;
align-items: center;
padding: 0.7rem 0.8rem;
border-radius: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
animation: fadeSlideIn 0.3s ease;
transition: background 0.2s;
}
.pi-log-entry:hover { background: #16162080; }
.pi-log-pos {
color: var(--dim);
text-align: right;
font-size: 0.72rem;
}
.pi-log-number {
color: var(--text);
word-break: break-all;
line-height: 1.4;
}
.pi-log-badge {
font-size: 0.7rem;
font-weight: 600;
padding: 0.25rem 0.6rem;
border-radius: 20px;
white-space: nowrap;
}
.badge-prime {
background: var(--green-glow);
color: var(--green);
border: 1px solid var(--green);
}
.badge-composite {
background: #1a1a2a;
color: var(--dim);
border: 1px solid var(--border);
}
.badge-even {
background: transparent;
color: #333;
border: 1px solid #1a1a24;
font-size: 0.65rem;
}
.badge-testing {
background: var(--amber-glow);
color: var(--amber);
border: 1px solid var(--amber);
}
/* Prime celebration */
.pi-log-entry.is-prime-entry {
background: linear-gradient(90deg, var(--green-glow), transparent);
border: 1px solid var(--green);
}
.pi-log-entry.is-prime-entry .pi-log-number {
color: var(--green);
font-weight: 600;
}
/* Prime discovery flash */
@keyframes primeFlash {
0% { box-shadow: 0 0 0 0 var(--green); }
50% { box-shadow: 0 0 30px 5px var(--green-glow); }
100% { box-shadow: 0 0 0 0 transparent; }
}
.prime-flash {
animation: primeFlash 1s ease;
}
/* Responsive */
@media (max-width: 600px) {
h1 { font-size: 1.8rem; }
.input-row { flex-direction: column; }
.container { padding: 1.5rem 1rem; }
.pi-symbol { font-size: 3.5rem; }
.pi-stats { grid-template-columns: repeat(2, 1fr); }
.pi-log-entry { grid-template-columns: 45px 1fr auto; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Miller-Rabin</h1>
<p>Watch the probabilistic primality test work step-by-step on numbers with hundreds of digits.</p>
</header>
<div class="input-section">
<label>Number to test</label>
<div class="input-row">
<input type="text" id="numberInput" placeholder="Enter a large number or try a preset..." spellcheck="false" />
<button class="btn btn-primary" id="runBtn" onclick="runTest()">Test</button>
</div>
<div class="presets">
<button class="preset-btn" onclick="loadPreset('mersenne31')">2<sup>31</sup> - 1</button>
<button class="preset-btn" onclick="loadPreset('mersenne61')">2<sup>61</sup> - 1</button>
<button class="preset-btn" onclick="loadPreset('mersenne127')">2<sup>127</sup> - 1</button>
<button class="preset-btn" onclick="loadPreset('mersenne521')">2<sup>521</sup> - 1</button>
<button class="preset-btn" onclick="loadPreset('rsa100')">RSA-100</button>
<button class="preset-btn" onclick="loadPreset('carmichael')">Carmichael 561</button>
<button class="preset-btn" onclick="loadPreset('big_composite')">Big Composite</button>
<button class="preset-btn" onclick="loadPreset('random_prime')">Random ~200 digits</button>
</div>
</div>
<div class="decomposition" id="decomposition">
<div class="section-title"><span class="step-num">1</span> Decompose n - 1 as 2<sup>s</sup> &middot; d</div>
<div class="math-display" id="decompDisplay"></div>
</div>
<div class="running-indicator" id="runningIndicator">
<div class="spinner"></div>
<span>Testing witnesses...</span>
</div>
<div class="rounds-container" id="roundsContainer"></div>
<div class="final-result" id="finalResult">
<div class="result-icon" id="resultIcon"></div>
<div class="result-text" id="resultText"></div>
<div class="result-sub" id="resultSub"></div>
<div class="confidence" id="confidence"></div>
</div>
<!-- ── Pi Prime Hunt ───────────────────────── -->
<div class="pi-divider">
<span class="pi-divider-label">Part II</span>
</div>
<div class="pi-header">
<div class="pi-symbol">&pi;</div>
<h2>The Pi Prime Hunt</h2>
<p>Take the first N digits of &pi; as an integer. Is it prime? Keep adding digits and test each odd number with Miller-Rabin. How far can we go?</p>
</div>
<div class="pi-controls">
<button class="btn btn-primary" id="piStartBtn" onclick="startPiHunt()">Start Hunt</button>
<button class="btn btn-stop" id="piStopBtn" onclick="stopPiHunt()" style="display:none">Stop</button>
<div class="speed-control">
<span>Speed:</span>
<select id="piSpeed">
<option value="slow">Slow</option>
<option value="normal" selected>Normal</option>
<option value="fast">Fast</option>
<option value="ludicrous">Ludicrous</option>
</select>
</div>
</div>
<div class="pi-stats">
<div class="pi-stat stat-position">
<div class="pi-stat-value" id="piStatPos">0</div>
<div class="pi-stat-label">Digits scanned</div>
</div>
<div class="pi-stat stat-primes">
<div class="pi-stat-value" id="piStatPrimes">0</div>
<div class="pi-stat-label">Primes found</div>
</div>
<div class="pi-stat stat-composites">
<div class="pi-stat-value" id="piStatComposites">0</div>
<div class="pi-stat-label">Composite</div>
</div>
<div class="pi-stat stat-skipped">
<div class="pi-stat-value" id="piStatSkipped">0</div>
<div class="pi-stat-label">Even (skipped)</div>
</div>
</div>
<div class="pi-digit-display" id="piDigitDisplay" style="display:none">
<div class="section-title">&pi; digits</div>
<div class="pi-digits-scroll" id="piDigitsScroll"></div>
</div>
<div class="pi-results-log" id="piResultsLog" style="display:none">
<div class="section-title" style="padding: 1.2rem 1.5rem 0.6rem;">Results</div>
<div class="pi-log-list" id="piLogList"></div>
</div>
</div>
<script>
// BigInt helpers
function bigRandom(min, max) {
const range = max - min;
const bits = range.toString(2).length;
const bytes = Math.ceil(bits / 8);
let result;
do {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
result = BigInt('0x' + Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(''));
result = result % range;
} while (result < 0n);
return result + min;
}
function modPow(base, exp, mod) {
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = (result * base) % mod;
exp = exp / 2n;
base = (base * base) % mod;
}
return result;
}
function formatBigNum(n) {
const s = n.toString();
if (s.length <= 20) return `<span class="digit-group">${s}</span>`;
const groups = [];
for (let i = 0; i < s.length; i += 4) {
groups.push(`<span class="digit-group">${s.slice(i, i + 4)}</span>`);
}
return groups.join('<wbr>');
}
function truncNum(n, maxLen = 40) {
const s = n.toString();
if (s.length <= maxLen) return s;
const half = Math.floor((maxLen - 3) / 2);
return s.slice(0, half) + '...' + s.slice(-half);
}
// Presets
const presets = {
mersenne31: (2n ** 31n - 1n).toString(),
mersenne61: (2n ** 61n - 1n).toString(),
mersenne127: (2n ** 127n - 1n).toString(),
mersenne521: (2n ** 521n - 1n).toString(),
rsa100: '1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139',
carmichael: '561',
big_composite: (2n ** 521n - 1n - 6n).toString(),
random_prime: null,
};
function generateProbablePrime(digits) {
// generate a random odd number of ~digits length and return it
// (it might not be prime, but that's fine — the test will reveal that)
let s = '';
s += (Math.floor(Math.random() * 9) + 1).toString();
for (let i = 1; i < digits; i++) s += Math.floor(Math.random() * 10).toString();
let n = BigInt(s);
if (n % 2n === 0n) n += 1n;
return n.toString();
}
function loadPreset(key) {
const input = document.getElementById('numberInput');
if (key === 'random_prime') {
input.value = generateProbablePrime(200);
} else {
input.value = presets[key];
}
}
// State
let isRunning = false;
async function runTest() {
if (isRunning) return;
const input = document.getElementById('numberInput').value.trim().replace(/[^0-9]/g, '');
if (!input || input === '0' || input === '1') return;
const n = BigInt(input);
if (n < 2n) return;
if (n === 2n || n === 3n) {
showResult(true, n, 0, 0);
return;
}
if (n % 2n === 0n) {
showResult(false, n, 0, 0, 'It is even.');
return;
}
isRunning = true;
document.getElementById('runBtn').disabled = true;
document.getElementById('roundsContainer').innerHTML = '';
document.getElementById('roundsContainer').classList.remove('visible');
document.getElementById('finalResult').classList.remove('visible');
// Decompose n-1 = 2^s * d
let d = n - 1n;
let s = 0n;
while (d % 2n === 0n) {
d /= 2n;
s++;
}
showDecomposition(n, s, d);
await sleep(600);
document.getElementById('runningIndicator').classList.add('visible');
document.getElementById('roundsContainer').classList.add('visible');
const k = 12; // number of witnesses
let allPassed = true;
const witnesses = [];
// Pick witnesses
if (n < 100n) {
// small numbers: use 2, 3, etc
for (let a = 2n; a < n - 1n && witnesses.length < k; a++) witnesses.push(a);
} else {
// First few deterministic witnesses, then random
const fixed = [2n, 3n, 5n, 7n, 11n, 13n];
for (const a of fixed) { if (a < n - 1n) witnesses.push(a); }
while (witnesses.length < k) {
witnesses.push(bigRandom(14n, n - 2n));
}
}
for (let i = 0; i < witnesses.length; i++) {
const a = witnesses[i];
const result = await testWitness(a, d, s, n, i);
if (!result.passed) {
allPassed = false;
// Composite — stop early
document.getElementById('runningIndicator').classList.remove('visible');
showResult(false, n, i + 1, k, `Witness a = ${truncNum(a)} proved composite.`);
isRunning = false;
document.getElementById('runBtn').disabled = false;
return;
}
await sleep(200);
}
document.getElementById('runningIndicator').classList.remove('visible');
showResult(true, n, k, k);
isRunning = false;
document.getElementById('runBtn').disabled = false;
}
function showDecomposition(n, s, d) {
const el = document.getElementById('decomposition');
el.classList.add('visible');
const nStr = n.toString();
const nm1Str = (n - 1n).toString();
const dStr = d.toString();
el.querySelector('#decompDisplay').innerHTML = `
<div><span class="label">n =</span> <span class="big-number">${formatBigNum(n)}</span></div>
<div class="digit-count">${nStr.length} digits</div>
<br>
<div><span class="label">n - 1 =</span> <span class="big-number">${formatBigNum(n - 1n)}</span></div>
<br>
<div>
<span class="label">n - 1 =</span>
<span class="operator">2</span><sup class="highlight-s">${s.toString()}</sup>
<span class="operator">&times;</span>
<span class="highlight-d">${truncNum(d, 80)}</span>
</div>
<div style="margin-top: 0.4rem;">
<span class="label">s =</span> <span class="highlight-s">${s.toString()}</span>
&nbsp;&nbsp;
<span class="label">d =</span> <span class="highlight-d">${dStr.length > 60 ? dStr.length + '-digit odd number' : dStr}</span>
</div>
`;
}
async function testWitness(a, d, s, n, roundIdx) {
const container = document.getElementById('roundsContainer');
const round = document.createElement('div');
round.className = 'witness-round';
container.appendChild(round);
const header = document.createElement('div');
header.className = 'round-header';
header.innerHTML = `
<div class="round-header-left">
<span class="round-number">Round ${roundIdx + 1}</span>
<span class="witness-value">a = <span>${truncNum(a, 50)}</span></span>
</div>
`;
round.appendChild(header);
const body = document.createElement('div');
body.className = 'round-body open';
round.appendChild(body);
const pipeline = document.createElement('div');
pipeline.className = 'step-pipeline';
body.appendChild(pipeline);
header.onclick = () => body.classList.toggle('open');
// Step 1: Compute a^d mod n
const x0 = modPow(a, d, n);
const step1 = addPipelineStep(pipeline, 'Initial computation', `
a<sup>d</sup> mod n <span class="eq">=</span> ${truncNum(a, 20)}<sup>${truncNum(d, 20)}</sup> mod ${truncNum(n, 20)}
<br><span class="eq">=</span> <span class="val">${truncNum(x0, 60)}</span>
`);
await sleep(300);
// Check if x0 = 1 or x0 = n-1
if (x0 === 1n) {
step1.classList.add('step-pass');
addPipelineStep(pipeline, 'Result', `
<span class="val">${truncNum(x0)}</span> <span class="eq">===</span> 1 <span class="yes">&rarr; probably prime</span>
`, 'step-pass');
addVerdict(round, header, true);
return { passed: true, reason: 'x0 === 1' };
}
if (x0 === n - 1n) {
step1.classList.add('step-pass');
addPipelineStep(pipeline, 'Result', `
<span class="val">${truncNum(x0)}</span> <span class="eq">===</span> n - 1 <span class="yes">&rarr; probably prime</span>
`, 'step-pass');
addVerdict(round, header, true);
return { passed: true, reason: 'x0 === n-1' };
}
step1.classList.add('step-neutral');
addPipelineStep(pipeline, 'Check', `
<span class="val">${truncNum(x0)}</span>
<span class="cond">&ne; 1 and &ne; n-1 &rarr; proceed to squaring</span>
`, 'step-neutral');
await sleep(200);
// Squaring phase
const sqStep = addPipelineStep(pipeline, `Repeated squaring (up to s-1 = ${(s - 1n).toString()} times)`, '', 'step-active');
let x = x0;
let found = false;
const grid = document.createElement('div');
grid.className = 'squaring-grid';
sqStep.querySelector('.step-math').appendChild(grid);
for (let r = 1n; r < s; r++) {
x = (x * x) % n;
const iterEl = document.createElement('span');
iterEl.className = 'sq-iter';
iterEl.textContent = `r=${r}`;
const valEl = document.createElement('span');
valEl.className = 'sq-val';
valEl.textContent = truncNum(x, 60);
const checkEl = document.createElement('span');
checkEl.className = 'sq-check';
if (x === n - 1n) {
checkEl.classList.add('hit');
checkEl.textContent = '= n-1 ✓';
found = true;
} else if (x === 1n) {
checkEl.classList.add('miss');
checkEl.textContent = '= 1 (non-trivial sqrt!)';
} else {
checkEl.classList.add('miss');
checkEl.textContent = '—';
}
grid.appendChild(iterEl);
grid.appendChild(valEl);
grid.appendChild(checkEl);
await sleep(150);
if (found) break;
}
if (found) {
sqStep.classList.remove('step-active');
sqStep.classList.add('step-pass');
addPipelineStep(pipeline, 'Conclusion', `
Found x <span class="eq">===</span> n - 1 during squaring <span class="yes">&rarr; probably prime</span>
`, 'step-pass');
addVerdict(round, header, true);
return { passed: true, reason: 'found n-1 in squaring' };
} else {
sqStep.classList.remove('step-active');
sqStep.classList.add('step-fail');
addPipelineStep(pipeline, 'Conclusion', `
Never reached 1 or n - 1 <span class="no">&rarr; COMPOSITE (definitive)</span>
`, 'step-fail');
addVerdict(round, header, false);
return { passed: false, reason: 'never hit n-1' };
}
}
function addPipelineStep(pipeline, label, mathHTML, cls = '') {
const step = document.createElement('div');
step.className = `pipeline-step ${cls}`;
step.innerHTML = `
<div class="step-label">${label}</div>
<div class="step-math">${mathHTML}</div>
`;
pipeline.appendChild(step);
return step;
}
function addVerdict(round, header, passed) {
const badge = document.createElement('span');
badge.className = `round-verdict ${passed ? 'verdict-pass' : 'verdict-fail'}`;
badge.textContent = passed ? 'PASS' : 'COMPOSITE';
header.appendChild(badge);
}
function showResult(isPrime, n, rounds, totalRounds, reason) {
const el = document.getElementById('finalResult');
el.className = `final-result visible ${isPrime ? 'is-prime' : 'is-composite'}`;
document.getElementById('resultIcon').textContent = isPrime ? '◆' : '✕';
document.getElementById('resultText').textContent = isPrime ? 'Probably Prime' : 'Composite';
if (isPrime && rounds > 0) {
const errorProb = Math.pow(4, -rounds);
const nines = Math.min(Math.floor(-Math.log10(errorProb)), 99);
document.getElementById('resultSub').textContent =
`Passed all ${rounds} witness rounds.`;
document.getElementById('confidence').innerHTML = `
Confidence: ${nines > 50 ? '>99.99...%' : (100 * (1 - errorProb)).toFixed(Math.min(nines + 1, 15)) + '%'}
<br><span style="color: var(--dim)">Error probability &le; 4<sup>-${rounds}</sup> &asymp; ${errorProb.toExponential(2)}</span>
<div class="confidence-bar"><div class="confidence-fill" style="width: 100%"></div></div>
`;
} else if (isPrime) {
document.getElementById('resultSub').textContent = 'Trivially prime.';
document.getElementById('confidence').innerHTML = `Deterministic — 100% certain`;
} else {
document.getElementById('resultSub').textContent = reason || 'Definitively not prime.';
document.getElementById('confidence').innerHTML =
`<span style="color: var(--red)">Composite — no probability involved. This is certain.</span>`;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Enter key
document.getElementById('numberInput').addEventListener('keydown', e => {
if (e.key === 'Enter') runTest();
});
// ── Pi Prime Hunt ─────────────────────────────────────
// 1000 digits of pi (no decimal point)
const PI_DIGITS = '3141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127145263560827785771342757789609173637178721468440901224953430146549585371050792279689258923542019956112129021960864034418159813629774771309960518707211349999998372978049951059731732816096318595024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303598253490428755468731159562863882353787593751957781857780532171226806613001927876611195909216420198';
let piHuntRunning = false;
let piHuntStopped = false;
let piPrimesFound = [];
// Fast (non-animated) Miller-Rabin for the pi hunt
function millerRabinFast(n, k = 12) {
if (n < 2n) return false;
if (n === 2n || n === 3n) return true;
if (n % 2n === 0n) return false;
let d = n - 1n;
let s = 0n;
while (d % 2n === 0n) { d /= 2n; s++; }
const witnesses = [];
const fixed = [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n];
for (const a of fixed) {
if (a < n - 1n) witnesses.push(a);
if (witnesses.length >= k) break;
}
while (witnesses.length < k && n > 40n) {
witnesses.push(bigRandom(38n, n - 2n));
}
for (const a of witnesses) {
let x = modPow(a, d, n);
if (x === 1n || x === n - 1n) continue;
let found = false;
for (let r = 1n; r < s; r++) {
x = (x * x) % n;
if (x === n - 1n) { found = true; break; }
}
if (!found) return false;
}
return true;
}
function getSpeedDelay() {
const v = document.getElementById('piSpeed').value;
return { slow: 500, normal: 150, fast: 30, ludicrous: 1 }[v];
}
function renderPiDigits(position, primePositions) {
const scroll = document.getElementById('piDigitsScroll');
// Show a window of digits around current position
const displayLen = Math.min(PI_DIGITS.length, Math.max(position + 100, 200));
let html = '<span class="d-decimal">&pi; = </span>';
for (let i = 0; i < displayLen; i++) {
if (i === 1) html += '<span class="d-decimal">.</span>'; // decimal point after first digit
if (i < position) {
// check if this position was a prime endpoint
if (primePositions.has(i + 1)) {
html += `<span class="d-prime">${PI_DIGITS[i]}</span>`;
} else {
html += `<span class="d-consumed">${PI_DIGITS[i]}</span>`;
}
} else if (i === position) {
html += `<span class="d-current">${PI_DIGITS[i]}</span>`;
} else {
html += `<span class="d-upcoming">${PI_DIGITS[i]}</span>`;
}
// spacing every 10 digits for readability
if ((i + 1) % 10 === 0 && i > 0 && i < displayLen - 1) html += ' ';
if ((i + 1) % 50 === 0 && i > 0 && i < displayLen - 1) html += '<br>';
}
if (displayLen < PI_DIGITS.length) html += '<span class="d-upcoming">...</span>';
scroll.innerHTML = html;
// Auto-scroll to keep the current position visible
const currentEl = scroll.querySelector('.d-current');
if (currentEl) currentEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
function addPiLogEntry(pos, numberStr, status, isPrime) {
const list = document.getElementById('piLogList');
const entry = document.createElement('div');
entry.className = `pi-log-entry ${isPrime ? 'is-prime-entry prime-flash' : ''}`;
let badgeClass, badgeText;
if (status === 'even') { badgeClass = 'badge-even'; badgeText = 'EVEN'; }
else if (status === 'prime') { badgeClass = 'badge-prime'; badgeText = 'PRIME'; }
else if (status === 'composite') { badgeClass = 'badge-composite'; badgeText = 'COMP'; }
else { badgeClass = 'badge-testing'; badgeText = 'TEST...'; }
// Truncate display for very long numbers
let displayNum;
if (numberStr.length <= 50) {
displayNum = numberStr;
} else {
displayNum = numberStr.slice(0, 24) + '<span style="color:var(--dim)">...' + numberStr.length + ' digits...</span>' + numberStr.slice(-12);
}
entry.innerHTML = `
<div class="pi-log-pos">${pos} digit${pos > 1 ? 's' : ''}</div>
<div class="pi-log-number">${displayNum}</div>
<div><span class="pi-log-badge ${badgeClass}">${badgeText}</span></div>
`;
// Insert at top for newest-first
list.insertBefore(entry, list.firstChild);
return entry;
}
function updatePiStats(pos, primes, composites, skipped) {
document.getElementById('piStatPos').textContent = pos;
document.getElementById('piStatPrimes').textContent = primes;
document.getElementById('piStatComposites').textContent = composites;
document.getElementById('piStatSkipped').textContent = skipped;
}
async function startPiHunt() {
if (piHuntRunning) return;
piHuntRunning = true;
piHuntStopped = false;
document.getElementById('piStartBtn').style.display = 'none';
document.getElementById('piStopBtn').style.display = '';
document.getElementById('piDigitDisplay').style.display = '';
document.getElementById('piResultsLog').style.display = '';
document.getElementById('piLogList').innerHTML = '';
let primesCount = 0, compositesCount = 0, skippedCount = 0;
const primePositions = new Set();
piPrimesFound = [];
for (let pos = 1; pos <= PI_DIGITS.length; pos++) {
if (piHuntStopped) break;
const numStr = PI_DIGITS.slice(0, pos);
const n = BigInt(numStr);
const lastDigit = parseInt(PI_DIGITS[pos - 1]);
renderPiDigits(pos, primePositions);
if (pos > 1 && lastDigit % 2 === 0) {
// Even — skip
skippedCount++;
addPiLogEntry(pos, numStr, 'even', false);
updatePiStats(pos, primesCount, compositesCount, skippedCount);
const delay = getSpeedDelay();
if (delay > 1) await sleep(Math.max(delay / 3, 1));
continue;
}
// Also skip if ends in 5 and length > 1 (divisible by 5)
if (pos > 1 && lastDigit === 5) {
compositesCount++;
addPiLogEntry(pos, numStr, 'composite', false);
updatePiStats(pos, primesCount, compositesCount, skippedCount);
const delay = getSpeedDelay();
if (delay > 1) await sleep(Math.max(delay / 3, 1));
continue;
}
// Odd number — run Miller-Rabin
const testEntry = addPiLogEntry(pos, numStr, 'testing', false);
updatePiStats(pos, primesCount, compositesCount, skippedCount);
// Yield to browser before heavy computation
await sleep(1);
const isPrime = millerRabinFast(n);
// Replace the "testing" entry with the result
testEntry.remove();
if (isPrime) {
primesCount++;
primePositions.add(pos);
piPrimesFound.push({ pos, numStr });
addPiLogEntry(pos, numStr, 'prime', true);
renderPiDigits(pos, primePositions);
// Longer pause to celebrate finding a prime
const delay = getSpeedDelay();
await sleep(Math.max(delay * 3, 100));
} else {
compositesCount++;
addPiLogEntry(pos, numStr, 'composite', false);
const delay = getSpeedDelay();
if (delay > 1) await sleep(delay);
}
updatePiStats(pos, primesCount, compositesCount, skippedCount);
}
piHuntRunning = false;
document.getElementById('piStartBtn').style.display = '';
document.getElementById('piStartBtn').textContent = 'Restart Hunt';
document.getElementById('piStopBtn').style.display = 'none';
}
function stopPiHunt() {
piHuntStopped = true;
piHuntRunning = false;
document.getElementById('piStartBtn').style.display = '';
document.getElementById('piStartBtn').textContent = 'Restart Hunt';
document.getElementById('piStopBtn').style.display = 'none';
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment