|
|
<!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-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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
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}; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
if (snake.some(s => s.x === head.x && s.y === head.y)) { gameOver(); return; } |
|
|
|
|
|
snake.unshift(head); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
winGame(); |
|
|
} else { |
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
init(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |