CoolFace
Apppublic

dispatchAI/tiny-model-dungeon

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
index.html433 linesDownload Raw Back to root
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