Bug Report Reporting for: General
Report sent. Thank you.
Report an Issue Current passage
Report sent. Thank you.
Ed History Games — Carolina Uncovered ✦ ✦ ✦

Walker's Appeal

To the Coloured Citizens of the World


A note before you begin This document was written in 1829 by David Walker, a free Black man from Wilmington, North Carolina. It contains language that was standard in its time but that we recognize differently today, including terms for race and for people of other faiths. Those words are part of the historical record. The glossary in this game will help you understand how they were used in 1829 and how we understand them now. Walker's argument is urgent, radical, and historically significant. Read it as he wrote it.

David Walker published his Appeal to the Coloured Citizens of the World in three editions between 1829 and 1830. It was the most incendiary abolitionist document of its century. Copies were sewn into the clothing of sailors and smuggled into Southern ports. Several states passed laws making it illegal to possess. Walker was found dead in his shop in Boston in 1830, less than a year after the third edition was published. The cause was never determined.

This game covers the Preamble -- the opening section where Walker lays out his argument, his evidence, and his challenge to both Black Americans and white Americans. Read carefully. Walker chose every word.

Choose your level to begin
Have a class code? Enter it first.
No code -- play without one

Levels 2 and 3 are free. Level 1 requires a school subscription.

function copyToken() { const val = document.getElementById('token-value').textContent; navigator.clipboard.writeText(val).catch(() => { const ta = document.createElement('textarea'); ta.value = val; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }); const conf = document.getElementById('token-copy-confirm'); conf.textContent = 'Copied to clipboard.'; conf.style.display = 'block'; } function emailToken() { const val = document.getElementById('token-value').textContent; const nick = studentNickname || 'Student'; const subject = encodeURIComponent("Walker's Appeal Completion Token -- " + nick); const body = encodeURIComponent("Here is my completion token for Walker's Appeal: The Preamble.\n\nNickname: " + nick + "\nToken: " + val + "\n\nPlease submit this to your teacher."); window.open('mailto:?subject=' + subject + '&body=' + body); } function simpleHash(str) { // djb2 hash, returns 4-char hex string let hash = 5381; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash) + str.charCodeAt(i); hash = hash & 0xFFFFFF; // keep 24 bits } return hash.toString(16).toUpperCase().padStart(6, '0'); } function buildToken(code, totalScore, maxScore, level) { const mins = Math.floor(Date.now() / 60000) % 10000; const codeStr = code || 'OPEN'; const codeKey = codeStr.startsWith('WA-') ? codeStr.slice(3) : codeStr; const nick = (studentNickname || 'Student').replace(/[^A-Za-z0-9_-]/g,'').slice(0,12) || 'Student'; const body = codeKey + '-S' + totalScore + '-M' + maxScore + '-L' + level + '-T' + mins + '-N' + nick; const check = simpleHash(body + TOKEN_SECRET); return 'WA-' + body + '-V' + check; } function parseToken(token) { // Returns { valid, code, score, maxScore, level, error } token = token.trim().toUpperCase(); const re = /^WA-(.+)-S(\d+)-M(\d+)-L(\d)-T(\d+)-V([0-9A-F]{6})$/; const m = token.match(re); if (!m) return { valid: false, error: "Token format not recognized. Check for typos." }; const [, code, scoreStr, maxStr, levelStr, , check] = m; const body = code + '-S' + scoreStr + '-M' + maxStr + '-L' + levelStr + '-T' + m[5]; const expected = simpleHash(body + TOKEN_SECRET); if (check !== expected) return { valid: false, error: "Token signature does not match. This token may have been altered." }; return { valid: true, code: code === 'OPEN' ? null : code, score: parseInt(scoreStr), maxScore: parseInt(maxStr), level: parseInt(levelStr) }; } function scoreToGrade(s, max) { const pct = s / max; if (s >= 28) return 'A'; if (s >= 22) return 'B'; if (s >= 16) return 'C'; return 'D'; } // ============================================================ // CODE GENERATION AND VALIDATION // ============================================================ function generateCodeStr() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no I, O, 0, 1 to avoid confusion let r = ''; for (let i = 0; i < 4; i++) r += chars[Math.floor(Math.random() * chars.length)]; return 'WA-' + r; } function generateCodes() { const n = Math.min(120, Math.max(1, parseInt(document.getElementById('gen-count').value) || 30)); generatedCodes = []; for (let i = 0; i < n; i++) { let code; do { code = generateCodeStr(); } while (usedCodes[code] || generatedCodes.includes(code)); generatedCodes.push(code); } const box = document.getElementById('code-list-box'); // Format as neat columns: 5 per row let lines = []; for (let i = 0; i < generatedCodes.length; i += 5) { lines.push(generatedCodes.slice(i, i + 5).join(' ')); } box.textContent = lines.join('\n'); document.getElementById('code-list-output').style.display = 'block'; document.getElementById('copy-codes-btn').style.display = ''; } function copyCodes() { const text = generatedCodes.join('\n'); navigator.clipboard.writeText(text).then(() => { const btn = document.getElementById('copy-codes-btn'); const orig = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = orig, 2000); }).catch(() => { prompt('Copy these codes:', generatedCodes.join(', ')); }); } // ============================================================ // TEACHER ACCOUNT SYSTEM FUNCTIONS // ============================================================ function getTeacherData() { return { setupUsesRemaining, accounts: teacherAccounts }; } function saveTeacherData() { // Inject updated data back into the page's own script tag // so a "Download Updated File" triggers a re-bake window.__pendingSave = JSON.stringify({ setupUsesRemaining, accounts: teacherAccounts }); } function showTeacherLogin() { showScreen('teacher-screen'); document.getElementById('teacher-login-area').style.display = ''; document.getElementById('teacher-setup-area').style.display = 'none'; document.getElementById('teacher-dashboard').style.display = 'none'; document.getElementById('teacher-pin-input').value = ''; document.getElementById('teacher-pin-msg').textContent = ''; } function teacherPinLogin() { const pin = document.getElementById('teacher-pin-input').value.trim(); const msg = document.getElementById('teacher-pin-msg'); if (!pin) { msg.textContent = 'Please enter your PIN.'; return; } // Check setup code if (pin === SETUP_CODE) { if (setupUsesRemaining <= 0) { msg.textContent = 'Setup code has already been used twice and is no longer active.'; return; } if (teacherAccounts.length >= 2) { msg.textContent = 'Maximum teacher accounts already created.'; return; } // Show setup flow document.getElementById('teacher-login-area').style.display = 'none'; document.getElementById('teacher-setup-area').style.display = ''; document.getElementById('setup-msg').textContent = ''; document.getElementById('new-pin-input').value = ''; document.getElementById('new-pin-confirm').value = ''; return; } // Check existing teacher PINs const idx = teacherAccounts.findIndex(t => t.pin === pin); if (idx === -1) { msg.textContent = 'PIN not recognized.'; return; } activeTeacher = idx; showTeacherDashboard(); } function createTeacherPin() { const pin1 = document.getElementById('new-pin-input').value.trim(); const pin2 = document.getElementById('new-pin-confirm').value.trim(); const msg = document.getElementById('setup-msg'); if (!pin1) { msg.textContent = 'Please enter a PIN.'; return; } if (pin1 !== pin2) { msg.textContent = 'PINs do not match.'; return; } if (pin1 === SETUP_CODE) { msg.textContent = 'You cannot use the setup code as your PIN.'; return; } if (pin1.length < 4) { msg.textContent = 'PIN must be at least 4 characters.'; return; } if (teacherAccounts.some(t => t.pin === pin1)) { msg.textContent = 'That PIN is already in use.'; return; } teacherAccounts.push({ pin: pin1, codes: [], usedCodes: [] }); setupUsesRemaining--; activeTeacher = teacherAccounts.length - 1; saveTeacherData(); document.getElementById('teacher-setup-area').style.display = 'none'; showTeacherDashboard(); } function showTeacherDashboard() { document.getElementById('teacher-login-area').style.display = 'none'; document.getElementById('teacher-setup-area').style.display = 'none'; document.getElementById('teacher-dashboard').style.display = ''; document.getElementById('teacher-acct-label').textContent = 'Teacher Account ' + (activeTeacher + 1); // Show existing codes if any renderCodeList(); } function renderCodeList() { const teacher = teacherAccounts[activeTeacher]; const box = document.getElementById('code-list-box'); const output = document.getElementById('code-list-output'); if (teacher.codes.length === 0) { output.style.display = 'none'; return; } output.style.display = 'block'; // Show with used/unused status let lines = []; const codes = teacher.codes; for (let i = 0; i < codes.length; i += 5) { lines.push(codes.slice(i, i+5).map(c => { const used = teacher.usedCodes && teacher.usedCodes.includes(c); return used ? '[' + c + ']' : c; }).join(' ')); } box.textContent = lines.join('\n'); document.getElementById('copy-codes-btn').style.display = ''; } function generateCodes() { const teacher = teacherAccounts[activeTeacher]; const n = Math.min(120, Math.max(1, parseInt(document.getElementById('gen-count').value) || 30)); const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const allExisting = teacherAccounts.flatMap(t => t.codes); teacher.codes = []; while (teacher.codes.length < n) { let code = 'WA-'; for (let i = 0; i < 4; i++) code += chars[Math.floor(Math.random() * chars.length)]; if (!allExisting.includes(code) && !teacher.codes.includes(code)) { teacher.codes.push(code); } } teacher.usedCodes = []; saveTeacherData(); renderCodeList(); document.getElementById('download-btn').style.display = ''; } function copyCodes() { const box = document.getElementById('code-list-box'); navigator.clipboard.writeText(box.textContent).catch(() => { const ta = document.createElement('textarea'); ta.value = box.textContent; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }); document.getElementById('copy-codes-btn').textContent = 'Copied!'; setTimeout(() => { document.getElementById('copy-codes-btn').textContent = 'Copy All'; }, 2000); } function downloadUpdatedFile() { // Re-bake teacher data into the HTML and trigger download let src = document.documentElement.outerHTML; const data = JSON.stringify({ setupUsesRemaining, accounts: teacherAccounts }); src = src.replace(/2/g, setupUsesRemaining); src = src.replace(/[]/g, JSON.stringify(teacherAccounts)); // Also fix the SETUP_CODE uses remaining inline const blob = new Blob([src], {type:'text/html'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'walker-appeal-preamble-v6.html'; a.click(); } // ============================================================ // STUDENT CODE VALIDATION // ============================================================ function validateCode() { const raw = document.getElementById('code-input').value.trim().toUpperCase(); const msg = document.getElementById('code-msg'); if (!raw) { msg.textContent = 'Please enter your code.'; msg.className = 'code-msg err'; return; } if (!/^WA-[A-Z0-9]{4}$/.test(raw)) { msg.textContent = 'That does not look like a valid code. Codes look like WA-4X9K.'; msg.className = 'code-msg err'; return; } // Check across all teacher accounts let found = false; for (const teacher of teacherAccounts) { if (teacher.codes.includes(raw)) { if (teacher.usedCodes && teacher.usedCodes.includes(raw)) { msg.textContent = 'That code has already been used.'; msg.className = 'code-msg err'; return; } found = true; break; } } if (!found) { msg.textContent = 'Code not recognized. Check with your teacher.'; msg.className = 'code-msg err'; return; } sessionCode = raw; msg.textContent = 'Code accepted. Choose your level below.'; msg.className = 'code-msg ok'; unlockLevels(); } function skipCode() { sessionCode = null; unlockLevels(); } function markCodeUsed(code) { for (const teacher of teacherAccounts) { if (teacher.codes.includes(code)) { if (!teacher.usedCodes) teacher.usedCodes = []; if (!teacher.usedCodes.includes(code)) teacher.usedCodes.push(code); break; } } } function startGame(level) { // Capture nickname const nickEl = document.getElementById('nickname-input'); studentNickname = nickEl ? nickEl.value.trim() || 'Student' : 'Student'; // Mark code as used if (sessionCode) markCodeUsed(sessionCode); currentLevel = level; currentPassageIndex = 0; currentQuestionIndex = 0; score = 0; bonusScore = 0; streak = 0; evidencePieces = 0; bonusPieces = 0; awaitingBonus = false; answered = false; consecutiveFullMastery = 0; showScreen('game-screen'); renderPassage(); } function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden')); document.getElementById(id).classList.remove('hidden'); window.scrollTo(0,0); // Reset code entry area when returning to start screen if (id === 'start-screen') { document.getElementById('code-entry-area').style.opacity = '1'; // Only lock levels if no valid code accepted yet in this play session if (!sessionCode) { const row = document.getElementById('level-row'); row.style.opacity = '0.35'; row.style.pointerEvents = 'none'; } } } function renderPassage() { const p = PASSAGES[currentPassageIndex]; awaitingBonus = false; answered = false; currentQuestionIndex = 0; bugContext = "Passage " + p.id; // HUD document.getElementById('hud-passage').textContent = "Passage " + p.id + " of " + PASSAGES.length; document.getElementById('hud-level').textContent = "Level " + currentLevel; document.getElementById('hud-score').textContent = "Score: " + (score + bonusScore); updateStreak(); updateEvidence(); // Passage document.getElementById('passage-header').textContent = p.header; document.getElementById('standard-tag').textContent = p.standard; document.getElementById('passage-text').innerHTML = p.text; // Context box const ctx = document.getElementById('context-box'); if (currentLevel >= 2 && p.context) { ctx.classList.remove('hidden'); document.getElementById('context-text').innerHTML = p.context; } else { ctx.classList.add('hidden'); } // Question renderQuestion(p.questions[0], false); } function renderQuestion(q, isBonus) { awaitingBonus = isBonus; answered = false; const p = PASSAGES[currentPassageIndex]; let qlabel = "Question"; if (!isBonus && p.questions.length > 1) { qlabel = "Question " + (currentQuestionIndex + 1) + " of " + p.questions.length; } else if (isBonus) { qlabel = "Bonus Question"; } document.getElementById('q-label').textContent = qlabel; document.getElementById('q-bonus-badge').classList.toggle('hidden', !isBonus); document.getElementById('q-text').textContent = q.text; document.getElementById('feedback-box').classList.add('hidden'); document.getElementById('next-area').classList.add('hidden'); const opts = document.getElementById('options'); opts.innerHTML = ''; q.options.forEach(opt => { const btn = document.createElement('button'); btn.className = 'opt-btn'; btn.innerHTML = `${opt.letter}.${opt.text}`; btn.onclick = () => selectAnswer(opt, q, btn); opts.appendChild(btn); }); } function selectAnswer(opt, q, btn) { if (answered) return; answered = true; // Disable all buttons document.querySelectorAll('.opt-btn').forEach(b => b.disabled = true); const tier = opt.tier; const isBonus = awaitingBonus; // Style button if (tier === 'full') btn.classList.add('correct'); else if (tier === 'wrong' || tier === 'wrong2') btn.classList.add('incorrect'); else btn.classList.add('partial'); // Scoring let pts = 0; if (!isBonus) { if (tier === 'full') { pts = 2; consecutiveFullMastery++; } else if (tier === 'partial') { pts = 1; consecutiveFullMastery = 0; } else { consecutiveFullMastery = 0; } score += pts; streak = tier === 'full' ? streak + 1 : 0; if (tier === 'full') evidencePieces = Math.min(evidencePieces + 1, 10); } else { if (tier === 'full') { bonusScore += 1; bonusPieces = Math.min(bonusPieces + 1, 10); } } // Check badge if (consecutiveFullMastery >= 3 && currentLevel >= 3 && !hasBadge) { hasBadge = true; setTimeout(() => showBadge(), 800); } // Feedback const fb = q.feedback[tier] || q.feedback['wrong'] || ''; const tierLabels = { full: 'Full Mastery', partial: 'Partial Mastery', surface: 'Surface Reading', wrong: 'Incorrect', wrong2: 'Incorrect' }; const tierClasses = { full: 'full', partial: 'partial', surface: 'surface', wrong: 'wrong', wrong2: 'wrong' }; document.getElementById('feedback-tier').textContent = tierLabels[tier] || 'Incorrect'; document.getElementById('feedback-tier').className = 'feedback-tier ' + (tierClasses[tier] || 'wrong'); document.getElementById('feedback-text').textContent = fb; document.getElementById('feedback-box').classList.remove('hidden'); // Score update document.getElementById('hud-score').textContent = "Score: " + (score + bonusScore); updateStreak(); updateEvidence(); // Next button const nextArea = document.getElementById('next-area'); nextArea.classList.remove('hidden'); // If full mastery on last base question and passage has bonus, offer bonus const p = PASSAGES[currentPassageIndex]; const hasMoreQuestions = !isBonus && currentQuestionIndex < p.questions.length - 1; if (hasMoreQuestions) { document.getElementById('next-btn').textContent = "Next Question"; document.getElementById('next-btn').onclick = () => { currentQuestionIndex++; renderQuestion(p.questions[currentQuestionIndex], false); }; } else if (!isBonus && tier === 'full' && p.bonus) { document.getElementById('next-btn').textContent = "Answer Bonus Question (+1)"; document.getElementById('next-btn').onclick = () => { renderQuestion(buildBonusQ(p.bonus), true); }; } else { const isLast = currentPassageIndex >= PASSAGES.length - 1; document.getElementById('next-btn').textContent = isLast ? "See Final Score" : "Next Passage"; document.getElementById('next-btn').onclick = isLast ? endGame : nextPassage; } } function buildBonusQ(bonusData) { return { text: bonusData.text, options: bonusData.options, feedback: bonusData.feedback }; } function nextPassage() { currentPassageIndex++; if (currentPassageIndex >= PASSAGES.length) { endGame(); return; } renderPassage(); } function endGame() { bugContext = "End of Game"; showScreen('end-screen'); const total = score + bonusScore; const max = 32; document.getElementById('end-score-val').textContent = total + " / " + max; let msg = ""; if (total >= 28) msg = "Outstanding work. Walker's argument is not easy to read -- it demands historical knowledge, close attention to language, and willingness to sit with moral complexity. You did all three."; else if (total >= 22) msg = "Strong work. You understood the historical context and the rhetorical structure of Walker's argument. Look back at the passages where you earned partial mastery and push one step further."; else if (total >= 16) msg = "Good effort. You are engaging with a difficult document that has challenged readers for nearly two hundred years. Review the feedback on each passage and try again."; else msg = "Walker's Appeal rewards rereading. The Preamble builds its argument piece by piece across ten passages. Go back to the passages where you struggled and look at what Walker is doing -- not just what he is saying."; document.getElementById('end-message').textContent = msg; // Generate and show completion token if a code was used if (sessionCode) { const token = buildToken(sessionCode, total, max, currentLevel); document.getElementById('token-value').textContent = token; document.getElementById('token-box').classList.remove('hidden'); } else { document.getElementById('token-box').classList.add('hidden'); } } function updateStreak() { const el = document.getElementById('streak-count'); const fire = document.getElementById('streak-fire'); el.textContent = streak; if (streak > 0) { fire.classList.add('pulse'); setTimeout(() => fire.classList.remove('pulse'), 400); } } function updateEvidence() { const bar = document.getElementById('evidence-bar'); bar.innerHTML = ''; for (let i = 0; i < 10; i++) { const d = document.createElement('div'); d.className = 'ev-piece' + (i < evidencePieces ? ' earned' : ''); bar.appendChild(d); } for (let i = 0; i < 10; i++) { const d = document.createElement('div'); d.className = 'ev-piece' + (i < bonusPieces ? ' earned bonus' : ''); bar.appendChild(d); } } // GLOSSARY function openGlossary(word) { const g = GLOSSARY[word]; if (!g) return; document.getElementById('drawer-word').textContent = word; document.getElementById('drawer-content-1829').textContent = g.def1829; document.getElementById('drawer-content-modern').textContent = g.defModern; setDrawerTab('1829'); document.getElementById('glossary-drawer').classList.remove('closed'); document.getElementById('drawer-overlay').classList.remove('closed'); } function closeGlossary() { document.getElementById('glossary-drawer').classList.add('closed'); document.getElementById('drawer-overlay').classList.add('closed'); } function setDrawerTab(tab) { document.getElementById('drawer-content-1829').classList.toggle('hidden', tab !== '1829'); document.getElementById('drawer-content-modern').classList.toggle('hidden', tab !== 'modern'); document.getElementById('tab-1829').classList.toggle('active', tab === '1829'); document.getElementById('tab-modern').classList.toggle('active', tab === 'modern'); } // BUG REPORT function toggleBugPanel() { const panel = document.getElementById('bug-panel'); const toggle = document.getElementById('bug-toggle'); const isOpen = !panel.classList.contains('closed'); panel.classList.toggle('closed', isOpen); toggle.classList.toggle('open', !isOpen); document.getElementById('bug-context-label').textContent = "Reporting for: " + bugContext; document.getElementById('bug-sent').style.display = 'none'; } function submitBug(formId) { const form = document.getElementById(formId); const checks = Array.from(form.querySelectorAll('input[type=checkbox]:checked')).map(c => c.value); const detail = form.querySelector('textarea').value; const context = form.dataset.context || bugContext; if (checks.length === 0 && !detail.trim()) { alert("Please select at least one issue type or describe the problem."); return; } const report = [ "Bug Report: Walker's Appeal", "Context: " + context, "Issues: " + (checks.length ? checks.join(', ') : 'Not specified'), "Details: " + (detail.trim() || 'None provided'), "---", "Sent from Ed History Games" ].join("\n"); navigator.clipboard.writeText(report).then(() => { alert("Report copied to clipboard. Paste it wherever you need."); }).catch(() => { prompt("Copy this report:", report); }); } function submitPageBug() { const checks = ['bc1','bc2','bc3','bc4'].map(id => document.getElementById(id)).filter(el => el && el.checked).map(el => el.value); const detail = document.getElementById('bug-detail').value; if (checks.length === 0 && !detail.trim()) { alert("Please select at least one issue type or describe the problem."); return; } const report = [ "Bug Report: Walker's Appeal", "Context: " + bugContext, "Issues: " + (checks.length ? checks.join(', ') : 'Not specified'), "Details: " + (detail.trim() || 'None provided'), "---", "Sent from Ed History Games" ].join("\n"); navigator.clipboard.writeText(report).then(() => { document.getElementById('bug-sent').style.display = 'block'; document.getElementById('bug-sent').textContent = 'Copied to clipboard. Paste it wherever you need.'; document.getElementById('bug-panel').classList.add('closed'); document.getElementById('bug-toggle').classList.remove('open'); setTimeout(() => { document.getElementById('bug-sent').style.display = 'none'; }, 4000); }).catch(() => { // Fallback: show text in a prompt so user can copy manually prompt("Copy this report:", report); }); } // BADGE function showBadge() { document.getElementById('badge-popup').classList.remove('hidden'); } function dismissBadge() { document.getElementById('badge-popup').classList.add('hidden'); }
Bug Report Reporting for: General
Report sent. Thank you.
Report an Issue Current passage
Report sent. Thank you.
Ed History Games — Carolina Uncovered ✦ ✦ ✦

Walker's Appeal

To the Coloured Citizens of the World


A note before you begin This document was written in 1829 by David Walker, a free Black man from Wilmington, North Carolina. It contains language that was standard in its time but that we recognize differently today, including terms for race and for people of other faiths. Those words are part of the historical record. The glossary in this game will help you understand how they were used in 1829 and how we understand them now. Walker's argument is urgent, radical, and historically significant. Read it as he wrote it.

David Walker published his Appeal to the Coloured Citizens of the World in three editions between 1829 and 1830. It was the most incendiary abolitionist document of its century. Copies were sewn into the clothing of sailors and smuggled into Southern ports. Several states passed laws making it illegal to possess. Walker was found dead in his shop in Boston in 1830, less than a year after the third edition was published. The cause was never determined.

This game covers the Preamble -- the opening section where Walker lays out his argument, his evidence, and his challenge to both Black Americans and white Americans. Read carefully. Walker chose every word.

Choose your level to begin
Have a class code? Enter it first.
No code -- play without one

Levels 2 and 3 are free. Level 1 requires a school subscription.