3morixd commited on
Commit
199beca
·
verified ·
1 Parent(s): 1ef2df5

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. README.md +42 -4
  2. index.html +431 -18
README.md CHANGED
@@ -1,10 +1,48 @@
1
  ---
2
  title: Tiny Model Dungeon
3
- emoji: 😻
4
- colorFrom: gray
5
  colorTo: blue
6
  sdk: static
7
- pinned: false
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Tiny Model Dungeon
3
+ emoji: ⚔️
4
+ colorFrom: indigo
5
  colorTo: blue
6
  sdk: static
7
+ app_file: index.html
8
+ pinned: true
9
+ tags:
10
+ - game
11
+ - webgpu
12
+ - on-device
13
+ - mobile-ai
14
+ - text-adventure
15
  ---
16
 
17
+ # ⚔️ Tiny Model Dungeon
18
+
19
+ An AI-powered text adventure game where the **entire AI runs in your browser** — no server, no cloud, no API costs.
20
+
21
+ ## The pitch
22
+
23
+ > *This whole game's AI fits in 200MB.*
24
+
25
+ The dungeon master is a **360M parameter model** (SmolLM2-360M-Instruct) running entirely on your device via WebGPU. Every room description, monster encounter, and treasure find is generated in real-time by a model smaller than a single photo album.
26
+
27
+ ## How it works
28
+
29
+ 1. On page load, the model (~200MB ONNX) downloads once and is cached by your browser
30
+ 2. WebGPU accelerates inference on your GPU (falls back to WASM on older browsers)
31
+ 3. Every player action generates a unique AI narration — no scripted responses
32
+ 4. The game never contacts a server after the initial model download
33
+
34
+ ## Why?
35
+
36
+ This is dispatchAI's thesis made tangible: **small models are powerful enough for real applications.** A 360M model generating creative dungeon descriptions proves that mobile-sized AI is not a toy — it's a product.
37
+
38
+ ## Tech
39
+
40
+ - **Model**: [SmolLM2-360M-Instruct](https://huggingface.co/dispatchAI/SmolLM2-360M-Instruct-mobile)
41
+ - **Inference**: [transformers.js](https://github.com/xenova/transformers.js) v3.7.0
42
+ - **Backend**: WebGPU (with WASM fallback)
43
+ - **Hosting**: Static HuggingFace Space (free forever)
44
+ - **Cost**: $0
45
+
46
+ ---
47
+
48
+ *Built by [dispatchAI](https://huggingface.co/dispatchAI) — mobile AI that moves at the speed of need.*
index.html CHANGED
@@ -1,19 +1,432 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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 narration
375
+ 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 feel
390
+ 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 immediately
429
+ initGame();
430
+ </script>
431
+ </body>
432
  </html>