Commit cb83ff3077

cb83ff30771a50411606057c5cfd8487bf0303eb

parent: c74dd95b19

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-27 06:16 UTC

Add vector tile graphics with an ASCII toggle

sprites.js holds 24 SVG symbols (32x32 viewBox) whose gradients read
the floor theme's CSS variables, so each floor keeps its palette. When
graphics are on, render() fills each grid cell with gfxCell() layers
(floor variant + sprite; walls pick a top or front face) instead of the
glyph. Cells scale to 40px, down to phone width. Lettered exits draw as
archways with the letter; unknown tiles fall back to the glyph.

g toggles graphics/ASCII and is stored in localStorage (default on).
sprites.js loads before data.js so validate.js still finds the inline
engine; validate.js now evaluates it and stubs classList and
insertAdjacentHTML.

Layout: unified · split

1mb/index.html +7 −2
@@ -53,6 +53,7 @@ linear-gradient(180deg,transparent,rgba(0,0,0,.12));mix-blend-mode:screen;opacit
53#s[data-state="win"]{--accent:#f3dd8f;--panelEdge:#7a6930;--gridEdge:#6e5f2c;--mist:#f3dd8f18} 53#s[data-state="win"]{--accent:#f3dd8f;--panelEdge:#7a6930;--gridEdge:#6e5f2c;--mist:#f3dd8f18}
54#s[data-state="camp"]{--accent:#c8f2dd;--panelEdge:#376050;--gridEdge:#2f5648;--mist:#8ff0cc18} 54#s[data-state="camp"]{--accent:#c8f2dd;--panelEdge:#376050;--gridEdge:#2f5648;--mist:#8ff0cc18}
55@media (max-width:780px){#s{justify-content:flex-start;padding:12px 10px 20px}#p{flex-direction:column;align-items:center;width:100%}#g,#i,#g.splash,#l,#u{width:min(92vw,420px);max-width:100%}#i,#g.splash{min-width:0}#g.splash{overflow:auto}#s[data-state="play"] #d{display:grid}} 55@media (max-width:780px){#s{justify-content:flex-start;padding:12px 10px 20px}#p{flex-direction:column;align-items:center;width:100%}#g,#i,#g.splash,#l,#u{width:min(92vw,420px);max-width:100%}#i,#g.splash{min-width:0}#g.splash{overflow:auto}#s[data-state="play"] #d{display:grid}}
56#s.gfx{--cs:min(40px,calc((92vw - 26px)/12))}#s.gfx #g:not(.splash){grid-template-columns:repeat(12,var(--cs));gap:0}#s.gfx .t{width:var(--cs);height:var(--cs);font-size:0;text-shadow:none}.t>svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.t>b{position:relative;font-size:calc(var(--cs)*.6)}.t>b.dl{font-size:calc(var(--cs)*.34);color:var(--exit);top:10%}.B.alert{--eye:#ffa8e4}.B.enraged{--eye:#ff4040}
56@keyframes flicker{0%,100%{opacity:1;transform:translateY(0)}25%{opacity:.75;transform:translateY(-.5px)}50%{opacity:.94}75%{opacity:.68;transform:translateY(.5px)}} 57@keyframes flicker{0%,100%{opacity:1;transform:translateY(0)}25%{opacity:.75;transform:translateY(-.5px)}50%{opacity:.94}75%{opacity:.68;transform:translateY(.5px)}}
57@keyframes pulse{0%,100%{opacity:.84;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}} 58@keyframes pulse{0%,100%{opacity:.84;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
58@keyframes step{0%{transform:scale(.82)}100%{transform:scale(1)}} 59@keyframes step{0%{transform:scale(.82)}100%{transform:scale(1)}}
@@ -61,6 +62,7 @@ linear-gradient(180deg,transparent,rgba(0,0,0,.12));mix-blend-mode:screen;opacit
61@keyframes relicpulse{0%{transform:scale(1)}50%{transform:scale(1.035)}100%{transform:scale(1)}} 62@keyframes relicpulse{0%{transform:scale(1)}50%{transform:scale(1.035)}100%{transform:scale(1)}}
62</style> 63</style>
63<div id=s><div id=u></div><div id=p><div id=g></div><div id=i></div></div><div id=l></div><div id=d><button data-k=ArrowUp class=pu>^</button><button data-k=ArrowLeft class=pl>&lt;</button><button data-k=. class=pw>*</button><button data-k=ArrowRight class=pr>&gt;</button><button data-k=ArrowDown class=pd>v</button></div><button id=r hidden>restart</button></div> 64<div id=s><div id=u></div><div id=p><div id=g></div><div id=i></div></div><div id=l></div><div id=d><button data-k=ArrowUp class=pu>^</button><button data-k=ArrowLeft class=pl>&lt;</button><button data-k=. class=pw>*</button><button data-k=ArrowRight class=pr>&gt;</button><button data-k=ArrowDown class=pd>v</button></div><button id=r hidden>restart</button></div>
65<script src=sprites.js></script>
64<script src=data.js></script> 66<script src=data.js></script>
65<script> 67<script>
66const SEl=document.getElementById("s"),U=document.getElementById("u"),G=document.getElementById("g"),I=document.getElementById("i"),L=document.getElementById("l"),X=document.getElementById("r"),D=document.getElementById("d") 68const SEl=document.getElementById("s"),U=document.getElementById("u"),G=document.getElementById("g"),I=document.getElementById("i"),L=document.getElementById("l"),X=document.getElementById("r"),D=document.getElementById("d")
@@ -970,6 +972,7 @@ function testFloor(){
970 972
971function key(e){ 973function key(e){
972 if(e.key=="m"){muted=!muted;render();return} 974 if(e.key=="m"){muted=!muted;render();return}
975 if(e.key=="g"){gfxToggle();render();return}
973 if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return} 976 if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return}
974 if(S.gameState=="title"){ 977 if(S.gameState=="title"){
975 if("12345678".includes(e.key)){startRun(MODE_IDS[+e.key-1],1);render();return} 978 if("12345678".includes(e.key)){startRun(MODE_IDS[+e.key-1],1);render();return}
@@ -1021,6 +1024,7 @@ function cls(x,y){
1021} 1024}
1022 1025
1023function render(){ 1026function render(){
1027 SEl.classList.toggle("gfx",!!GFX)
1024 U.className=S.gameState 1028 U.className=S.gameState
1025 SEl.dataset.state=S.gameState 1029 SEl.dataset.state=S.gameState
1026 SEl.dataset.floor=S.level||1 1030 SEl.dataset.floor=S.level||1
@@ -1028,7 +1032,7 @@ function render(){
1028 if(S.gameState=="title"){ 1032 if(S.gameState=="title"){
1029 delete G.dataset.intro 1033 delete G.dataset.intro
1030 U.innerHTML=`1MB ROGUELIKE<small>single-file campaign | minimalist descent</small>` 1034 U.innerHTML=`1MB ROGUELIKE<small>single-file campaign | minimalist descent</small>`
1031 I.textContent=`modes\n1 standard ${MODES.standard.desc}\n2 iron ${MODES.iron.desc}\n3 greed ${MODES.greed.desc}\n4 dark ${MODES.dark.desc}\n5 daily ${MODES.daily.desc}\n6 heavy ${MODES.heavy.desc}\n7 swift ${MODES.swift.desc}\n8 warded ${MODES.warded.desc}\n\nkeys\n1-8 begin\nr quick run${hasSave?"\nc continue saved run":""}\nm mute sound\n\nglyphs\n@ you B beast * relic\nY shrine & brazier t torch\n! crack ^ curse Q watcher\nj leech $ gold > gate` 1035 I.textContent=`modes\n1 standard ${MODES.standard.desc}\n2 iron ${MODES.iron.desc}\n3 greed ${MODES.greed.desc}\n4 dark ${MODES.dark.desc}\n5 daily ${MODES.daily.desc}\n6 heavy ${MODES.heavy.desc}\n7 swift ${MODES.swift.desc}\n8 warded ${MODES.warded.desc}\n\nkeys\n1-8 begin\nr quick run${hasSave?"\nc continue saved run":""}\nm mute sound\ng graphics / ascii\n\nglyphs\n@ you B beast * relic\nY shrine & brazier t torch\n! crack ^ curse Q watcher\nj leech $ gold > gate`
1032 L.textContent=`Pick a mode, then descend. Flame buys time; wards buy mistakes.${hasSave?" A saved run is waiting — press c to continue.":""}` 1036 L.textContent=`Pick a mode, then descend. Flame buys time; wards buy mistakes.${hasSave?" A saved run is waiting — press c to continue.":""}`
1033 X.textContent="start standard" 1037 X.textContent="start standard"
1034 X.onclick=()=>{startRun("standard",1);render()} 1038 X.onclick=()=>{startRun("standard",1);render()}
@@ -1079,7 +1083,8 @@ function render(){
1079 for(let y=0;y<H;y++)for(let x=0;x<W;x++){ 1083 for(let y=0;y<H;y++)for(let x=0;x<W;x++){
1080 const d=document.createElement("div") 1084 const d=document.createElement("div")
1081 d.className="t "+cls(x,y) 1085 d.className="t "+cls(x,y)
1082 d.textContent=draw(x,y) 1086 if(GFX)d.innerHTML=gfxCell(d.className,x,y,draw(x,y))
1087 else d.textContent=draw(x,y)
1083 G.appendChild(d) 1088 G.appendChild(d)
1084 } 1089 }
1085 SFX.play(S.fx) 1090 SFX.play(S.fx)
1mb/sprites.js added +73
@@ -0,0 +1,73 @@
1// Vector tile art. Symbols are 32x32 and pull colors from the floor theme's CSS variables.
2// Loaded after data.js; the engine calls gfxCell() per grid cell when graphics are on.
3const GFX_DEFS=`
4<linearGradient id=gF x1=0 y1=0 x2=0 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--floor) 62%,#46463e)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--floor) 65%,#2c2c27)"/></linearGradient>
5<linearGradient id=gW x1=0 y1=0 x2=0 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--wall) 92%,#fff)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--wall) 62%,#000)"/></linearGradient>
6<linearGradient id=gWF x1=0 y1=0 x2=0 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--wall) 55%,#000)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--wall) 22%,#000)"/></linearGradient>
7<radialGradient id=gPit r=.75><stop offset=0 style="stop-color:color-mix(in srgb,var(--pit) 32%,#000)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--pit) 14%,#000)"/></radialGradient>
8<linearGradient id=gStep x1=0 y1=0 x2=0 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--exit) 55%,#555)"/><stop offset=1 stop-color=#050403 /></linearGradient>
9<radialGradient id=gFl cx=.5 cy=.7 r=.6><stop offset=0 stop-color=#fffbe0 /><stop offset=.35 stop-color=#ffd35a /><stop offset=.75 style="stop-color:var(--torch)"/><stop offset=1 stop-color=#c2381a /></radialGradient>
10<radialGradient id=gGlow><stop offset=0 style="stop-color:var(--torch);stop-opacity:.55"/><stop offset=1 style="stop-color:var(--torch);stop-opacity:0"/></radialGradient>
11<linearGradient id=gG x1=0 y1=0 x2=0 y2=1><stop offset=0 stop-color=#fff3b0 /><stop offset=.45 stop-color=#e8b43c /><stop offset=1 stop-color=#7d4f12 /></linearGradient>
12<linearGradient id=gP x1=0 y1=0 x2=1 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--player) 80%,#fff)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--player) 45%,#000)"/></linearGradient>
13<radialGradient id=gB cx=.45 cy=.3 r=.8><stop offset=0 style="stop-color:color-mix(in srgb,var(--beast) 75%,#fff)"/><stop offset=.45 style="stop-color:color-mix(in srgb,var(--beast) 70%,#200)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--beast) 18%,#000)"/></radialGradient>
14<linearGradient id=gStone x1=0 y1=0 x2=0 y2=1><stop offset=0 stop-color=#b9b4a6 /><stop offset=1 stop-color=#4a463e /></linearGradient>
15<linearGradient id=gCry x1=0 y1=0 x2=1 y2=1><stop offset=0 stop-color=#fff /><stop offset=.4 style="stop-color:var(--shrine)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--shrine) 40%,#000)"/></linearGradient>
16<radialGradient id=gHalo><stop offset=0 style="stop-color:var(--shrine);stop-opacity:.6"/><stop offset=1 style="stop-color:var(--shrine);stop-opacity:0"/></radialGradient>
17<radialGradient id=gShade cx=.5 cy=.35 r=.65><stop offset=0 style="stop-color:color-mix(in srgb,var(--shade) 60%,#fff);stop-opacity:.95"/><stop offset=.7 style="stop-color:var(--shade);stop-opacity:.55"/><stop offset=1 style="stop-color:var(--shade);stop-opacity:0"/></radialGradient>
18<radialGradient id=gEye cx=.4 cy=.35 r=.7><stop offset=0 stop-color=#fff /><stop offset=.7 stop-color=#e8dccb /><stop offset=1 stop-color=#8d6f5e /></radialGradient>
19<radialGradient id=gIris><stop offset=0 style="stop-color:color-mix(in srgb,var(--watcher) 50%,#fff)"/><stop offset=.6 style="stop-color:var(--watcher)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--watcher) 35%,#000)"/></radialGradient>
20<linearGradient id=gL x1=0 y1=0 x2=0 y2=1><stop offset=0 style="stop-color:color-mix(in srgb,var(--leech) 75%,#fff)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--leech) 30%,#000)"/></linearGradient>
21<linearGradient id=gMask x1=0 y1=0 x2=1 y2=1><stop offset=0 stop-color=#fbf6ea /><stop offset=.6 stop-color=#cfc6b3 /><stop offset=1 stop-color=#7d7465 /></linearGradient>
22<linearGradient id=gWood x1=0 y1=0 x2=1 y2=0><stop offset=0 stop-color=#8a5b33 /><stop offset=1 stop-color=#3d2412 /></linearGradient>
23<linearGradient id=gIron x1=0 y1=0 x2=0 y2=1><stop offset=0 stop-color=#8b8f94 /><stop offset=1 stop-color=#23262a /></linearGradient>
24<radialGradient id=gHaz><stop offset=0 style="stop-color:var(--hazard);stop-opacity:.7"/><stop offset=1 style="stop-color:var(--hazard);stop-opacity:0"/></radialGradient>
25<radialGradient id=gRelic><stop offset=0 stop-color=#fff /><stop offset=.5 style="stop-color:var(--relic)"/><stop offset=1 style="stop-color:color-mix(in srgb,var(--relic) 30%,#600)"/></radialGradient>`
26const SHADOW=`<ellipse cx=16 cy=28.5 rx=8 ry=2.5 fill=#000 opacity=.45 />`
27const FLAME=(x,y,s)=>`<circle cx=${x} cy=${y} r=${7*s} fill=url(#gGlow) /><path transform="translate(${x} ${y}) scale(${s})" d="M0-6C3-3 4 0 3 2.5 2 4.5-2 4.5-3 2.5-4 0-2-2 0-6Z" fill=url(#gFl) /><path transform="translate(${x} ${y+1}) scale(${s})" d="M0-2.5C1.3-1 1.6.6 1 1.6.4 2.4-.4 2.4-1 1.6-1.6.6-1.2-1 0-2.5Z" fill=#fffbe6 />`
28const STONES=(rs)=>rs.map(([x,y,w,h])=>`<rect x=${x} y=${y} width=${w} height=${h} rx=1.5 fill=url(#gF) stroke=#0004 stroke-width=.5 /><path d="M${x+1.5} ${y+.8}H${x+w-1.5}" stroke=#ffffff14 stroke-width=.7 />`).join("")
29const FLOOR_BASE=`<rect width=32 height=32 style="fill:color-mix(in srgb,var(--floor) 70%,#16160f)"/>`
30const BRICKS=(y0,rows,grad)=>{let s="";for(let r=0;r<rows;r++){const y=y0+r*8,o=r%2?-6:0;for(let x=o;x<32;x+=12)s+=`<rect x=${x+.5} y=${y+.5} width=11 height=7 rx=1 fill=url(#${grad}) stroke=#0007 stroke-width=.6 /><path d="M${x+1.5} ${y+1.3}H${x+10.5}" stroke=#fff3 stroke-width=.7 />`}return s}
31const SPRITES={
32 f0:FLOOR_BASE+STONES([[1,1,14,9],[17,1,14,9],[1,12,9,8],[12,12,19,8],[1,22,18,9],[21,22,10,9]]),
33 f1:FLOOR_BASE+STONES([[1,1,10,12],[13,1,18,6],[13,9,9,11],[24,9,7,11],[1,15,10,16],[13,22,18,9]]),
34 f2:FLOOR_BASE+STONES([[1,1,19,8],[22,1,9,14],[1,11,9,9],[12,11,8,9],[22,17,9,14],[1,22,19,9]])+`<circle cx=11 cy=21 r=.9 fill=#0006 /><path d="M4 26l3 1 2-1.5" stroke=#0008 stroke-width=.6 fill=none />`,
35 w:`<rect width=32 height=32 style="fill:color-mix(in srgb,var(--wall) 30%,#000)"/>`+BRICKS(0,4,"gW"),
36 wf:`<rect width=32 height=32 style="fill:color-mix(in srgb,var(--wall) 20%,#000)"/>`+BRICKS(0,1,"gW")+BRICKS(8,3,"gWF")+`<rect y=8 width=32 height=2 fill=#0006 /><rect y=29 width=32 height=3 fill=#000 opacity=.55 />`,
37 o:`<rect width=32 height=32 fill=url(#gPit) /><path d="M0 3Q8 1 16 3T32 2" stroke=#000 stroke-width=3 opacity=.5 fill=none /><circle cx=11 cy=14 r=.8 style="fill:var(--pit)" opacity=.9 /><circle cx=21 cy=19 r=.6 fill=#ffb070 opacity=.8 /><circle cx=17 cy=10 r=.5 style="fill:var(--pit)" opacity=.7 />`,
38 i:`<rect x=4 y=3 width=24 height=26 rx=2 fill=#060504 style="stroke:var(--exit)" stroke-width=1.4 /><rect x=6 y=7 width=20 height=5 fill=url(#gStep) /><rect x=8 y=13 width=16 height=5 fill=url(#gStep) opacity=.8 /><rect x=10 y=19 width=12 height=4 fill=url(#gStep) opacity=.55 /><rect x=12 y=24 width=8 height=3 fill=url(#gStep) opacity=.3 /><path d="M5 4H27" style="stroke:var(--exit)" stroke-width=1 opacity=.8 /><rect x=4 y=3 width=24 height=26 rx=2 fill=none style="stroke:var(--exit)" stroke-width=3 opacity=.18 />`,
39 p:SHADOW+`<path d="M22 20L26.5 13" stroke=url(#gWood) stroke-width=2 stroke-linecap=round />`+FLAME(26.8,11,.75)+`<path d="M9.5 28.5Q8.5 19 12 15H20Q23.5 19 22.5 28.5Z" fill=url(#gP) stroke=#0008 stroke-width=.7 /><path d="M16 15V28" stroke=#0004 stroke-width=.8 /><rect x=11 y=21 width=10 height=1.6 fill=#3a2716 /><rect x=15 y=20.7 width=2 height=2.2 fill=#d8b35a /><path d="M10 12.5Q10 4.5 16 4.5T22 12.5L20.5 16H11.5Z" fill=url(#gP) stroke=#0008 stroke-width=.7 /><ellipse cx=16 cy=11.5 rx=4 ry=4.2 fill=#e3b98f /><path d="M12 10Q16 5.5 20 10" style="fill:color-mix(in srgb,var(--player) 40%,#000)"/><rect x=13.8 y=11.2 width=1.3 height=1.8 rx=.5 fill=#1b1410 /><rect x=17 y=11.2 width=1.3 height=1.8 rx=.5 fill=#1b1410 /><path d="M22.5 20.5L20 22" stroke=#e3b98f stroke-width=2 stroke-linecap=round />`,
40 B:SHADOW+`<path d="M11 11Q7 7 6 2Q10 5 13 9Z" fill=#dcd2bb stroke=#0008 stroke-width=.6 /><path d="M21 11Q25 7 26 2Q22 5 19 9Z" fill=#dcd2bb stroke=#0008 stroke-width=.6 /><path d="M4.5 28.5C3 20 7 9.5 16 9.5S29 20 27.5 28.5Z" fill=url(#gB) stroke=#000a stroke-width=.8 /><path d="M8 18Q11 16 13 19M24 18Q21 16 19 19" stroke=#0006 stroke-width=.8 fill=none /><path d="M9.5 14.5L14 16.5 13 17.8 9.5 16Z" style="fill:var(--eye,#ffe066)"/><path d="M22.5 14.5L18 16.5 19 17.8 22.5 16Z" style="fill:var(--eye,#ffe066)"/><circle cx=11.8 cy=16.2 r=3 style="fill:var(--eye,#ffe066)" opacity=.25 /><circle cx=20.2 cy=16.2 r=3 style="fill:var(--eye,#ffe066)" opacity=.25 /><path d="M10.5 22H21.5L20 25.5H12Z" fill=#1a0508 /><path d="M11 22L12.2 24 13.4 22 14.6 24 15.8 22 17 24 18.2 22 19.4 24 20.6 22" fill=#f4ecd8 /><path d="M5 28.5l1.5-2 1 2 1.5-2 1 2M22 28.5l1.5-2 1 2 1.5-2 1 2" stroke=#e8dfc8 stroke-width=.8 fill=none />`,
41 S:`<circle cx=16 cy=13 r=13 fill=url(#gHalo) /><path d="M9 29L11 21H21L23 29Z" fill=url(#gStone) stroke=#0008 stroke-width=.6 /><rect x=10 y=19 width=12 height=2.5 rx=.5 fill=#a7a193 stroke=#0008 stroke-width=.5 /><path d="M16 3L21 11 16 19 11 11Z" fill=url(#gCry) stroke=#0006 stroke-width=.5 /><path d="M16 3L16 19M11 11H21" stroke=#fff5 stroke-width=.5 /><path d="M13.5 8L15 6" stroke=#fff stroke-width=.9 stroke-linecap=round />`,
42 r:`<circle cx=16 cy=17 r=11 fill=url(#gGlow) opacity=.7 /><path d="M9 6Q16 14 23 6" stroke=url(#gG) stroke-width=1.2 fill=none stroke-dasharray="1.5 .8" /><circle cx=16 cy=18 r=7.5 fill=url(#gG) stroke=#5a360b stroke-width=.8 /><circle cx=16 cy=18 r=5.5 fill=none stroke=#fff4 stroke-width=.6 stroke-dasharray="1 1.2" /><circle cx=16 cy=18 r=3.6 fill=url(#gRelic) stroke=#4a2a08 stroke-width=.6 /><circle cx=14.8 cy=16.8 r=1 fill=#fff opacity=.9 /><path d="M25 7l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6Z" fill=#fff />`,
43 x:`<circle cx=16 cy=16 r=13 fill=url(#gHaz) opacity=.6 /><path d="M4 7L10 12 8 16 14 18 13 23 19 26 22 30M14 18L20 15 24 17 28 12M10 12L7 11" stroke=#000 stroke-width=3.2 fill=none stroke-linejoin=round /><path d="M4 7L10 12 8 16 14 18 13 23 19 26 22 30M14 18L20 15 24 17 28 12" style="stroke:var(--hazard)" stroke-width=1 fill=none stroke-linejoin=round />`,
44 z:`<circle cx=16 cy=16 r=13 fill=url(#gHaz) opacity=.5 /><circle cx=16 cy=16 r=11 fill=none style="stroke:var(--hazard)" stroke-width=1 opacity=.85 /><circle cx=16 cy=16 r=8.5 fill=none style="stroke:var(--hazard)" stroke-width=.5 stroke-dasharray="2 1.5" /><path d="M16 7.5L23.4 20.3H8.6Z" fill=none style="stroke:var(--hazard)" stroke-width=1.2 /><path d="M16 12V17M16 18.5V19.5" style="stroke:var(--hazard)" stroke-width=1.4 stroke-linecap=round />`,
45 n:`<ellipse cx=16 cy=29 rx=6 ry=1.6 style="fill:var(--shade)" opacity=.2 /><path d="M7 26Q7 5 16 5T25 26Q23 23.5 21.5 26.5 19.5 23.5 17.5 27 16 24 14 27 12 23.5 10.5 26.5 9 23.5 7 26Z" fill=url(#gShade) /><ellipse cx=12.8 cy=13.5 rx=1.8 ry=2.6 fill=#0a0f1e /><ellipse cx=19.2 cy=13.5 rx=1.8 ry=2.6 fill=#0a0f1e /><ellipse cx=16 cy=19.5 rx=1.6 ry=2.2 fill=#0a0f1e opacity=.8 />`,
46 q:`<ellipse cx=16 cy=29 rx=6 ry=1.8 fill=#000 opacity=.4 /><path d="M3 15Q16 1 29 15Q16 29 3 15Z" fill=url(#gEye) stroke=#2a1810 stroke-width=1 /><path d="M7 11.5Q10 13 11 15M25 11.5Q22 13 21 15M8 19Q11 17 12 16" stroke=#c0392b stroke-width=.4 fill=none opacity=.7 /><circle cx=16 cy=15 r=6.2 fill=url(#gIris) stroke=#1a0f08 stroke-width=.6 /><ellipse cx=16 cy=15 rx=1.4 ry=4.6 fill=#050303 /><circle cx=13.7 cy=12.6 r=1.4 fill=#fff opacity=.9 /><path d="M3 15Q16 1 29 15" fill=none stroke=#0006 stroke-width=2.5 />`,
47 l:`<ellipse cx=16 cy=26 rx=11 ry=2.4 fill=#000 opacity=.4 /><path d="M4 22Q3 16 8 16Q10 13 14 14Q17 11 21 13Q26 12 28 17Q30 22 26 24H7Q4 24 4 22Z" fill=url(#gL) stroke=#0009 stroke-width=.7 /><path d="M10 16.5Q11 20 10 24M15 14.5Q16.5 19 15.5 24M21 13.5Q22 18.5 21 24" stroke=#0005 stroke-width=.8 fill=none /><path d="M9 17Q12 15 14 15.5M18 14Q21 13 24 14.5" stroke=#fff6 stroke-width=.8 fill=none stroke-linecap=round /><circle cx=26 cy=19.5 r=3.2 fill=#3a0e12 stroke=#0009 stroke-width=.5 /><path d="M24 18l.8 1.2.8-1.3.8 1.3.8-1.2M24 21l.8-1 .8 1.1.8-1.1.8 1" stroke=#f2e6cf stroke-width=.5 fill=none />`,
48 g:`<ellipse cx=16 cy=27 rx=10 ry=2.2 fill=#000 opacity=.45 /><ellipse cx=10 cy=24 rx=5 ry=2 fill=#7d4f12 /><ellipse cx=10 cy=23 rx=5 ry=2 fill=url(#gG) stroke=#5a360b stroke-width=.5 /><ellipse cx=21 cy=24.5 rx=5 ry=2 fill=#7d4f12 /><ellipse cx=21 cy=23.5 rx=5 ry=2 fill=url(#gG) stroke=#5a360b stroke-width=.5 />`+[20,17,14,11].map(y=>`<ellipse cx=15.5 cy=${y+1} rx=5 ry=2 fill=#7d4f12 /><ellipse cx=15.5 cy=${y} rx=5 ry=2 fill=url(#gG) stroke=#5a360b stroke-width=.5 />`).join("")+`<ellipse cx=15.5 cy=11 rx=2.8 ry=.9 fill=none stroke=#fff7 stroke-width=.5 /><path d="M25 8l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5Z" fill=#fff />`,
49 I:`<circle cx=16 cy=12 r=12 fill=url(#gGlow) opacity=.5 /><path d="M8 29H24L23 25H9Z" fill=url(#gStone) stroke=#0008 stroke-width=.6 /><path d="M11 25Q10 17 13 14H19Q22 17 21 25Z" fill=url(#gG) stroke=#5a360b stroke-width=.6 /><path d="M11.5 18Q8 17 8.5 13M20.5 18Q24 17 23.5 13" stroke=url(#gG) stroke-width=1.8 fill=none stroke-linecap=round /><circle cx=16 cy=9.5 r=5 fill=url(#gG) stroke=#5a360b stroke-width=.6 /><path d="M11 7L12.5 3 14.5 6 16 2.5 17.5 6 19.5 3 21 7" fill=url(#gG) stroke=#5a360b stroke-width=.5 /><circle cx=14.2 cy=9.8 r=.9 fill=#c4122a /><circle cx=17.8 cy=9.8 r=.9 fill=#c4122a /><path d="M14 20H18" stroke=#5a360b stroke-width=.6 />`,
50 M:SHADOW+`<path d="M16 4C9 4 7.5 9 7.5 14 7.5 21 11.5 27 16 27S24.5 21 24.5 14C24.5 9 23 4 16 4Z" fill=url(#gMask) stroke=#3a342b stroke-width=.8 /><path d="M10 13.5Q12 11.5 14.2 13.5 12 15.3 10 13.5ZM22 13.5Q20 11.5 17.8 13.5 20 15.3 22 13.5Z" fill=#0b0907 /><path d="M16 14.5V19.5L14.8 20.5M13 23Q16 24.5 19 23" stroke=#5e564a stroke-width=.8 fill=none stroke-linecap=round /><path d="M9.5 9Q11 5.5 15 5" stroke=#fff stroke-width=1 fill=none opacity=.7 stroke-linecap=round /><path d="M7.5 13L4 12M24.5 13L28 12" stroke=#7a2e2e stroke-width=1 />`,
51 T:`<ellipse cx=15 cy=27 rx=7 ry=1.8 fill=#000 opacity=.4 /><path d="M9 27L19 12" stroke=url(#gWood) stroke-width=3 stroke-linecap=round /><path d="M17 15.5L20.5 10.5M16 14.5L19.5 9.5" stroke=#b99b6c stroke-width=1.2 />`+FLAME(20.5,8,1),
52 F:`<circle cx=16 cy=12 r=14 fill=url(#gGlow) /><path d="M10 28L12.5 20M22 28L19.5 20M16 29V21" stroke=url(#gIron) stroke-width=1.8 stroke-linecap=round /><path d="M6 15H26Q25 22 16 22T6 15Z" fill=url(#gIron) stroke=#0009 stroke-width=.7 /><ellipse cx=16 cy=15 rx=10 ry=2.2 fill=#1a0c05 stroke=#6a6e73 stroke-width=.7 /><ellipse cx=16 cy=15 rx=8 ry=1.4 fill=#ff7a2a opacity=.8 />`+FLAME(12.5,11,.9)+FLAME(19.5,11.5,.85)+FLAME(16,9,1.25),
53 C:`<circle cx=16 cy=16 r=12 fill=url(#gGlow) opacity=.6 />${SHADOW}<path d="M6 24L5 10 11 15.5 16 7 21 15.5 27 10 26 24Z" fill=url(#gG) stroke=#5a360b stroke-width=.8 stroke-linejoin=round /><rect x=6 y=22 width=20 height=4 rx=1 fill=url(#gG) stroke=#5a360b stroke-width=.7 /><circle cx=5 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=16 cy=6.5 r=1.5 fill=#fff1b8 /><circle cx=27 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=11 cy=24 r=1.2 fill=#2e6bd6 /><circle cx=16 cy=24 r=1.4 fill=#c4122a /><circle cx=21 cy=24 r=1.2 fill=#1e9e5a /><path d="M8 13L8.6 20" stroke=#fff8 stroke-width=.8 />`,
54 D:`<path d="M5 30V13Q5 3 16 3T27 13V30Z" fill=url(#gStone) stroke=#0009 stroke-width=.8 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=#050403 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=none style="stroke:var(--exit)" stroke-width=.8 opacity=.7 /><path d="M10 30H22" style="stroke:var(--exit)" stroke-width=2 opacity=.35 /><path d="M7 12Q8 6 13 4.5" stroke=#fff5 stroke-width=.8 fill=none />`,
55 K:`<circle cx=16 cy=16 r=11 fill=url(#gGlow) opacity=.5 /><circle cx=10 cy=12 r=5.5 fill=none stroke=#5a360b stroke-width=3.6 /><circle cx=10 cy=12 r=5.5 fill=none stroke=url(#gG) stroke-width=2.4 /><circle cx=10 cy=12 r=1.6 fill=url(#gG) /><path d="M14 16L25.5 27.5" stroke=#5a360b stroke-width=3.4 stroke-linecap=round /><path d="M14 16L25.5 27.5" stroke=url(#gG) stroke-width=2 stroke-linecap=round /><path d="M21.5 23.5L19 26M24 26L22 28" stroke=url(#gG) stroke-width=2.2 stroke-linecap=round /><path d="M7 9.5Q8.5 8 10.5 8" stroke=#fff stroke-width=.8 fill=none opacity=.8 />`
56}
57// Grid-cell kinds from cls() that sit on top of a floor tile.
58const GFX_ON_FLOOR="pBiSrxznqlgIMTFCK"
59let GFX=1
60try{GFX=localStorage.getItem("mrl_gfx")!=="0"}catch(e){}
61document.getElementById("s").insertAdjacentHTML("afterbegin",`<svg width=0 height=0 style=position:absolute aria-hidden=true><defs>${GFX_DEFS}${Object.entries(SPRITES).map(([k,v])=>`<symbol id=s-${k} viewBox="0 0 32 32">${v}</symbol>`).join("")}</defs></svg>`)
62function gfxToggle(){GFX=!GFX;try{localStorage.setItem("mrl_gfx",GFX?"1":"0")}catch(e){}}
63function gfxCell(c,x,y,glyph){
64 const k=c.split(" ")[1]
65 if(!k||k=="u")return ""
66 const use=id=>`<svg viewBox="0 0 32 32"><use href="#s-${id}"/></svg>`
67 if(k=="w")return use(y==H-1||tile(x,y+1)=="#"?"w":"wf")
68 if(k=="o")return use("o")
69 const fl=use("f"+((x*7+y*13)%3))
70 if(k=="f")return fl
71 if(k!="i"&&R[S.r]&&k in R[S.r].ex)return fl+use("D")+`<b class=dl>${glyph}</b>`
72 return fl+(GFX_ON_FLOOR.includes(k)?use(k):`<b>${glyph}</b>`)
73}
README.org +2 −1
@@ -17,6 +17,7 @@ controls:
17- space or . wait a turn 17- space or . wait a turn
18- t drop torch 18- t drop torch
19- m mute / unmute 19- m mute / unmute
20- g graphics / ascii tiles
20- r restart 21- r restart
21- c continue a saved run (title screen) 22- c continue a saved run (title screen)
22#+end_example 23#+end_example
@@ -31,7 +32,7 @@ modes (press a number on the title screen):
31 32
32win once and a hidden seventh floor opens. 33win once and a hidden seventh floor opens.
33 34
34budget: the game is the two raw files in 1mb/, no build step, and 35budget: the game is the raw files in 1mb/, no build step, and
35together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is 36together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is
36the gate; it prints the byte ledger and proves every escape room 37the gate; it prints the byte ledger and proves every escape room
37stays winnable across hundreds of seeds. ledger history in 38stays winnable across hundreds of seeds. ledger history in
tools/validate.js +5
@@ -17,6 +17,7 @@ const gameDir = process.argv[3] || path.join(__dirname, "..", "1mb")
17 17
18const html = fs.readFileSync(path.join(gameDir, "index.html"), "utf8") 18const html = fs.readFileSync(path.join(gameDir, "index.html"), "utf8")
19const dataSrc = fs.readFileSync(path.join(gameDir, "data.js"), "utf8") 19const dataSrc = fs.readFileSync(path.join(gameDir, "data.js"), "utf8")
20const spriteSrc = fs.readFileSync(path.join(gameDir, "sprites.js"), "utf8")
20 21
21// The engine is the inline <script> block that follows the data.js include. 22// The engine is the inline <script> block that follows the data.js include.
22const m = html.match(/<script src=data\.js><\/script>\s*<script>([\s\S]*?)<\/script>/) 23const m = html.match(/<script src=data\.js><\/script>\s*<script>([\s\S]*?)<\/script>/)
@@ -36,6 +37,8 @@ function el() {
36 hidden: false, 37 hidden: false,
37 appendChild() {}, 38 appendChild() {},
38 onclick: null, 39 onclick: null,
40 classList: { toggle() {} },
41 insertAdjacentHTML() {},
39 } 42 }
40} 43}
41 44
@@ -57,6 +60,7 @@ const sandbox = {
57const ctx = vm.createContext(sandbox) 60const ctx = vm.createContext(sandbox)
58 61
59try { 62try {
63 vm.runInContext(spriteSrc, ctx, { filename: "1mb/sprites.js" })
60 vm.runInContext(dataSrc, ctx, { filename: "1mb/data.js" }) 64 vm.runInContext(dataSrc, ctx, { filename: "1mb/data.js" })
61 vm.runInContext(engineSrc, ctx, { filename: "1mb/index.html#engine" }) 65 vm.runInContext(engineSrc, ctx, { filename: "1mb/index.html#engine" })
62} catch (e) { 66} catch (e) {
@@ -102,3 +106,4 @@ if (fails.length) {
102 process.exit(1) 106 process.exit(1)
103} 107}
104console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`) 108console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`)
109