DP-Snake / index.html
SorrelC's picture
Update index.html
2ac2ad7 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Archive Snake</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: monospace;
background: #0a0a0f;
min-height: 100vh;
padding: 10px;
color: #00ff41;
}
.container { max-width: 1000px; margin: 0 auto; }
h1 { text-align: center; font-size: 1.6em; color: #00ff41; text-shadow: 0 0 10px #00ff41; margin: 0 0 5px 0; }
.subtitle { text-align: center; color: #ffb000; font-size: 0.85em; margin-bottom: 15px; }
.game-layout { display: flex; gap: 15px; flex-wrap: wrap; }
.game-area { flex: 1; min-width: 320px; }
.side-panel { width: 220px; }
.game-board {
background: #000;
border: 2px solid #00cc33;
border-radius: 5px;
padding: 5px;
position: relative;
box-shadow: 0 0 15px rgba(0,255,65,0.2);
}
.grid {
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 1px;
background: #111;
border: 1px solid rgba(255,68,68,0.3);
}
.cell {
aspect-ratio: 1;
background: #0a0a0a;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
min-height: 20px;
}
.cell.snake { background: #00ff41; box-shadow: 0 0 5px #00ff41; }
.cell.snake-head { background: #00ff41; box-shadow: 0 0 8px #00ff41; }
.cell.snake-body { background: rgba(0,255,65,0.6); }
.panel { background: #161b22; border: 1px solid #30363d; border-radius: 5px; padding: 10px; margin-bottom: 10px; }
.panel-title { color: #ffb000; font-size: 0.9em; border-bottom: 1px solid #30363d; padding-bottom: 5px; margin-bottom: 8px; }
.stat-row { display: flex; justify-content: space-between; font-size: 0.8em; margin-bottom: 5px; }
.stat-label { color: #8b949e; }
.stat-value { color: #00ff41; font-weight: bold; }
.stat-value.warning { color: #ff4444; animation: pulse 0.5s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.progress-container { margin-bottom: 8px; }
.progress-label { display: flex; justify-content: space-between; font-size: 0.7em; margin-bottom: 3px; }
.progress-bar { height: 6px; background: #1a1a2e; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; transition: width 0.3s; }
.progress-fill.floppy { background: linear-gradient(90deg, #1a5fb4, #3584e4); }
.progress-fill.cdrom { background: linear-gradient(90deg, #9141ac, #c061cb); }
.progress-fill.vhs { background: linear-gradient(90deg, #c01c28, #e66100); }
.progress-fill.win { background: linear-gradient(90deg, #00ff41, #ffd700); }
.controls { display: flex; gap: 8px; justify-content: center; margin: 10px 0; flex-wrap: wrap; }
button {
font-family: monospace;
font-size: 0.9em;
padding: 8px 16px;
border: 2px solid #00ff41;
background: transparent;
color: #00ff41;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
button:hover { background: #00ff41; color: #000; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button.danger { border-color: #ff4444; color: #ff4444; }
button.danger:hover { background: #ff4444; color: #000; }
.instructions { background: #161b22; border: 1px solid #30363d; border-radius: 5px; padding: 10px; font-size: 0.75em; color: #8b949e; margin-top: 10px; }
.instructions strong { color: #ffb000; }
.key { background: #0d1117; padding: 2px 6px; border-radius: 3px; color: #00ff41; }
.attribution { background: #161b22; border: 1px solid #30363d; border-radius: 5px; padding: 12px; font-size: 0.7em; color: #8b949e; margin-top: 10px; line-height: 1.6; }
.attribution a { color: #ffb000; text-decoration: none; }
.attribution a:hover { text-decoration: underline; }
/* Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85);
z-index: 100;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-overlay.show { display: flex; }
.modal {
background: #161b22;
border: 2px solid #ff4444;
border-radius: 10px;
padding: 20px;
max-width: 400px;
width: 100%;
box-shadow: 0 0 30px rgba(255,68,68,0.3);
}
.modal.success { border-color: #00ff41; box-shadow: 0 0 30px rgba(0,255,65,0.3); }
.modal.win { border-color: #ffd700; box-shadow: 0 0 40px rgba(255,215,0,0.4); }
.modal h2 { text-align: center; margin-bottom: 10px; font-size: 1.3em; }
.modal h2.error { color: #ff4444; }
.modal h2.success { color: #00ff41; }
.modal h2.win { color: #ffd700; text-shadow: 0 0 15px #ffd700; }
.modal p { text-align: center; color: #8b949e; font-size: 0.85em; margin-bottom: 10px; }
.term-box {
background: #0d1117;
border: 2px solid #ffb000;
padding: 12px;
border-radius: 6px;
text-align: center;
margin-bottom: 15px;
}
.term-box span { color: #ffb000; font-size: 1.5em; text-shadow: 0 0 10px #ffb000; }
.answer-btn {
width: 100%;
text-align: left;
padding: 10px;
margin-bottom: 8px;
background: #0d1117;
border: 1px solid #30363d;
color: #c9d1d9;
font-size: 0.8em;
line-height: 1.4;
}
.answer-btn:hover { border-color: #00ff41; background: rgba(0,255,65,0.1); }
.answer-btn.correct { border-color: #00ff41; background: rgba(0,255,65,0.2); color: #00ff41; }
.answer-btn.wrong { border-color: #ff4444; background: rgba(255,68,68,0.2); color: #ff4444; }
.quiz-stakes { text-align: center; font-size: 0.85em; margin-top: 10px; }
.quiz-stakes .gain { color: #00ff41; }
.quiz-stakes .lose { color: #ff4444; }
.final-stats { background: #0d1117; border-radius: 6px; padding: 10px; margin: 15px 0; }
.final-stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #30363d; font-size: 0.85em; }
.final-stat:last-child { border-bottom: none; }
.final-stat .label { color: #8b949e; }
.final-stat .value { color: #00ff41; }
.trophy { font-size: 3em; text-align: center; margin-bottom: 10px; animation: bounce 0.5s ease infinite alternate; }
@keyframes bounce { from { transform: translateY(0); } to { transform: translateY(-8px); } }
.win-score { font-size: 2em; color: #ffd700; text-align: center; text-shadow: 0 0 15px #ffd700; margin: 10px 0; }
.legend { font-size: 0.75em; }
.legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.difficulty-slider { width: 100%; margin: 5px 0; }
.slider-labels { display: flex; justify-content: space-between; font-size: 0.65em; color: #666; }
@media (max-width: 600px) {
.game-layout { flex-direction: column; }
.side-panel { width: 100%; }
.cell { font-size: 10px; min-height: 14px; }
}
</style>
</head>
<body>
<div class="container">
<div class="game-layout">
<div class="game-area">
<h1>🐍 DIGITAL ARCHIVE SNAKE</h1>
<p class="subtitle">Rescue Data Before the Carriers Become Obsolete</p>
<div class="game-board">
<div class="grid" id="grid"></div>
</div>
<div class="controls">
<button id="startBtn" onclick="startGame()">▶ Start Mission</button>
<button id="pauseBtn" onclick="togglePause()" disabled>⏸ Pause</button>
<button id="soundBtn" onclick="toggleSound()">🔊 Sound</button>
<button id="newGameBtn" class="danger" onclick="newGame()" style="display:none;">🔄 New Game</button>
</div>
</div>
<div class="side-panel">
<div class="panel">
<div class="panel-title">📊 Status</div>
<div class="stat-row"><span class="stat-label">Score</span><span class="stat-value" id="score">0</span></div>
<div class="stat-row"><span class="stat-label">Time</span><span class="stat-value" id="timer">1:00</span></div>
<div class="stat-row"><span class="stat-label">Quizzes</span><span class="stat-value" id="quizzes">0/0</span></div>
<div class="progress-container" style="margin-top:10px;">
<div class="progress-label"><span id="levelDisplay">📀 Level 1</span><span id="targetDisplay">Need 3 each</span></div>
<div class="progress-bar"><div class="progress-fill win" id="winProgress" style="width:0%"></div></div>
</div>
</div>
<div class="panel">
<div class="panel-title">📥 Carrier Ingest</div>
<div class="progress-container">
<div class="progress-label"><span>💾 Floppy</span><span id="floppyCount">0</span></div>
<div class="progress-bar"><div class="progress-fill floppy" id="floppyProgress" style="width:0%"></div></div>
</div>
<div class="progress-container">
<div class="progress-label"><span>💿 CD-ROM</span><span id="cdromCount">0</span></div>
<div class="progress-bar"><div class="progress-fill cdrom" id="cdromProgress" style="width:0%"></div></div>
</div>
<div class="progress-container">
<div class="progress-label"><span>📼 VHS</span><span id="vhsCount">0</span></div>
<div class="progress-bar"><div class="progress-fill vhs" id="vhsProgress" style="width:0%"></div></div>
</div>
</div>
<div class="panel">
<div class="panel-title">🎮 Starting Level</div>
<div class="stat-row"><span class="stat-label">Level</span><span class="stat-value" id="diffName">Level 1</span></div>
<input type="range" class="difficulty-slider" id="diffSlider" min="1" max="3" value="1">
<div class="slider-labels"><span>Level 1</span><span>Level 2</span><span>Level 3</span></div>
<div class="legend" style="margin-top:10px;">
<div class="legend-item"><span>☠️</span><span style="color:#8b949e">Bit Rot = Quiz!</span></div>
<div class="legend-item"><span>🧱</span><span style="color:#8b949e">Edge = Quiz!</span></div>
</div>
</div>
</div>
</div>
<div class="instructions">
<strong>📋 Mission:</strong> Ingest data from 💾💿📼 obsolete carriers before they degrade! Collect the target amount of each type to advance.<br>
<strong>Controls:</strong> <span class="key">↑↓←→</span> or <span class="key">WASD</span><br>
<strong>Hazards:</strong> ☠️ Bit Rot and 🧱 Edges trigger quizzes. <span style="color:#00ff41">Correct = +time</span>, <span style="color:#ff4444">Wrong = -time</span><br>
<span style="color:#8b949e;font-size:0.9em">The clock is ticking — carriers degrade and playback devices are disappearing. Preserve the data now!</span>
</div>
<div class="attribution">
<strong>Source:</strong> Digital Preservation Handbook, 2nd Edition, <a href="https://www.dpconline.org/handbook" target="_blank">https://www.dpconline.org/handbook</a><br>
Digital Preservation Coalition © 2015 licensed under the <a href="http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" target="_blank">Open Government Licence v3.0</a>.<br>
This interactive learning tool was created for educational purposes. For the most up-to-date glossary, please visit the <a href="https://www.dpconline.org/handbook/glossary" target="_blank">DPC Handbook Glossary</a>.
</div>
</div>
<!-- Quiz Modal -->
<div class="modal-overlay" id="quizModal">
<div class="modal" id="quizModalInner">
<h2 class="error" id="quizTitle">☠️ Bit Rot Detected!</h2>
<p>Match this preservation term:</p>
<div class="term-box"><span id="quizTerm">OAIS</span></div>
<div id="answerOptions"></div>
<div class="quiz-stakes"><span class="gain" id="quizGain">✓ +10s</span> | <span class="lose" id="quizLose">✗ -10s</span></div>
</div>
</div>
<!-- Win Modal -->
<div class="modal-overlay" id="winModal">
<div class="modal win">
<div class="trophy">🏆</div>
<h2 class="win">🎉 ALL LEVELS COMPLETE! 🎉</h2>
<p>Archive fully preserved! All data rescued from obsolete carriers!</p>
<div class="win-score" id="winScore">500</div>
<div class="final-stats">
<div class="final-stat"><span class="label">💾 Floppies Ingested</span><span class="value" id="winFloppies">0</span></div>
<div class="final-stat"><span class="label">💿 CD-ROMs Ingested</span><span class="value" id="winCDs">0</span></div>
<div class="final-stat"><span class="label">📼 VHS Tapes Ingested</span><span class="value" id="winVHS">0</span></div>
<div class="final-stat"><span class="label">Quiz Accuracy</span><span class="value" id="winAcc">0%</span></div>
</div>
<button onclick="newGame()" style="width:100%;border-color:#ffd700;color:#ffd700;">🎮 Play Again</button>
</div>
</div>
<!-- Game Over Modal -->
<div class="modal-overlay" id="overModal">
<div class="modal">
<h2 class="error" id="overTitle">⏰ Time Expired!</h2>
<div class="final-stats">
<div class="final-stat"><span class="label">Final Score</span><span class="value" id="finalScore">0</span></div>
<div class="final-stat"><span class="label">💾 Floppies Ingested</span><span class="value" id="finalFloppies">0</span></div>
<div class="final-stat"><span class="label">💿 CD-ROMs Ingested</span><span class="value" id="finalCDs">0</span></div>
<div class="final-stat"><span class="label">📼 VHS Tapes Ingested</span><span class="value" id="finalVHS">0</span></div>
<div class="final-stat"><span class="label">Quiz Accuracy</span><span class="value" id="finalAcc">0%</span></div>
</div>
<button onclick="newGame()" style="width:100%;">🔄 Try Again</button>
</div>
</div>
<!-- Level Complete Modal -->
<div class="modal-overlay" id="levelModal">
<div class="modal success">
<h2 class="success" id="levelCompleteTitle">🎉 Level 1 Complete!</h2>
<p>All target carriers successfully ingested!</p>
<div class="final-stats">
<div class="final-stat"><span class="label">Score</span><span class="value" id="levelScore">0</span></div>
<div class="final-stat"><span class="label">💾 Floppies</span><span class="value" id="levelFloppies">0</span></div>
<div class="final-stat"><span class="label">💿 CD-ROMs</span><span class="value" id="levelCDs">0</span></div>
<div class="final-stat"><span class="label">📼 VHS Tapes</span><span class="value" id="levelVHS">0</span></div>
</div>
<div style="background:#0d1117;padding:10px;border-radius:5px;margin:10px 0;text-align:center;">
<div style="color:#ffb000;font-size:0.9em;">⚠️ Next: <span id="nextLevelName">Level 2</span></div>
<div style="color:#8b949e;font-size:0.8em;margin-top:5px;">Target: <span id="nextLevelTarget">4</span> of each | ☠️ Bit Rots: <span id="nextLevelRots">5</span></div>
</div>
<button onclick="startNextLevel()" style="width:100%;border-color:#00ff41;color:#00ff41;">▶ Continue to Next Level</button>
</div>
</div>
<script>
// Audio system
let audioCtx = null;
let soundEnabled = true;
function initAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
}
}
function toggleSound() {
soundEnabled = !soundEnabled;
document.getElementById('soundBtn').textContent = soundEnabled ? '🔊 Sound' : '🔇 Muted';
}
function playSound(type) {
if (!audioCtx || !soundEnabled) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'collect') {
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start(); osc.stop(audioCtx.currentTime + 0.1);
} else if (type === 'correct') {
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start(); osc.stop(audioCtx.currentTime + 0.2);
} else if (type === 'wrong') {
osc.frequency.setValueAtTime(200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.25);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.25);
osc.start(); osc.stop(audioCtx.currentTime + 0.25);
} else if (type === 'bonus') {
[523, 659, 784].forEach((freq, i) => {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
o.frequency.setValueAtTime(freq, audioCtx.currentTime + i * 0.1);
g.gain.setValueAtTime(0.08, audioCtx.currentTime + i * 0.1);
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + i * 0.1 + 0.15);
o.start(audioCtx.currentTime + i * 0.1);
o.stop(audioCtx.currentTime + i * 0.1 + 0.15);
});
} else if (type === 'win') {
const notes = [523, 659, 784, 1047, 784, 1047];
const durs = [0.12, 0.12, 0.12, 0.2, 0.12, 0.3];
let t = audioCtx.currentTime;
notes.forEach((freq, i) => {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(0.12, t);
g.gain.exponentialRampToValueAtTime(0.01, t + durs[i]);
o.start(t); o.stop(t + durs[i]);
t += durs[i];
});
} else if (type === 'hazard') {
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.setValueAtTime(100, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
osc.start(); osc.stop(audioCtx.currentTime + 0.15);
} else if (type === 'gameover') {
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.5);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
osc.start(); osc.stop(audioCtx.currentTime + 0.5);
}
} catch(e) {}
}
const COLS = 25, ROWS = 15, TIME = 60, GOAL = 5;
const DIFF = { 1: {name:'Level 1',rots:2,gain:12,loss:4,target:3}, 2: {name:'Level 2',rots:5,gain:10,loss:8,target:4}, 3: {name:'Level 3',rots:8,gain:8,loss:12,target:5} };
const ARTS = [ {e:'💾',pts:10,k:'floppy',b:10}, {e:'💿',pts:20,k:'cdrom',b:15}, {e:'📼',pts:30,k:'vhs',b:20} ];
const GLOSS = [
{t:"OAIS",d:"Open Archival Information System - reference model for digital archives"},
{t:"AIP",d:"Archival Information Package - preserved within an OAIS"},
{t:"SIP",d:"Submission Information Package - delivered to construct AIPs"},
{t:"DIP",d:"Dissemination Information Package - sent to users on request"},
{t:"Checksum",d:"Unique numerical signature to verify file integrity"},
{t:"Bit Preservation",d:"Basic preservation including backups and fixity-checking"},
{t:"Born-Digital",d:"Digital materials with no analogue equivalent"},
{t:"Emulation",d:"Imitating obsolete systems on future computers"},
{t:"Migration",d:"Transferring resources between hardware/software generations"},
{t:"Fixity Check",d:"Verifying a file has not been altered or corrupted"},
{t:"Ingest",d:"Turning a SIP into an AIP in a digital archive"},
{t:"PREMIS",d:"Preservation Metadata Implementation Strategies standard"},
{t:"PRONOM",d:"Database of file formats for long-term access"},
{t:"DROID",d:"File format identification tool based on PRONOM"},
{t:"Dark Archive",d:"Archive not accessible until trigger events occur"},
{t:"TDR",d:"Trusted Digital Repository for reliable long-term access"},
{t:"WARC",d:"Web ARChive format for archived websites"},
{t:"Metadata",d:"Information describing significant aspects of a resource"},
{t:"PDF/A",d:"PDF versions intended for archival use"},
{t:"Authenticity",d:"The digital material is what it purports to be"}
];
let grid, cells, snake, dir, nextDir, arts, rots, score, time, diff, coll, bonus, qC, qT, running, paused, quiz, quizReason, pending, gameLoop, timerLoop, level;
function init() {
grid = document.getElementById('grid');
grid.innerHTML = '';
cells = [];
for (let i = 0; i < ROWS * COLS; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
grid.appendChild(cell);
cells.push(cell);
}
document.getElementById('diffSlider').addEventListener('input', e => {
const startLvl = +e.target.value;
document.getElementById('diffName').textContent = DIFF[startLvl].name;
document.getElementById('levelDisplay').textContent = '📀 ' + DIFF[startLvl].name;
document.getElementById('targetDisplay').textContent = 'Need ' + DIFF[startLvl].target + ' each';
});
document.addEventListener('keydown', handleKey);
level = 1;
diff = 1;
render();
}
function idx(x, y) { return y * COLS + x; }
function randPos() {
let x, y, tries = 0;
do {
x = Math.floor(Math.random() * COLS);
y = Math.floor(Math.random() * ROWS);
tries++;
} while (tries < 100 && (snake.some(s => s.x === x && s.y === y) || arts.some(a => a.x === x && a.y === y) || rots.some(r => r.x === x && r.y === y)));
return {x, y};
}
function startGame(startLevel = null) {
initAudio();
if (startLevel === null) {
startLevel = +document.getElementById('diffSlider').value;
}
level = startLevel;
diff = level;
snake = [{x:5,y:7},{x:4,y:7},{x:3,y:7}];
dir = {x:1,y:0}; nextDir = {x:1,y:0};
arts = []; rots = [];
score = 0; time = TIME; qC = 0; qT = 0;
coll = {floppy:0,cdrom:0,vhs:0};
bonus = {floppy:0,cdrom:0,vhs:0};
running = true; paused = false; quiz = null; pending = null;
for (let i = 0; i < 5; i++) { const p = randPos(); arts.push({...p, ...ARTS[Math.floor(Math.random()*ARTS.length)]}); }
for (let i = 0; i < DIFF[diff].rots; i++) rots.push(randPos());
document.getElementById('startBtn').style.display = 'none';
document.getElementById('newGameBtn').style.display = 'inline-block';
document.getElementById('pauseBtn').disabled = false;
document.getElementById('pauseBtn').textContent = '⏸ Pause';
document.getElementById('diffSlider').disabled = true;
document.getElementById('levelDisplay').textContent = '📀 ' + DIFF[level].name;
document.getElementById('targetDisplay').textContent = 'Need ' + DIFF[level].target + ' each';
document.getElementById('diffName').textContent = DIFF[level].name;
clearInterval(gameLoop); clearInterval(timerLoop);
gameLoop = setInterval(update, 150);
timerLoop = setInterval(tick, 1000);
updateUI(); render();
}
function newGame() {
clearInterval(gameLoop); clearInterval(timerLoop);
document.getElementById('quizModal').classList.remove('show');
document.getElementById('winModal').classList.remove('show');
document.getElementById('overModal').classList.remove('show');
document.getElementById('levelModal').classList.remove('show');
document.getElementById('diffSlider').disabled = false;
startGame();
}
function togglePause() {
if (!running) return;
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? '▶ Resume' : '⏸ Pause';
if (paused) { clearInterval(gameLoop); clearInterval(timerLoop); }
else { gameLoop = setInterval(update, 150); timerLoop = setInterval(tick, 1000); }
}
function update() {
if (paused || quiz || !running) return;
dir = {...nextDir};
const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};
// Edge
if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
if (head.x < 0) head.x = COLS - 1;
if (head.x >= COLS) head.x = 0;
if (head.y < 0) head.y = ROWS - 1;
if (head.y >= ROWS) head.y = 0;
pending = head;
triggerQuiz('edge');
return;
}
// Self
if (snake.some(s => s.x === head.x && s.y === head.y)) { gameOver(); return; }
snake.unshift(head);
// Artifact
let ate = false;
const artIdx = arts.findIndex(a => a.x === head.x && a.y === head.y);
if (artIdx !== -1) {
ate = true;
const art = arts[artIdx];
score += art.pts;
coll[art.k]++;
playSound('collect');
checkBonus(art.k, art.b);
arts.splice(artIdx, 1);
const p = randPos();
arts.push({...p, ...ARTS[Math.floor(Math.random()*ARTS.length)]});
if (checkLevelComplete()) { levelComplete(); return; }
}
if (!ate) snake.pop();
// Bit rot
const rotIdx = rots.findIndex(r => r.x === head.x && r.y === head.y);
if (rotIdx !== -1) {
rots[rotIdx] = randPos();
triggerQuiz('bitrot');
return;
}
updateUI(); render();
}
function checkBonus(k, b) {
const lvl = Math.floor(coll[k] / GOAL);
if (lvl > bonus[k]) {
time += b;
score += 50;
bonus[k] = lvl;
playSound('bonus');
}
}
function checkLevelComplete() {
const target = DIFF[level].target;
if (coll.floppy >= target && coll.cdrom >= target && coll.vhs >= target) {
return true;
}
return false;
}
function tick() {
if (paused || quiz || !running) return;
time--;
updateUI();
if (time <= 0) gameOver();
}
function triggerQuiz(reason) {
playSound('hazard');
quiz = true; quizReason = reason;
const i = Math.floor(Math.random() * GLOSS.length);
const correct = GLOSS[i];
let wrong = [];
while (wrong.length < 3) {
const j = Math.floor(Math.random() * GLOSS.length);
if (j !== i && !wrong.some(w => w.t === GLOSS[j].t)) wrong.push(GLOSS[j]);
}
const answers = [correct, ...wrong].sort(() => Math.random() - 0.5);
document.getElementById('quizTitle').textContent = reason === 'edge' ? '🧱 Edge Impact!' : '☠️ Bit Rot Detected!';
document.getElementById('quizTerm').textContent = correct.t;
document.getElementById('quizGain').textContent = '✓ +' + DIFF[diff].gain + 's';
document.getElementById('quizLose').textContent = '✗ -' + DIFF[diff].loss + 's';
const opts = document.getElementById('answerOptions');
opts.innerHTML = '';
answers.forEach(a => {
const btn = document.createElement('button');
btn.className = 'answer-btn';
btn.textContent = a.d;
btn.onclick = () => answerQuiz(a.t === correct.t, btn, correct.d);
opts.appendChild(btn);
});
document.getElementById('quizModalInner').classList.remove('success');
document.getElementById('quizModal').classList.add('show');
}
function answerQuiz(isCorrect, btn, correctDef) {
qT++;
document.querySelectorAll('.answer-btn').forEach(b => {
b.onclick = null;
if (b.textContent === correctDef) b.classList.add('correct');
});
const modal = document.getElementById('quizModalInner');
const title = document.getElementById('quizTitle');
if (isCorrect) {
qC++;
time += DIFF[diff].gain;
score += 25;
btn.classList.add('correct');
modal.classList.add('success');
title.textContent = '✓ Correct!';
title.className = 'success';
playSound('correct');
} else {
time = Math.max(0, time - DIFF[diff].loss);
btn.classList.add('wrong');
title.textContent = '✗ Wrong!';
title.className = 'error';
playSound('wrong');
}
updateUI();
setTimeout(() => {
document.getElementById('quizModal').classList.remove('show');
quiz = null;
if (pending) { snake.unshift(pending); snake.pop(); pending = null; }
if (time <= 0) gameOver();
else render();
}, 1200);
}
function updateUI() {
document.getElementById('score').textContent = score;
const m = Math.floor(Math.max(0,time)/60), s = Math.max(0,time)%60;
const timerEl = document.getElementById('timer');
timerEl.textContent = m + ':' + s.toString().padStart(2,'0');
timerEl.className = 'stat-value' + (time <= 15 ? ' warning' : '');
document.getElementById('quizzes').textContent = qC + '/' + qT;
// Calculate level progress (min of all 3 types progress toward target)
const target = DIFF[level].target;
const floppyPct = Math.min(100, (coll.floppy / target) * 100);
const cdromPct = Math.min(100, (coll.cdrom / target) * 100);
const vhsPct = Math.min(100, (coll.vhs / target) * 100);
const avgPct = Math.round((floppyPct + cdromPct + vhsPct) / 3);
document.getElementById('winProgress').style.width = avgPct + '%';
['floppy','cdrom','vhs'].forEach(k => {
const count = coll[k];
const pct = Math.min(100, (count / target) * 100);
document.getElementById(k+'Count').textContent = count + '/' + target;
document.getElementById(k+'Progress').style.width = pct + '%';
});
}
function render() {
cells.forEach(c => { c.className = 'cell'; c.textContent = ''; });
rots.forEach(r => { cells[idx(r.x,r.y)].textContent = '☠️'; });
arts.forEach(a => { cells[idx(a.x,a.y)].textContent = a.e; });
snake.forEach((s,i) => {
const c = cells[idx(s.x,s.y)];
c.className = 'cell ' + (i === 0 ? 'snake-head' : 'snake-body');
});
}
function levelComplete() {
running = false;
clearInterval(gameLoop); clearInterval(timerLoop);
playSound('win');
if (level >= 3) {
// Beat all levels - final win!
winGame();
} else {
// Show level complete modal, then advance
document.getElementById('levelCompleteTitle').textContent = '🎉 ' + DIFF[level].name + ' Complete!';
document.getElementById('levelScore').textContent = score;
document.getElementById('levelFloppies').textContent = coll.floppy;
document.getElementById('levelCDs').textContent = coll.cdrom;
document.getElementById('levelVHS').textContent = coll.vhs;
document.getElementById('nextLevelName').textContent = DIFF[level + 1].name;
document.getElementById('nextLevelTarget').textContent = DIFF[level + 1].target;
document.getElementById('nextLevelRots').textContent = DIFF[level + 1].rots;
document.getElementById('levelModal').classList.add('show');
}
}
function startNextLevel() {
document.getElementById('levelModal').classList.remove('show');
startGame(level + 1);
}
function winGame() {
running = false;
clearInterval(gameLoop); clearInterval(timerLoop);
document.getElementById('pauseBtn').disabled = true;
playSound('win');
const acc = qT > 0 ? Math.round(qC/qT*100) : 0;
document.getElementById('winScore').textContent = score;
document.getElementById('winFloppies').textContent = coll.floppy;
document.getElementById('winCDs').textContent = coll.cdrom;
document.getElementById('winVHS').textContent = coll.vhs;
document.getElementById('winAcc').textContent = acc + '%';
document.getElementById('winModal').classList.add('show');
}
function gameOver() {
running = false;
clearInterval(gameLoop); clearInterval(timerLoop);
document.getElementById('pauseBtn').disabled = true;
playSound('gameover');
const acc = qT > 0 ? Math.round(qC/qT*100) : 0;
document.getElementById('overTitle').textContent = time <= 0 ? '⏰ Time Expired!' : '💀 Archive Corrupted!';
document.getElementById('finalScore').textContent = score;
document.getElementById('finalFloppies').textContent = coll.floppy;
document.getElementById('finalCDs').textContent = coll.cdrom;
document.getElementById('finalVHS').textContent = coll.vhs;
document.getElementById('finalAcc').textContent = acc + '%';
document.getElementById('overModal').classList.add('show');
}
function handleKey(e) {
if (!running || paused || quiz) return;
const k = e.key.toLowerCase();
if ((k === 'arrowup' || k === 'w') && dir.y !== 1) nextDir = {x:0,y:-1};
else if ((k === 'arrowdown' || k === 's') && dir.y !== -1) nextDir = {x:0,y:1};
else if ((k === 'arrowleft' || k === 'a') && dir.x !== 1) nextDir = {x:-1,y:0};
else if ((k === 'arrowright' || k === 'd') && dir.x !== -1) nextDir = {x:1,y:0};
if (['arrowup','arrowdown','arrowleft','arrowright'].includes(k)) e.preventDefault();
}
// Initialize on load
init();
</script>
</body>
</html>