Every section of Claude-authored code from the puzzle page (vendored Tau Prolog is deliberately excluded — see the linked verification instead), each with a note on which technique it uses and, where one applies, a textbook that teaches the same thing.
The 655 original lines were isolated from the shipped page by diffing its two vendored blocks (Tau Prolog's core.js and lists.js, both v0.3.4, BSD-3-Clause) byte-for-byte against the pristine npm package files, confirming they're unmodified. Everything outside those two byte-identical blocks — CSS, HTML, and all inline JS — is reproduced verbatim below, split by function/constant, in source order.
<style>
:root{
--paper:#eef1ea;
--ink:#1c2a27;
--ink-soft:#4c5a56;
--rule:#c7cfc4;
--accent:#1f8a70;
--accent-soft:#dcece5;
--card:#ffffff;
--serif: "Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
--sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box;}
body{
margin:0; background:var(--paper); color:var(--ink);
font-family:var(--sans); line-height:1.5;
}
.wrap{max-width:880px; margin:0 auto; padding:2.5rem 1.5rem 5rem;}
h1{
font-family:var(--serif); font-weight:600; font-size:2.4rem;
line-height:1.15; margin:0 0 .3rem;
}
.kicker{color:var(--ink-soft); font-size:1.05rem; margin:0 0 2rem; max-width:60ch;}
h2{
font-family:var(--serif); font-size:1.4rem; margin:2.5rem 0 .8rem;
border-bottom:1px solid var(--rule); padding-bottom:.4rem;
}
.setup{background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:1.2rem 1.4rem; margin-bottom:1.4rem;}
.setup dl{display:grid; grid-template-columns:auto 1fr; gap:.3rem 1rem; margin:0; font-size:.95rem;}
.setup dt{color:var(--ink-soft); white-space:nowrap;}
.setup dd{margin:0;}
ol.clues{padding-left:1.4rem; margin:0;}
ol.clues li{margin-bottom:.55rem; max-width:68ch;}
.question{font-style:italic; margin-top:1.2rem; max-width:68ch;}
table.grid{width:100%; border-collapse:collapse; background:var(--card); border:1px solid var(--rule);}
table.grid caption{text-align:left; font-size:.85rem; color:var(--ink-soft); margin-bottom:.5rem;}
table.grid th, table.grid td{border:1px solid var(--rule); padding:.5rem .6rem; text-align:left; font-size:.92rem;}
table.grid th{background:var(--accent-soft); font-weight:600;}
table.grid select{width:100%; padding:.35rem; font-size:.88rem; border:1px solid var(--rule); border-radius:4px; background:var(--card);}
.year-cell{font-family:var(--serif); font-weight:600; font-size:1rem;}
.actions{margin-top:1rem; display:flex; flex-wrap:wrap; gap:.6rem; align-items:center;}
button{
font:inherit; font-size:.92rem; cursor:pointer; border-radius:6px;
padding:.55rem 1rem; border:1px solid var(--ink); background:var(--ink); color:var(--paper);
}
button.secondary{background:var(--card); color:var(--ink); border:1px solid var(--rule);}
button:hover{opacity:.88;}
#gradeResult{font-size:.95rem; min-height:1.4em;}
#gradeResult.ok{color:var(--accent); font-weight:600;}
#gradeResult.no{color:#a1442c;}
.engines{display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(240px,1fr));}
.engine-card{background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:1rem 1.1rem;}
.engine-card h3{margin:0 0 .2rem; font-size:1.05rem;}
.engine-card p.lic{margin:0 0 .8rem; font-size:.8rem; color:var(--ink-soft);}
.engine-card .status{font-size:.85rem; min-height:1.3em; margin-top:.5rem; color:var(--ink-soft);}
.result-table{width:100%; border-collapse:collapse; margin-top:.6rem; font-size:.82rem;}
.result-table th,.result-table td{border:1px solid var(--rule); padding:.3rem .4rem; text-align:left;}
.result-table th{background:var(--accent-soft);}
details.provenance{background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:.9rem 1.1rem; margin-top:2.5rem; font-size:.88rem;}
details.provenance summary{cursor:pointer; font-weight:600;}
details.provenance ul{margin:.6rem 0 0; padding-left:1.3rem;}
details.provenance li{margin-bottom:.5rem;}
footer{margin-top:3rem; font-size:.8rem; color:var(--ink-soft);}
.debugger{background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:1.1rem 1.2rem;}
.step-counter{font-size:.9rem; color:var(--ink-soft); margin-left:.3rem;}
.debugger-grid{display:grid; grid-template-columns:1.3fr 1fr; gap:1rem; margin-top:1rem;}
@media (max-width:700px){ .debugger-grid{grid-template-columns:1fr;} }
.pane h4{margin:0 0 .35rem; font-size:.8rem; text-transform:none; color:var(--ink-soft); font-weight:600;}
.pane pre{
background:#12201c; color:#d7e7df; border-radius:6px; padding:.7rem .8rem;
font-size:.82rem; line-height:1.5; overflow-x:auto; margin:0 0 .9rem;
white-space:pre-wrap; word-break:break-word;
}
.pane pre .current-line{background:rgba(31,138,112,.35); display:block; margin:0 -.8rem; padding:0 .8rem;}
.human-pane{background:var(--accent-soft); border-radius:6px; padding:.9rem 1rem;}
.human-pane p{margin:0 0 .6rem;}
#clueHeading{color:var(--ink); font-family:var(--serif); font-size:1.1rem; margin-bottom:.5rem;}
</style>
Plain CSS. Visual design choices, not a programming technique — nothing to cite.
No textbook citation applies here.
<body>
<div class="wrap">
<h1>The iGEM Chronology Puzzle</h1>
<p class="kicker"><strong>Version 0.2</strong> — Tau Prolog only, but with a step-through debugger: watch the actual resolution trace, one clue at a time, alongside a plain-English account of what each step establishes. (Version 0.1, which runs all three engines side by side without a trace view, is preserved separately.)</p>
<h2>Setup</h2>
<div class="setup">
<p>Six iGEM teams competed in six different years, 2020–2025, each in a different country, each working with a different signature organism, each with a different project name, and each earning a different special recognition. Using only the clues below, figure out which team goes with which year.</p>
<dl>
<dt>Countries</dt><dd>Netherlands · China · USA · Hong Kong · Canada · Germany</dd>
<dt>Organisms</dt><dd>Nematodes · Cyanobacteria · Yeast · Black Soldier Fly · Spider silk (engineered bacteria) · Peptide-sensor bacteria</dd>
<dt>Projects</dt><dd>RootPatch · (unnamed algae project) · Helo · Ecoplaster · ReneWool · SPROUT</dd>
<dt>Recognition</dt><dd>Best Food & Nutrition Project nom. · Silver medal only · Best Therapeutics Project nom. · Best Sustainable Development Project nom. · Gold medal, no special nomination · Safety & Security Prize winner</dd>
</dl>
</div>
<h2>Clues</h2>
<ol class="clues" id="cluesList"><!-- filled by script --></ol>
<p class="question">Which country, organism, project, and award go with each year, 2020–2025?</p>
<h2>Your answer</h2>
<div style="overflow-x:auto;">
<table class="grid" id="answerGrid">
<caption>Pick one option per column for each year, then check your work.</caption>
<thead>
<tr><th>Year</th><th>Country</th><th>Organism</th><th>Project</th><th>Recognition</th></tr>
</thead>
<tbody><!-- filled by script --></tbody>
</table>
</div>
<div class="actions">
<button id="checkBtn">Check my answers</button>
<button id="clearBtn" class="secondary">Clear grid</button>
<span id="gradeResult" role="status"></span>
</div>
<h2>Watch Tau Prolog solve it</h2>
<p>This runs the real Prolog resolution once, records every step Tau Prolog's own (undocumented but built-in) debugger hook exposes, then lets you step through the 21 checkpoints that matter — one per clue — forwards or backwards.</p>
<div class="debugger" id="debuggerPanel">
<div class="actions">
<button id="runTraceBtn">Run & trace</button>
<button id="prevStepBtn" class="secondary" disabled>← Prev</button>
<button id="nextStepBtn" class="secondary" disabled>Next →</button>
<button id="playBtn" class="secondary" disabled>▶ Play</button>
<span id="stepCounter" class="step-counter"></span>
</div>
<div class="debugger-grid">
<div class="pane raw-pane">
<h4>Source — currently executing</h4>
<pre id="sourceView">Click "Run & trace" to solve the puzzle and load a trace.</pre>
<h4>Grounded call at this step</h4>
<pre id="callView"></pre>
</div>
<div class="pane human-pane">
<h4 id="clueHeading">Reasoning</h4>
<p id="clueText"></p>
<p id="clueReasoning"></p>
</div>
</div>
<h4>What's known so far</h4>
<div style="overflow-x:auto;">
<table class="grid" id="traceGrid">
<thead><tr><th>Year</th><th>Country</th><th>Organism</th><th>Project</th><th>Recognition</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<details class="provenance">
<summary>What's original code on this page, and where the rest comes from</summary>
<ul>
<li><strong>Tau Prolog</strong> is vendored unmodified from its published package. The puzzle is an ordinary Prolog program (permutation + member/nth1 — the standard textbook technique for zebra-style puzzles). For this version, each clue was pulled out into its own tiny named predicate (<code>clue1/1</code>, <code>clue2/3</code>, ...) purely so the trace has clean, readable checkpoints to filter for — it changes nothing about how the puzzle is solved.</li>
<li><strong>The debugger hook itself is not a new library</strong> — it's Tau Prolog's own internal machinery. <code>Thread.prototype.step</code> already checks <code>this.debugger</code> and records every resolution step into <code>this.debugger_states</code> if it's turned on; we just flip that flag and read the array back afterward. <code>Term.prototype.select()</code> and <code>Term.prototype.apply()</code>, also Tau Prolog's own, are what pull "the subgoal currently being tried" and "its arguments under the current bindings" out of each recorded step.</li>
<li><strong>The clue text, reasoning blurbs, and the "what's known so far" reveal order</strong> are hand-written for this puzzle (mapping each clue to the grid cell(s) it pins down) — content, not a technique, so nothing to cite there.</li>
<li><strong>The stepper itself</strong> (prev/next/play, building the raw and human panes, the DOM comparisons that grade your grid) is ordinary event handling and DOM manipulation — the kind covered in the "Modern JavaScript Tutorial" (javascript.info) chapters on the Document, Events, and Forms.</li>
</ul>
</details>
<footer>
Version 0.2 of this page. Version 0.1 (Tau Prolog, ramo, and microKanren all solving independently, no trace view) is preserved separately as <code>igem-puzzle-v0.1.html</code>.
Clue 21 ("Ecoplaster was the Black Soldier Fly team's project") was added after the original 20-clue set turned out to have two valid solutions differing only in which of the two unnamed projects went where.
</footer>
</div>
<script>
Page structure and the puzzle's own content (clue setup text, panel labels). Ordinary HTML — nothing to cite.
No textbook citation applies here.
const YEARS = [2020,2021,2022,2023,2024,2025];
const COUNTRIES = ['netherlands','china','usa','hongkong','canada','germany'];
const ORGANISMS = ['nematodes','cyanobacteria','yeast','black_soldier_fly','spider_silk','peptide_sensor'];
const PROJECTS = ['rootpatch','algae_project','helo','ecoplaster','renewool','sprout'];
const AWARDS = ['best_food','silver_only','best_therapeutics','best_sustainable','gold_no_nom','safety_security'];
const LABEL = {
netherlands:'Netherlands', china:'China', usa:'USA', hongkong:'Hong Kong', canada:'Canada', germany:'Germany',
nematodes:'Nematodes', cyanobacteria:'Cyanobacteria', yeast:'Yeast', black_soldier_fly:'Black Soldier Fly',
spider_silk:'Spider silk (engineered bacteria)', peptide_sensor:'Peptide-sensor bacteria',
rootpatch:'RootPatch', algae_project:'(unnamed algae project)', helo:'Helo', ecoplaster:'Ecoplaster',
renewool:'ReneWool', sprout:'SPROUT',
best_food:'Best Food & Nutrition Project nom.', silver_only:'Silver medal only',
best_therapeutics:'Best Therapeutics Project nom.', best_sustainable:'Best Sustainable Development Project nom.',
gold_no_nom:'Gold medal, no special nomination', safety_security:'Safety & Security Prize winner'
};
const CLUES = [
"The peptide-sensor bacteria team competed in the most recent year of the six.",
"The cyanobacteria team competed exactly one year after the nematode team.",
"The yeast team competed exactly one year after the cyanobacteria team.",
"The Black Soldier Fly team competed sometime after the yeast team (not necessarily right after).",
"The spider-silk team competed exactly one year after the Black Soldier Fly team.",
"The nematode team was Dutch.",
"The Chinese team worked with cyanobacteria.",
"The Canadian team made spider silk.",
"The Hong Kong team's year was exactly three years after the Dutch team's year.",
"The team that won only a Silver medal (no gold) worked with cyanobacteria.",
"Whoever was nominated for Best Therapeutics Project used yeast.",
"The American team was nominated for Best Therapeutics Project.",
"The team that won a Gold medal with no special nomination made spider silk.",
"Whoever was nominated for Best Food & Nutrition Project worked with nematodes.",
"Whoever was nominated for Best Sustainable Development Project worked with the Black Soldier Fly.",
"The German team won the Safety and Security Prize.",
"\u201cRootPatch\u201d was the nematode team's project.",
"\u201cHelo\u201d was the yeast team's project.",
"\u201cReneWool\u201d was the spider-silk team's project.",
"\u201cSPROUT\u201d was the peptide-sensor bacteria team's project.",
"Ecoplaster was the Black Soldier Fly team's project."
];
Plain data — the puzzle's own vocabulary and clue text. Content, not a technique — nothing to cite.
No textbook citation applies here.
function renderClues() {
const ol = document.getElementById('cluesList');
CLUES.forEach(text => {
const li = document.createElement('li');
li.textContent = text;
ol.appendChild(li);
});
}
Creating elements and appending them in a loop is the basic DOM-building pattern.
📖 The Modern JavaScript Tutorial (javascript.info) — "Document" chapter, specifically "Modifying the document" (createElement / append).
function buildSelect(values, name) {
const sel = document.createElement('select');
sel.dataset.field = name;
const blank = document.createElement('option');
blank.value = ''; blank.textContent = '—';
sel.appendChild(blank);
values.forEach(v => {
const opt = document.createElement('option');
opt.value = v; opt.textContent = LABEL[v];
sel.appendChild(opt);
});
return sel;
}
Same DOM-construction pattern, applied to form controls.
📖 The Modern JavaScript Tutorial — "Forms, controls" chapter (building/populating <select> elements).
function renderGrid() {
const tbody = document.querySelector('#answerGrid tbody');
YEARS.forEach((year, i) => {
const tr = document.createElement('tr');
tr.dataset.row = i;
const yearTd = document.createElement('td');
yearTd.className = 'year-cell';
yearTd.textContent = year;
tr.appendChild(yearTd);
[[COUNTRIES,'country'], [ORGANISMS,'organism'], [PROJECTS,'project'], [AWARDS,'award']].forEach(([values, field]) => {
const td = document.createElement('td');
td.appendChild(buildSelect(values, field));
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
Nested loops building table rows/cells from arrays of data.
📖 The Modern JavaScript Tutorial — "Document" chapter (DOM construction); the nested-array iteration itself is plain Array#forEach, covered in any JS reference, including MDN's Array docs.
function readGrid() {
const rows = [...document.querySelectorAll('#answerGrid tbody tr')];
return rows.map(row => {
const get = (field) => row.querySelector('select[data-field="' + field + '"]').value;
return { country: get('country'), organism: get('organism'), project: get('project'), award: get('award') };
});
}
/* the one true answer, established earlier by all three engines agreeing */
Spreading a NodeList into a real array (`[...document.querySelectorAll(...)]`) so Array methods like .map work on it.
📖 You Don't Know JS Yet — "ES6 & Beyond" chapter, on the spread operator.
const ANSWER = [
{ country:'netherlands', organism:'nematodes', project:'rootpatch', award:'best_food' },
{ country:'china', organism:'cyanobacteria', project:'algae_project', award:'silver_only' },
{ country:'usa', organism:'yeast', project:'helo', award:'best_therapeutics' },
{ country:'hongkong', organism:'black_soldier_fly', project:'ecoplaster', award:'best_sustainable' },
{ country:'canada', organism:'spider_silk', project:'renewool', award:'gold_no_nom' },
{ country:'germany', organism:'peptide_sensor', project:'sprout', award:'safety_security' }
];
function gradeGrid() {
const yours = readGrid();
const el = document.getElementById('gradeResult');
let correctCells = 0, totalCells = 0, allFilled = true;
yours.forEach((row, i) => {
['country','organism','project','award'].forEach(field => {
totalCells++;
if (!row[field]) allFilled = false;
if (row[field] && row[field] === ANSWER[i][field]) correctCells++;
});
});
el.className = '';
if (correctCells === totalCells) {
el.textContent = 'All 24 cells correct \u2014 that\u2019s the puzzle solved.';
el.classList.add('ok');
} else {
el.textContent = correctCells + ' / ' + totalCells + ' cells correct so far' + (allFilled ? '.' : ' (grid not full yet).');
el.classList.add('no');
}
}
Straightforward array iteration and comparison — no particular technique to cite beyond ordinary Array#forEach usage.
No textbook citation applies here.
const PROLOG_PROGRAM = `
:- use_module(library(lists)).
clue1(Organisms) :- nth1(6, Organisms, peptide_sensor).
clue2(Organisms, Nn, Nc) :- nth1(Nn, Organisms, nematodes), Nc is Nn+1, nth1(Nc, Organisms, cyanobacteria).
clue3(Organisms, Nc, Ny) :- Ny is Nc+1, nth1(Ny, Organisms, yeast).
clue4(Organisms, Ny, Nb) :- nth1(Nb, Organisms, black_soldier_fly), Nb > Ny.
clue5(Organisms, Nb, Ns) :- Ns is Nb+1, nth1(Ns, Organisms, spider_silk).
clue6(Countries, Nn) :- nth1(Nn, Countries, netherlands).
clue7(Countries, Nc) :- nth1(Nc, Countries, china).
clue8(Countries, Ns) :- nth1(Ns, Countries, canada).
clue9(Countries, Nn, Nu, Ng) :- Nh is Nn+3, nth1(Nh, Countries, hongkong), nth1(Nu, Countries, usa), nth1(Ng, Countries, germany).
clue10(Awards, Nc) :- nth1(Nc, Awards, silver_only).
clue11(Awards, Ny) :- nth1(Ny, Awards, best_therapeutics).
clue12(Awards, Nu) :- nth1(Nu, Awards, best_therapeutics).
clue13(Awards, Ns) :- nth1(Ns, Awards, gold_no_nom).
clue14(Awards, Nn) :- nth1(Nn, Awards, best_food).
clue15(Awards, Nb) :- nth1(Nb, Awards, best_sustainable).
clue16(Awards, Ng) :- nth1(Ng, Awards, safety_security).
clue17(Projects, Nn) :- nth1(Nn, Projects, rootpatch).
clue18(Projects, Ny) :- nth1(Ny, Projects, helo).
clue19(Projects, Ns) :- nth1(Ns, Projects, renewool).
clue20(Projects) :- nth1(6, Projects, sprout).
clue21(Projects, Nb) :- nth1(Nb, Projects, ecoplaster).
solve(Countries, Organisms, Projects, Awards) :-
permutation([nematodes,cyanobacteria,yeast,black_soldier_fly,spider_silk,peptide_sensor], Organisms),
clue1(Organisms),
clue2(Organisms, Nn, Nc),
clue3(Organisms, Nc, Ny),
clue4(Organisms, Ny, Nb),
clue5(Organisms, Nb, Ns),
permutation([netherlands,china,usa,hongkong,canada,germany], Countries),
clue6(Countries, Nn),
clue7(Countries, Nc),
clue8(Countries, Ns),
clue9(Countries, Nn, Nu, Ng),
permutation([rootpatch,algae_project,helo,ecoplaster,renewool,sprout], Projects),
clue17(Projects, Nn),
clue18(Projects, Ny),
clue19(Projects, Ns),
clue20(Projects),
clue21(Projects, Nb),
permutation([best_food,silver_only,best_therapeutics,best_sustainable,gold_no_nom,safety_security], Awards),
clue10(Awards, Nc),
clue11(Awards, Ny),
clue12(Awards, Nu),
clue13(Awards, Ns),
clue14(Awards, Nn),
clue15(Awards, Nb),
clue16(Awards, Ng).
`;
// Pre-split into lines so we can highlight "the current line" in the raw
// source view -- same idea as any source viewer: find the line whose text
// starts with the predicate name.
This is Prolog source, not JavaScript. The permutation + nth1 "generate, then filter" technique is the standard way zebra-style puzzles are solved in Prolog — it's not from any of the three JS textbooks, since it's a Prolog-specific idiom.
📖 Not a JS textbook match — see instead any introductory Prolog resource, e.g. "Learn Prolog Now!" (the classic zebra-puzzle chapter) or SWI-Prolog's own documentation examples.
const PROGRAM_LINES = PROLOG_PROGRAM.split('\n');
function sourceLineFor(clueId) {
const idx = PROGRAM_LINES.findIndex(l => l.trim().startsWith(clueId + '('));
return { idx, text: idx >= 0 ? PROGRAM_LINES[idx].trim() : '' };
}
// Which grid cell(s) each clue pins down, using the FINAL answer (already
// established and verified unique) to fill in the value -- this is just
// bookkeeping for the animation, not part of solving the puzzle.
Splitting a string into lines and searching an array with .findIndex — plain array/string methods.
📖 You Don't Know JS Yet — "Types & Grammar" covers String/Array method usage in this style; no deeper technique involved.
const CLUE_REVEALS = {
1: [[5,'organism']],
2: [[0,'organism'],[1,'organism']],
3: [[2,'organism']],
4: [],
5: [[3,'organism'],[4,'organism']],
6: [[0,'country']],
7: [[1,'country']],
8: [[4,'country']],
9: [[3,'country']],
17:[[0,'project']],
18:[[2,'project']],
19:[[4,'project']],
20:[[5,'project']],
21:[[3,'project'],[1,'project']],
10:[[1,'award']],
11:[[2,'award']],
12:[[2,'country']],
13:[[4,'award']],
14:[[0,'award']],
15:[[3,'award']],
16:[[5,'country'],[5,'award']]
};
const CLUE_REASONING = {
1: "Pins the last slot directly: peptide-sensor bacteria is 2025.",
2: "Places nematodes and cyanobacteria as a consecutive pair, wherever that pair ends up.",
3: "Extends the chain: yeast immediately follows cyanobacteria.",
4: "Rules out nematodes, cyanobacteria and yeast as candidates for Black Soldier Fly \u2014 it must fall later in the sequence. Combined with clue 5, and only two slots left before 2025, this is about to pin an exact position.",
5: "Fixes Black Soldier Fly and spider silk into the two remaining slots before peptide-sensor bacteria, completing the organism order: nematodes \u2192 cyanobacteria \u2192 yeast \u2192 Black Soldier Fly \u2192 spider silk \u2192 peptide-sensor.",
6: "Ties the Netherlands directly to the nematode year, already known to be 2020.",
7: "Ties China to the cyanobacteria year, 2021.",
8: "Ties Canada to the spider-silk year, 2024.",
9: "Places Hong Kong exactly three years after the Netherlands (2020) \u2014 2023, already known to be the Black Soldier Fly year.",
17: "Ties RootPatch to the nematode year, 2020.",
18: "Ties Helo to the yeast year, 2022.",
19: "Ties ReneWool to the spider-silk year, 2024.",
20: "Ties SPROUT to the peptide-sensor year, 2025.",
21: "Ties Ecoplaster to the Black Soldier Fly year, 2023 \u2014 which leaves the unnamed algae project for 2021, by elimination.",
10: "Ties the silver-only medal to the cyanobacteria year, 2021.",
11: "Ties the Best Therapeutics nomination to the yeast year, 2022.",
12: "Ties the USA to Best Therapeutics \u2014 already known to be the yeast year, 2022 \u2014 so the USA is 2022.",
13: "Ties the no-nomination Gold medal to the spider-silk year, 2024.",
14: "Ties the Best Food & Nutrition nomination to the nematode year, 2020.",
15: "Ties the Best Sustainable Development nomination to the Black Soldier Fly year, 2023.",
16: "Ties Germany to the Safety & Security Prize \u2014 the only year and award left \u2014 completing the grid: 2025."
};
Hand-written mapping of "which clue reveals which grid cell" plus the plain-English reasoning for each clue. Content, not a technique — nothing to cite.
No textbook citation applies here.
function plListToArray(term) {
const out = [];
let t = term;
while (t && t.indicator === './2') {
out.push(t.args[0].id);
t = t.args[1];
}
return out;
}
// Solve once with Tau Prolog's built-in debugger flag turned on, then
// filter its recorded resolution steps down to the ones whose current
// subgoal is one of our named clue predicates. `Term.prototype.select()`
// (peeling the leftmost subgoal off the remaining conjunction) and
// `Term.prototype.apply()` (grounding a term under a substitution) are
// both Tau Prolog's own methods -- nothing new is added to the engine.
A small recursive/iterative walk over a linked, cons-style structure (Prolog's "." list representation) — the same shape as walking any singly-linked list.
📖 SICP JS — the sections on processing lists recursively (Chapter 2, "Building Abstractions with Data"); the general technique of walking a cons-style structure one link at a time is exactly what's taught there.
function solveWithTauTrace(onDone) {
const session = pl.create(1000000);
const t0 = performance.now();
session.consult(PROLOG_PROGRAM, {
success: function () {
session.thread.debugger = true;
session.query('solve(Countries, Organisms, Projects, Awards).');
session.answer(answer => {
const ms = (performance.now() - t0).toFixed(1);
if (pl.type.is_error(answer) || !answer) {
onDone({ ok:false, ms, error: session.format_answer(answer) });
return;
}
const countries = plListToArray(answer.links['Countries']);
const organisms = plListToArray(answer.links['Organisms']);
const projects = plListToArray(answer.links['Projects']);
const awards = plListToArray(answer.links['Awards']);
const houses = countries.map((c,i) => ({ country:c, organism:organisms[i], project:projects[i], award:awards[i] }));
const rawStates = session.thread.debugger_states;
const steps = [];
rawStates.forEach(point => {
if (!point.goal) return;
const atom = pl.type.is_term(point.goal) ? point.goal.select() : point.goal;
const m = pl.type.is_term(atom) && /^clue(\d+)$/.exec(atom.id);
if (!m) return;
const clueNum = parseInt(m[1], 10);
const grounded = atom.apply(point.substitution);
steps.push({ clueNum, callText: grounded.toString() });
});
onDone({ ok:true, ms, houses, steps, totalRawSteps: rawStates.length });
});
},
error: function (err) {
onDone({ ok:false, ms: (performance.now()-t0).toFixed(1), error: pl.format_answer(err) });
}
});
}
The consult/query/answer calls follow Tau Prolog's own documented usage pattern. The only genuinely new part is filtering thread.debugger_states for clue-numbered goals and pairing each with its grounded call — ordinary array filtering/mapping over data Tau Prolog already hands back.
📖 No textbook needed for the filtering itself (plain Array#forEach/Array#push); the API usage itself mirrors Tau Prolog's own README, not a textbook.
let TRACE = null; // { steps, houses }
let STEP_INDEX = -1; // -1 = nothing shown yet
let playTimer = null;
function cellsKnownThrough(stepIndex) {
// recomputed from scratch each time -- simplest way to make stepping
// backwards "un-reveal" cells correctly, with no incremental state to
// get out of sync.
const known = YEARS.map(() => ({}));
for (let s = 0; s <= stepIndex; s++) {
const clueNum = TRACE.steps[s].clueNum;
(CLUE_REVEALS[clueNum] || []).forEach(([row, field]) => {
known[row][field] = TRACE.houses[row][field];
});
}
return known;
}
Rather than incrementally mutating a "known so far" object as the visitor steps forward/back, this recomputes it fresh from step 0 every time. That sidesteps an entire class of bugs where stepping backward would need to carefully undo mutations.
📖 This "derive state fresh rather than mutate it" habit is a general functional-programming idiom rather than one specific textbook passage — SICP (JS or otherwise) touches on the hazards of mutable state, but there isn't a single matching section to point to.
function renderTraceGrid(known) {
const tbody = document.querySelector('#traceGrid tbody');
tbody.innerHTML = '';
YEARS.forEach((year, i) => {
const tr = document.createElement('tr');
const yearTd = document.createElement('td');
yearTd.className = 'year-cell';
yearTd.textContent = year;
tr.appendChild(yearTd);
['country','organism','project','award'].forEach(field => {
const td = document.createElement('td');
const val = known[i][field];
td.textContent = val ? LABEL[val] : '\u2014';
if (!val) td.style.color = 'var(--ink-soft)';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
function renderStep() {
const i = STEP_INDEX;
const total = TRACE.steps.length;
document.getElementById('stepCounter').textContent = i < 0 ? '' : ('Step ' + (i+1) + ' of ' + total);
document.getElementById('prevStepBtn').disabled = i <= 0;
document.getElementById('nextStepBtn').disabled = i >= total - 1;
if (i < 0) return;
const step = TRACE.steps[i];
const { idx: lineIdx, text: lineText } = sourceLineFor('clue' + step.clueNum);
const sourceEl = document.getElementById('sourceView');
sourceEl.innerHTML = '';
PROGRAM_LINES.forEach((line, li) => {
const span = document.createElement('span');
span.className = li === lineIdx ? 'current-line' : '';
span.textContent = line + '\n';
sourceEl.appendChild(span);
});
if (lineIdx >= 0) {
// scroll the highlighted line into view within the <pre>
const target = sourceEl.children[lineIdx];
if (target && target.scrollIntoView) target.scrollIntoView({ block: 'center' });
}
document.getElementById('callView').textContent = step.callText;
document.getElementById('clueHeading').textContent = 'Clue ' + step.clueNum;
document.getElementById('clueText').textContent = CLUES[step.clueNum - 1];
document.getElementById('clueReasoning').textContent = CLUE_REASONING[step.clueNum] || '';
renderTraceGrid(cellsKnownThrough(i));
}
More DOM construction/update — same pattern as renderClues/renderGrid above.
📖 The Modern JavaScript Tutorial — "Document" chapter, again (createElement, textContent, classList).
function wireButtons() {
document.getElementById('checkBtn').addEventListener('click', gradeGrid);
document.getElementById('clearBtn').addEventListener('click', () => {
document.querySelectorAll('#answerGrid select').forEach(s => s.value = '');
const el = document.getElementById('gradeResult');
el.textContent = ''; el.className = '';
});
document.getElementById('runTraceBtn').addEventListener('click', (e) => {
const btn = e.currentTarget;
btn.disabled = true;
btn.textContent = 'Solving\u2026';
solveWithTauTrace(res => {
btn.disabled = false;
btn.textContent = 'Run & trace';
if (!res.ok) {
document.getElementById('sourceView').textContent = 'No solution found.';
return;
}
TRACE = { steps: res.steps, houses: res.houses };
STEP_INDEX = 0;
document.getElementById('prevStepBtn').disabled = false;
document.getElementById('nextStepBtn').disabled = false;
document.getElementById('playBtn').disabled = false;
renderStep();
});
});
document.getElementById('prevStepBtn').addEventListener('click', () => {
if (!TRACE || STEP_INDEX <= 0) return;
STEP_INDEX--; renderStep();
});
document.getElementById('nextStepBtn').addEventListener('click', () => {
if (!TRACE || STEP_INDEX >= TRACE.steps.length - 1) return;
STEP_INDEX++; renderStep();
});
document.getElementById('playBtn').addEventListener('click', (e) => {
const btn = e.currentTarget;
if (playTimer) {
clearInterval(playTimer); playTimer = null;
btn.textContent = '\u25B6 Play';
return;
}
btn.textContent = '\u23F8 Pause';
playTimer = setInterval(() => {
if (!TRACE || STEP_INDEX >= TRACE.steps.length - 1) {
clearInterval(playTimer); playTimer = null; btn.textContent = '\u25B6 Play';
return;
}
STEP_INDEX++; renderStep();
}, 900);
});
}
renderClues();
renderGrid();
wireButtons();
</script>
</body>
</html>
addEventListener-based event handling, plus a start/stop toggle built on setInterval/clearInterval for the "Play" button.
📖 The Modern JavaScript Tutorial — "Introduction to browser events" chapter for the event handling; "Scheduling: setTimeout and setInterval" for the play/pause timer.