summaryrefslogtreecommitdiff
path: root/1mb/index.html
diff options
context:
space:
mode:
Diffstat (limited to '1mb/index.html')
-rw-r--r--1mb/index.html227
1 files changed, 188 insertions, 39 deletions
diff --git a/1mb/index.html b/1mb/index.html
index 32cc57c..4668483 100644
--- a/1mb/index.html
+++ b/1mb/index.html
@@ -131,13 +131,90 @@ function campOffers(next){
return [a,b]
}
+function genRoom(rand,spec){
+ const pick=n=>(rand()*n)|0,lo=spec.ring?2:1,hi=spec.ring?W-3:W-2
+ const bad=[...(spec.keep||[]),...(spec.blocked||[])]
+ const openAt=(g,x,y)=>g[y][x]=="."&&!bad.some(b=>b[0]==x&&b[1]==y)
+ const spots=g=>{const c=[];for(let y=lo;y<=hi;y++)for(let x=lo;x<=hi;x++)if(openAt(g,x,y))c.push([x,y]);return c}
+ for(let t=0;t<25;t++){
+ const sparse=t==24
+ const g=Array.from({length:H},(_,y)=>Array.from({length:W},(_,x)=>!x||!y||x==W-1||y==H-1?"#":"."))
+ if(spec.ring)for(let i=1;i<W-1;i++)g[1][i]=g[H-2][i]=g[i][1]=g[i][W-2]="O"
+ if(!sparse){
+ for(let w=spec.walls||0;w>0;){
+ const hz=pick(2),len=2+pick(3)
+ const x=lo+pick(hi-lo+1-(hz?len:0)),y=lo+pick(hi-lo+1-(hz?0:len))
+ for(let i=0;i<len;i++)if(openAt(g,hz?x+i:x,hz?y:y+i))g[hz?y:y+i][hz?x+i:x]="#"
+ w-=len
+ }
+ let px=lo+pick(hi-lo+1),py=lo+pick(hi-lo+1)
+ for(let p=spec.pits||0;p>0;p--){
+ if(openAt(g,px,py))g[py][px]="O"
+ if(pick(5)){const d=[[1,0],[-1,0],[0,1],[0,-1]][pick(4)]
+ px=Math.min(hi,Math.max(lo,px+d[0]));py=Math.min(hi,Math.max(lo,py+d[1]))
+ }else{px=lo+pick(hi-lo+1);py=lo+pick(hi-lo+1)}
+ }
+ }
+ if(spec.stamp){
+ const sh=spec.stamp.length,sw=spec.stamp[0].length
+ let put=0
+ for(let s=0;s<20&&!put;s++){
+ const sx=lo+pick(hi-lo+2-sw),sy=lo+pick(hi-lo+2-sh)
+ if(bad.some(b=>b[0]>=sx&&b[0]<sx+sw&&b[1]>=sy&&b[1]<sy+sh))continue
+ for(let y=0;y<sh;y++)for(let x=0;x<sw;x++)g[sy+y][sx+x]=spec.stamp[y][x]
+ put=1
+ }
+ if(!put)continue
+ }
+ let full=0
+ for(const kind of [sparse?0:spec.traps,spec.items])if(kind)for(const ch in kind)for(let n=kind[ch];n>0;n--){
+ const c=spots(g)
+ if(!c.length){full=1;break}
+ const [x,y]=c[pick(c.length)];g[y][x]=ch
+ }
+ for(const ch in spec.exits){
+ const at=spec.exits[ch]
+ if(at=="any"){
+ const c=spots(g)
+ if(!c.length){full=1;break}
+ const [x,y]=c[pick(c.length)];g[y][x]=ch
+ }else g[at[1]][at[0]]=ch
+ }
+ if(full)continue
+ for(const [x,y] of spec.keep||[])if(g[y][x]=="#"||g[y][x]=="O")g[y][x]="."
+ const solid=(x,y)=>g[y][x]=="#"||g[y][x]=="O"||(spec.blocked||[]).some(b=>b[0]==x&&b[1]==y)
+ let total=0,start=null
+ for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(!solid(x,y)){total++;start=start||[x,y]}
+ const seen=new Set([start.join()]),q=[start]
+ while(q.length){
+ const [x,y]=q.pop()
+ for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
+ const nx=x+dx,ny=y+dy,k=nx+","+ny
+ if(nx<0||ny<0||nx>=W||ny>=H||solid(nx,ny)||seen.has(k))continue
+ seen.add(k);q.push([nx,ny])
+ }
+ }
+ if(seen.size==total)return g.map(row=>row.join(""))
+ }
+}
+function procSpec(kind,level,code){
+ const spec={...PROC_SPECS[kind]},keep=[(P[code]||P[code[0]]).slice()],blocked=[]
+ for(const s of BLOCK_SPOTS[level]||[])if(s[0]==code)blocked.push([s[1],s[2]])
+ for(const T of [RELIC_SPOTS,EFFIGY_SPOTS,BRAZIER_SPOTS,SHRINE_SPOTS,CURSE_SPOTS,SHADE_SPOTS,WATCHER_SPOTS,LEECH_SPOTS])
+ for(const s of T[level]||[])if(s[0]==code)keep.push([s[1],s[2]])
+ if(kind=="escapes")for(const b of LEVELS[level].beasts)keep.push(b.slice())
+ spec.keep=keep;spec.blocked=blocked
+ return spec
+}
+
function build(seed0){
- const L=LEVELS[S&&S.level||1],r=rng(seed0+((S&&S.level||1)-1)*9973)
- const B=BASES[S&&S.level||1]
- V={hall:(r()*L.halls.length)|0,pit:(r()*L.pits.length)|0,idol:(r()*L.idols.length)|0,crack:(r()*L.cracks.length)|0,escape:(r()*L.escapes.length)|0,beast:(r()*L.beasts.length)|0}
+ const level=S&&S.level||1
+ const L=LEVELS[level],r=rng(seed0+(level-1)*9973)
+ const B=BASES[level]
+ const slots=k=>level<2||(k=="escapes"&&level>=6)?0:PROC_SLOTS
+ V={hall:(r()*(L.halls.length+slots("halls")))|0,pit:(r()*(L.pits.length+slots("pits")))|0,idol:(r()*(L.idols.length+slots("idols")))|0,crack:(r()*(L.cracks.length+slots("cracks")))|0,escape:(r()*(L.escapes.length+slots("escapes")))|0,beast:(r()*L.beasts.length)|0}
B0=L.beasts[V.beast]
const relicBag=RELIC_IDS.slice().sort(()=>r()-.5)
- const level=S&&S.level||1
O={relics:pickSpots(r,RELIC_SPOTS[level]||[],(level>=5?RELIC_SLOTS_HIGH:level>=3?RELIC_SLOTS_MID:RELIC_SLOTS_LOW)+(S&&S.mods&&S.mods.includes("rich")?RELIC_SLOTS_RICH_BONUS:0)).map((v,i)=>({r:v[0],x:v[1],y:v[2],id:relicBag[i%relicBag.length],on:1})),
braziers:pickSpots(r,BRAZIER_SPOTS[level]||[],level>=5?3:level>=3?2:1).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
shrines:pickSpots(r,SHRINE_SPOTS[level]||[],level>=4?3:level>=3?2:1).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
@@ -147,13 +224,19 @@ function build(seed0){
leeches:pickSpots(r,LEECH_SPOTS[level]||[],Math.max(0,level-2)).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
blocks:pickSpots(r,BLOCK_SPOTS[level]||[],99).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
effigies:pickSpots(r,EFFIGY_SPOTS[level]||[],Math.max(0,level-1)).map(v=>({r:v[0],x:v[1],y:v[2],on:1}))}
- R={
- g:B.g,b:B.b,v:B.v,d:B.d,
- h:{m:L.halls[V.hall],ex:{a:"p",b:"i",c:"c",d:"d",">":"g"}},
- p:{m:L.pits[V.pit],ex:{">":"v"}},
- i:{m:L.idols[V.idol],ex:{">":"h"}},
- c:{m:L.cracks[V.crack],ex:{">":"v"}},
- e:{m:L.escapes[V.escape],ex:{">":"g"}}
+ R={}
+ for(const code in GRAPH[level]){
+ const spec=GRAPH[level][code],arr=L[spec.arr],vi=V[spec.v]
+ const m=spec.base?B[spec.base]:vi<arr.length?arr[vi]:genRoom(r,procSpec(spec.arr,level,code))||arr[vi%arr.length]
+ R[code]={m,ex:{...spec.ex}}
+ }
+ if(R.e&&V.escape>=L.escapes.length){
+ let ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
+ for(let i=0;i<6&&!ok;i++){
+ R.e.m=genRoom(r,procSpec("escapes",level,"e"))||L.escapes[V.escape%L.escapes.length]
+ ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
+ }
+ if(!ok)R.e.m=L.escapes[V.escape%L.escapes.length]
}
}
@@ -341,8 +424,8 @@ function tile(x,y,r=S.r){
if(overAt(r,x,y,"watchers"))return "q"
if(overAt(r,x,y,"leeches"))return "l"
if(overAt(r,x,y,"curses"))return "z"
- if(r=="b"&&S.flags.mask&&x==4&&y==2)return "."
- if(r=="b"&&S.flags.torch&&x==5&&y==2)return "."
+ if(r[0]=="b"&&S.flags.mask&&x==4&&y==2)return "."
+ if(r[0]=="b"&&S.flags.torch&&x==5&&y==2)return "."
return base(x,y,r)
}
function setMsg(msg){S.msg=msg}
@@ -350,7 +433,7 @@ function die(msg,cause=msg){S.alive=0;S.gameState="dead";S.deathCause=cause;S.en
function win(){S.win=1;S.gameState="win";S.endTitle="dawn";S.endLead="You escape alive.";S.fx="win";setMsg("Dawn spills through the gate. You escape alive.");S.end=summary();clearSave();hasSave=!1}
function sign(n){return n<0?-1:n>0?1:0}
function dist(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
-function dark(r=S.r){return LEVELS[S.level].dark.includes(r)}
+function dark(r=S.r){return LEVELS[S.level].dark.includes(r[0])}
function carriedFire(){return S.flags.torch&&!S.flags.mask}
function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)}
@@ -378,6 +461,7 @@ function runStyle(){
}
function roomMsg(r=S.r){
+ r=r[0]
if(r=="g")return S.flags.crown&&S.flags.key?(S.level==1?"The gate shudders instead of opening. Something deeper calls you on.":S.level<5?"The gate yields, but the campaign is not done with you yet.":S.level==5?"The gate opens onto the final road.": "The last gate waits. Only what survives the shrines goes home."):(S.level==1?"The broken gate yawns behind you. Bones left, beast right, ruin below.":S.level==2?"The gate is still shut. The deeper ruin waits below.":S.level==3?"The gate is quiet, but the shrines are not.":S.level==4?"The hunt floor waits below the gate, listening.":S.level==5?"The hollow floor waits below the gate, hungry for what remains.":"The final gate is quiet. Shrines and hunger ring it like judges.")
if(r=="b")return S.flags.torch?(S.level==1?"The bone room is dim now, only soot and old footprints.":S.level==2?"The bones are freshened by newer ash. Even the dead have been disturbed.":"The bones are carved with ward-scratches. Someone learned too late."):(S.level==1?"A torch and a funeral mask wait among the bones.":S.level==2?"A torch and a funeral mask wait among the bones, as if placed for your return.":"A torch, a mask, and an old shrine wait among the bones.")
if(r=="e"){
@@ -496,7 +580,7 @@ function variantSummary(){
}
function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){
- const map=LEVELS[level].escapes[escapeIdx],start=P.e,beast=LEVELS[level].beasts[beastIdx],q=[[{x:start[0],y:start[1],bx:beast[0],by:beast[1],torch,chase:0,ward:level==3?8:0},""]],seen=new Set()
+ const map=LEVELS[level].escapes[escapeIdx]||R.e.m,start=P.e,beast=LEVELS[level].beasts[beastIdx],q=[[{x:start[0],y:start[1],bx:beast[0],by:beast[1],torch,chase:0,ward:level==3?8:0},""]],seen=new Set()
while(q.length){
const [s,path]=q.shift(),k=[s.x,s.y,s.bx,s.by,s.torch,s.chase,s.ward].join()
if(seen.has(k))continue
@@ -531,18 +615,70 @@ function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){
return ""
}
+function floorSolve(seed0,level=S&&S.level||1){
+ const kS=S,kV=V,kR=R,kO=O,kB=B0
+ S={level,mods:kS&&kS.mods||pickMods(seed0)}
+ build(seed0)
+ const solidAt=(rm,x,y)=>{const t=R[rm].m[y][x];return t=="#"||t=="O"||!!overAt(rm,x,y,"blocks")}
+ const find=(rm,ch)=>{for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(R[rm].m[y][x]==ch)return [x,y];return null}
+ const exitTo=(rm,dest)=>{for(const ch in R[rm].ex)if(R[rm].ex[ch].replace("!","")==dest)return find(rm,ch);return null}
+ const cost=(rm,from,...tos)=>{
+ let total=0,at=from
+ for(const to of tos){
+ if(!to)return -1
+ let d=-1
+ const seen=new Set([at.join()]),q=[[at[0],at[1],0]]
+ while(q.length){
+ const [x,y,n]=q.shift()
+ if(x==to[0]&&y==to[1]){d=n;break}
+ for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
+ const nx=x+dx,ny=y+dy,k=nx+","+ny
+ if(nx<0||ny<0||nx>=W||ny>=H||seen.has(k)||solidAt(rm,nx,ny))continue
+ seen.add(k);q.push([nx,ny,n+1])
+ }
+ }
+ if(d<0)return -1
+ total+=d;at=to
+ }
+ return total
+ }
+ const sp=rm=>(P[rm]||P[rm[0]]).slice()
+ const out=(()=>{
+ if(!R.b||!R.h||!R.i||!R.v||!R.e)return ""
+ let total=0
+ for(const c of [
+ cost("g",sp("g"),exitTo("g","b")),
+ cost("b",sp("b"),find("b","T"),exitTo("b","h")),
+ cost("h",sp("h"),exitTo("h","i")),
+ cost("i",sp("i"),find("i","I"),exitTo("i","h"))
+ ]){if(c<0)return "";total+=c}
+ const via=r2=>{
+ const a=cost("h",sp("h"),exitTo("h",r2))
+ if(a<0)return -1
+ const b=cost(r2,sp(r2),exitTo(r2,"v"))
+ return b<0?-1:a+b
+ }
+ const vp=via("p"),vc=via("c")
+ if(vp<0&&vc<0)return ""
+ total+=vp<0?vc:vc<0?vp:Math.min(vp,vc)
+ const vault=cost("v",sp("v"),find("v","C"),find("v","K"),exitTo("v","e"))
+ if(vault<0)return ""
+ total+=vault
+ // floor 1 crosses e with fresh-run fuel plus a brazier refuel: 8 is fair
+ const esc=escapeSolve(level,V.escape,V.beast,level==2?6:8)
+ if(!esc)return ""
+ return "ok:"+(total+esc.length)
+ })()
+ S=kS;V=kV;R=kR;O=kO;B0=kB
+ return out
+}
+
function validateSeeds(n=24){
- const keep=S
let bad=0,msg=[]
- for(const level of [2,3,4,5,6])for(let i=0;i<n;i++){
- const t={level,mods:keep.mods||pickMods(seed+i)}
- S=t;build(seed+i)
- const path=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
- if(!path){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)} e${V.escape} b${V.beast}`)}
+ for(const level of [1,2,3,4,5,6,7])for(let i=0;i<n;i++){
+ if(!floorSolve(seed+i,level)){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)}`)}
}
- S=keep
- build(seed)
- return `check ${n} seeds | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}`
+ return `check ${n} seeds x floors 1-7 | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}`
}
function dropTorch(){
@@ -582,6 +718,7 @@ function beastState(){
function cover(x,y){
let n=0
+ if(!R.e)return 0
for(const d of [[1,0],[-1,0],[0,1],[0,-1]])if(tile(x+d[0],y+d[1],"e")=="#")n++
return n
}
@@ -619,7 +756,7 @@ function beastTurn(){
function burnTorch(){
if(!S.alive||S.win||!dark())return
if(S.flags.torch){
- let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r)?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE
+ let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r[0])?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE
if(S.level>=3&&S.flags.ward)drain++
if(hasMod("thin"))drain++
if(S.flags.relic=="wick")drain++
@@ -656,31 +793,32 @@ function shadow(){
return 0
}
-function enter(r,x=P[r][0],y=P[r][1],msg){
+function enter(r,x=(P[r]||P[r[0]])[0],y=(P[r]||P[r[0]])[1],msg){
S.hint=""
S.r=r;S.x=x;S.y=y
- if(r=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}}
- if(r=="p")S.path.pit++
- if(r=="c")S.path.crack++
- if(r=="d")return die("The black arch keeps its promise.")
- if(r=="e"&&dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast is on you before you can breathe.")
- if(r=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){
+ const f=r[0]
+ if(f=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}}
+ if(f=="p")S.path.pit++
+ if(f=="c")S.path.crack++
+ if(f=="d")return die("The black arch keeps its promise.")
+ if(f=="e"&&dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast is on you before you can breathe.")
+ if(f=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){
if(S.level>=3&&S.flags.ward){loseWard(S.flags.relic=="greave"?4:3);msg="Ward parts the crack; the blessing thins."}
else return die("Weighted by treasure, you wedge fast in the crack.")
}
- if(r=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){
+ if(f=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){
S.flags.torch=Math.max(0,S.flags.torch-(S.flags.relic=="greave"?2:S.flags.relic=="wick"?0:S.flags.torch))
S.flags.ash=!S.flags.torch?1:S.flags.ash
msg=S.flags.torch?"Crack strips the torch.":"Crack snuffs the torch."
}
- if(r=="g"&&S.flags.crown&&S.flags.key){
+ if(f=="g"&&S.flags.crown&&S.flags.key){
if(S.level<5)return startCamp(S.level+1)
if(S.level==5)return startFinale()
return win()
}
- if(r=="b"&&!S.flags.torch)showHint("torch")
- if(r=="c")showHint("crack")
- if(r=="e")showHint("beast")
+ if(f=="b"&&!S.flags.torch)showHint("torch")
+ if(f=="c")showHint("crack")
+ if(f=="e")showHint("beast")
setMsg(msg||roomMsg(r))
}
@@ -786,8 +924,9 @@ function step(dx,dy){
if(t=="#")return setMsg(roomMsg())
S.turn++
if(shadow()){setMsg("Darkness twists the room around you.");burnTorch();beastTurn();return}
- const ex=R[S.r].ex[t]
+ let ex=R[S.r].ex[t]
if(ex){
+ if(ex[0]=="!"){delete R[S.r].ex[t];ex=ex.slice(1)}
if(S.r=="e"&&nx==S.beast.x&&ny==S.beast.y)return die("The beast tears you open.")
burnTorch();return enter(ex)
}
@@ -819,6 +958,15 @@ function jumpLevel(level=2){
render()
}
+function testFloor(){
+ startRun(S&&S.mode||"standard",0)
+ S.level=9;S.currentLevel=9
+ build(seed)
+ S.r="g";S.x=P.g[0];S.y=P.g[1]
+ setMsg("Proving ground. Exit a in the hall drops one-way into h2; exit b in h2 climbs back.")
+ render()
+}
+
function key(e){
if(e.key=="m"){muted=!muted;render();return}
if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return}
@@ -835,6 +983,7 @@ function key(e){
if(e.key=="J"){jumpLevel(3);return}
if(e.key=="k"){jumpLevel(4);return}
if(e.key=="K"){jumpLevel(5);return}
+ if(e.key=="T"){testFloor();return}
if(S.gameState=="camp"){
if("1234".includes(e.key))applyCampChoice(e.key)
render()
@@ -915,7 +1064,7 @@ function render(){
remember()
const th=floorTheme()
U.innerHTML=`floor ${S.level} | ${th.name}<small>${buildTitle()} | ${th.tag} | ${th.accent} | ${MODES[S.mode||"standard"].label} | seed ${S.seed.toString(36)}</small>`
- I.textContent=sidePanel()+(DBG.show?`\n\ndebug\n${variantSummary()}\nsolve : ${escapeSolve(S.level,V.escape,V.beast,S.level>=3?8:6)?"ok":"fail"}${DBG.out?`\ncheck : ${DBG.out}`:""}\nkeys : v V R j J k K`:"")
+ I.textContent=sidePanel()+(DBG.show?`\n\ndebug\n${variantSummary()}\nsolve : ${R.e?escapeSolve(S.level,V.escape,V.beast,S.level>=3?8:6)?"ok":"fail":"n/a"}\nfloor : ${R.i&&R.v&&R.e?floorSolve(seed,S.level)||"fail":"n/a"}${DBG.out?`\ncheck : ${DBG.out}`:""}\nkeys : v V R j J k K T`:"")
L.textContent=[S.msg,S.hint,roomMsg()].filter(Boolean).join("\n")
X.onclick=()=>{reset(1);render()}
X.hidden=true