dispatchAI/tiny-model-dungeon
0
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>dispatchAI — Tiny Model Dungeon</title>7 <style>8 :root {9 --ink: #0A0F1A;10 --off-white: #F5F7FA;11 --electric-blue: #2E6BFF;12 --cyan: #1FE0E6;13 --dungeon-dark: #0d0d14;14 --dungeon-bg: #14141f;15 }16 * { margin: 0; padding: 0; box-sizing: border-box; }17 body {18 font-family: 'Courier New', 'SF Mono', monospace;19 background: var(--dungeon-dark);20 color: var(--off-white);21 min-height: 100vh;22 display: flex;23 flex-direction: column;24 align-items: center;25 padding: 10px;26 }27 .header {28 text-align: center;29 margin-bottom: 15px;30 }31 .title {32 font-size: 1.8em;33 font-weight: bold;34 color: var(--cyan);35 text-shadow: 0 0 20px rgba(31, 224, 230, 0.3);36 }37 .subtitle {38 color: #666;39 font-size: 0.75em;40 margin-top: 3px;41 }42 .game-container {43 width: 100%;44 max-width: 800px;45 background: var(--dungeon-bg);46 border: 2px solid #1a2332;47 border-radius: 8px;48 overflow: hidden;49 display: flex;50 flex-direction: column;51 height: 600px;52 }53 .game-header {54 background: linear-gradient(135deg, rgba(46, 107, 255, 0.1), rgba(31, 224, 230, 0.1));55 padding: 8px 15px;56 border-bottom: 1px solid #1a2332;57 display: flex;58 justify-content: space-between;59 align-items: center;60 font-size: 0.8em;61 }62 .hp-bar {63 display: flex;64 align-items: center;65 gap: 5px;66 }67 .hp-track {68 width: 100px;69 height: 8px;70 background: #1a2332;71 border-radius: 4px;72 overflow: hidden;73 }74 .hp-fill {75 height: 100%;76 background: linear-gradient(90deg, #ff4444, #ffaa00);77 transition: width 0.3s;78 }79 .game-screen {80 flex: 1;81 overflow-y: auto;82 padding: 15px;83 display: flex;84 flex-direction: column;85 gap: 8px;86 }87 .game-screen::-webkit-scrollbar { width: 6px; }88 .game-screen::-webkit-scrollbar-thumb { background: #1a2332; border-radius: 3px; }89 .narration {90 color: #aab4c8;91 font-size: 0.9em;92 line-height: 1.6;93 white-space: pre-wrap;94 }95 .narration.ai {96 color: var(--cyan);97 border-left: 3px solid var(--cyan);98 padding-left: 10px;99 }100 .action {101 color: var(--electric-blue);102 font-size: 0.85em;103 font-style: italic;104 }105 .system {106 color: #555;107 font-size: 0.75em;108 text-align: center;109 font-style: italic;110 }111 .input-area {112 padding: 10px 15px;113 border-top: 1px solid #1a2332;114 display: flex;115 gap: 8px;116 }117 .input-area input {118 flex: 1;119 background: var(--dungeon-dark);120 border: 1px solid #1a2332;121 color: var(--off-white);122 padding: 10px 14px;123 border-radius: 6px;124 font-family: inherit;125 font-size: 0.9em;126 outline: none;127 }128 .input-area input:focus { border-color: var(--cyan); }129 .input-area input:disabled { opacity: 0.5; }130 .input-area button {131 background: var(--cyan);132 color: var(--ink);133 border: none;134 padding: 10px 20px;135 border-radius: 6px;136 font-family: inherit;137 font-weight: bold;138 font-size: 0.85em;139 cursor: pointer;140 }141 .input-area button:disabled { opacity: 0.5; cursor: not-allowed; }142 .quick-actions {143 display: flex;144 gap: 5px;145 padding: 0 15px 10px;146 flex-wrap: wrap;147 }148 .quick-btn {149 background: #1a2332;150 border: 1px solid #233;151 color: #8892a6;152 padding: 5px 12px;153 border-radius: 4px;154 font-family: inherit;155 font-size: 0.75em;156 cursor: pointer;157 transition: all 0.2s;158 }159 .quick-btn:hover { border-color: var(--cyan); color: var(--cyan); }160 .stats-bar {161 width: 100%;162 max-width: 800px;163 margin-top: 10px;164 display: flex;165 gap: 8px;166 flex-wrap: wrap;167 justify-content: center;168 }169 .stat {170 background: var(--dungeon-bg);171 border: 1px solid #1a2332;172 border-radius: 4px;173 padding: 5px 10px;174 font-size: 0.7em;175 color: #666;176 }177 .stat span { color: var(--cyan); }178 .footer {179 margin-top: 15px;180 text-align: center;181 color: #444;182 font-size: 0.7em;183 }184 .footer a { color: var(--electric-blue); text-decoration: none; }185 .loading-overlay {186 position: fixed;187 inset: 0;188 background: rgba(10, 15, 26, 0.95);189 display: flex;190 flex-direction: column;191 align-items: center;192 justify-content: center;193 z-index: 100;194 gap: 15px;195 }196 .loading-overlay.hidden { display: none; }197 .loading-spinner {198 width: 40px;199 height: 40px;200 border: 3px solid #1a2332;201 border-top-color: var(--cyan);202 border-radius: 50%;203 animation: spin 1s linear infinite;204 }205 @keyframes spin { to { transform: rotate(360deg); } }206 .start-btn {207 background: linear-gradient(135deg, var(--electric-blue), var(--cyan));208 color: var(--ink);209 border: none;210 padding: 15px 40px;211 border-radius: 8px;212 font-family: inherit;213 font-weight: bold;214 font-size: 1.1em;215 cursor: pointer;216 }217 </style>218</head>219<body>220 <div class="header">221 <div class="title">⚔️ Tiny Model Dungeon</div>222 <div class="subtitle">An AI text adventure powered by a 360M parameter model running in your browser</div>223 </div>224 225 <div class="loading-overlay" id="loadingOverlay">226 <div class="loading-spinner"></div>227 <div id="loadingText" style="color: var(--cyan); font-size: 0.9em;">Loading the dungeon master (360M model)...</div>228 <div style="color: #555; font-size: 0.75em;">~200MB download, cached for future visits</div>229 </div>230 231 <div class="game-container">232 <div class="game-header">233 <span style="color: var(--cyan);">Dungeon Level 1</span>234 <div class="hp-bar">235 <span style="color: #ff4444;">HP</span>236 <div class="hp-track"><div class="hp-fill" id="hpFill" style="width: 100%"></div></div>237 <span id="hpText" style="color: #8892a6; font-size: 0.85em;">100/100</span>238 </div>239 </div>240 <div class="game-screen" id="gameScreen">241 <div class="system">The dungeon awaits... Click "Enter" to begin your adventure.</div>242 </div>243 <div class="quick-actions">244 <button class="quick-btn" onclick="quickAction('look around')" disabled>Look around</button>245 <button class="quick-btn" onclick="quickAction('check inventory')" disabled>Inventory</button>246 <button class="quick-btn" onclick="quickAction('attack')" disabled>Attack</button>247 <button class="quick-btn" onclick="quickAction('flee')" disabled>Flee</button>248 <button class="quick-btn" onclick="quickAction('search for treasure')" disabled>Search</button>249 </div>250 <div class="input-area">251 <input type="text" id="input" placeholder="What do you do?" disabled autocomplete="off">252 <button id="sendBtn" disabled>Go</button>253 </div>254 </div>255 256 <div class="stats-bar">257 <div class="stat">Model: <span>SmolLM2-360M</span></div>258 <div class="stat">Size: <span>~200MB</span></div>259 <div class="stat">Backend: <span id="backend">—</span></div>260 <div class="stat">Tokens/s: <span id="tps">—</span></div>261 </div>262 263 <div class="footer">264 The entire game AI fits in 200MB and runs on your device. No server, no cloud, no cost.<br>265 <a href="https://huggingface.co/dispatchAI" target="_blank">dispatchAI</a> |266 <a href="https://huggingface.co/dispatchAI/SmolLM2-360M-Instruct-mobile" target="_blank">Model</a> |267 <a href="https://github.com/xenova/transformers.js" target="_blank">transformers.js</a>268 </div>269 270 <script type="module">271 import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.0';272 273 env.allowLocalModels = false;274 env.useBrowserCache = true;275 276 let generator = null;277 let isGenerating = false;278 let hp = 100;279 let gameState = { room: 1, inventory: [], hasSword: false, monstersDefeated: 0 };280 281 const loadingOverlay = document.getElementById('loadingOverlay');282 const loadingText = document.getElementById('loadingText');283 const gameScreen = document.getElementById('gameScreen');284 const input = document.getElementById('input');285 const sendBtn = document.getElementById('sendBtn');286 const hpFill = document.getElementById('hpFill');287 const hpText = document.getElementById('hpText');288 const tpsEl = document.getElementById('tps');289 const backendEl = document.getElementById('backend');290 291 const SYSTEM_PROMPT = `You are the dungeon master in a text adventure game. The player explores a dungeon. Describe rooms, monsters, and treasure vividly but concisely (2-3 sentences). React to player actions creatively. If the player attacks, describe the combat. If they search, describe what they find. Keep the adventure exciting and dangerous.`;292 293 const MODEL_ID = 'onnx-community/SmolLM2-360M-Instruct-ONNX';294 295 function addMessage(text, type) {296 const div = document.createElement('div');297 div.className = type;298 div.textContent = text;299 gameScreen.appendChild(div);300 gameScreen.scrollTop = gameScreen.scrollHeight;301 }302 303 async function initGame() {304 loadingText.textContent = 'Checking WebGPU support...';305 let device = 'wasm';306 let dtype = 'q8';307 if (navigator.gpu) {308 try {309 const adapter = await navigator.gpu.requestAdapter();310 if (adapter) { device = 'webgpu'; dtype = 'q4f16'; }311 } catch(e) {}312 }313 backendEl.textContent = device.toUpperCase();314 315 loadingText.textContent = `Downloading dungeon master (${device.toUpperCase()})...`;316 const t0 = performance.now();317 318 try {319 generator = await pipeline('text-generation', MODEL_ID, {320 device: device,321 dtype: dtype,322 });323 const loadTime = ((performance.now() - t0) / 1000).toFixed(1);324 loadingText.textContent = `Loaded in ${loadTime}s!`;325 setTimeout(() => {326 loadingOverlay.classList.add('hidden');327 startGame();328 }, 500);329 } catch(e) {330 loadingText.textContent = `Error: ${e.message}`;331 console.error(e);332 }333 }334 335 function startGame() {336 addMessage('You stand before the entrance to an ancient dungeon. The air is cold. Darkness stretches below.', 'narration ai');337 addMessage('Type your action or use the quick buttons above. Good luck, adventurer.', 'system');338 input.disabled = false;339 sendBtn.disabled = false;340 document.querySelectorAll('.quick-btn').forEach(b => b.disabled = false);341 input.focus();342 }343 344 async function takeAction(action) {345 if (isGenerating || !generator) return;346 if (!action.trim()) return;347 348 input.value = '';349 input.disabled = true;350 sendBtn.disabled = true;351 isGenerating = true;352 document.querySelectorAll('.quick-btn').forEach(b => b.disabled = true);353 354 addMessage(`> ${action}`, 'action');355 356 const messages = [357 { role: 'system', content: SYSTEM_PROMPT },358 { role: 'user', content: action },359 ];360 361 try {362 const t0 = performance.now();363 const output = await generator(messages, {364 max_new_tokens: 100,365 do_sample: true,366 temperature: 0.8,367 });368 const elapsed = (performance.now() - t0) / 1000;369 const text = output[0].generated_text[2].content;370 const tokenCount = Math.ceil(text.length / 4);371 const tps = (tokenCount / elapsed).toFixed(1);372 tpsEl.textContent = `${tps}`;373 374 // Type out the narration375 const aiMsg = document.createElement('div');376 aiMsg.className = 'narration ai';377 gameScreen.appendChild(aiMsg);378 let i = 0;379 const typeInterval = setInterval(() => {380 if (i < text.length) {381 aiMsg.textContent = text.slice(0, i + 1);382 gameScreen.scrollTop = gameScreen.scrollHeight;383 i++;384 } else {385 clearInterval(typeInterval);386 }387 }, 15);388 389 // Random HP changes for game feel390 if (action.toLowerCase().includes('attack')) {391 const dmg = Math.floor(Math.random() * 20) + 5;392 hp = Math.max(0, hp - dmg);393 updateHP();394 if (hp <= 0) {395 setTimeout(() => addMessage('You have fallen. The dungeon claims another soul... Refresh to try again.', 'system'), 2000);396 }397 }398 if (action.toLowerCase().includes('search') || action.toLowerCase().includes('treasure')) {399 if (Math.random() > 0.5) {400 hp = Math.min(100, hp + 10);401 updateHP();402 }403 }404 405 } catch(e) {406 addMessage(`Error: ${e.message}`, 'system');407 } finally {408 input.disabled = false;409 sendBtn.disabled = false;410 isGenerating = false;411 document.querySelectorAll('.quick-btn').forEach(b => b.disabled = false);412 input.focus();413 }414 }415 416 function updateHP() {417 hpFill.style.width = `${hp}%`;418 hpText.textContent = `${hp}/100`;419 }420 421 window.quickAction = takeAction;422 423 sendBtn.addEventListener('click', () => takeAction(input.value));424 input.addEventListener('keydown', (e) => {425 if (e.key === 'Enter') takeAction(input.value);426 });427 428 // Start loading immediately429 initGame();430 </script>431</body>432</html>433 