/* JSZM - JavaScript implementation of Z-machine This program is in public domain. Documentation: The exported function called JSZM is the constructor, which takes a Uint8Array as input. You can also use JSZM.Version for the version number which is object with properties: major, minor, subminor, timestamp. Properties of JSZM instances are: .highlight(fixpitch) = A generator function you define, which will be called to update the highlighting mode, which is fixpitch (if the argument is true) or normal (if argument is false). (You don't have to set it if you aren't implementing variable pitch by default.) .isTandy = A boolean, normally false. Set it to true to tell the game that it is a Tandy computer; this affects some games. .print(text,scripting) = A generator function that you must define, and will be called to print text. You must implement wrapping and buffering and scripting yourself. The second argument is true if it should be copied to the transcript or false if it should not be. .read(maxlen) = A generator function which you must define yourself, and which should return a string containing the player's input. Called when a READ instruction is executed; the argument is the maximum number of characters that are allowed (if you return a longer string, it will be truncated). .restarted() = A generator function you can optionally define. When the game starts or if restarted (with the RESTART instruction), it will be called after memory is initialized but before executing any more. .restore() = A generator function you can define yourself, which is called when restoring a saved game. Return a Uint8Array with the same contents passed to save() if successful, or you can return false or null or undefined if it failed. .run() = A generator function. Call it to run the program from the beginning, and call the next() method of the returned object to begin and to continue. This generator may call your own generator functions which may yield; it doesn't otherwise yield by itself. You must set up the other methods before calling run so that it can properly set up the contents of the Z-machine mode byte. This generator only finishes when a QUIT instruction is executed. .save(buf) = A generator function you can define yourself, and is called when saving the game. The argument is a Uint8Array, and you should attempt to save its contents somewhere, and then return true if successful or false if it failed. .serial = The serial number of the story file, as six ASCII characters. .screen(window) = Normally null. You can set it to a generator function which will be called when the SCREEN opcode is executed if you want to implement split screen. .split(height) = Normally null. You can set it to a generator function which will be called when the SPLIT opcode is executed if you want to implement split screen. .statusType = False for score/moves and true for hours/minutes. Use this to determine the meaning of arguments to updateStatusLine. .updateStatusLine(text,v18,v17) = Normally null, but can be a generator function if you are implementing the status line. It is called when a READ or USL instruction is executed. See statusType for the meaning of v18 and v17. Return value is unused. .verify() = A normal function. Calling it will attempt to verify the story file, and returns true if successful or false on error. You can override it with your own verification function if you want to. .zorkid = The ZORKID of the story file. This is what is normally displayed as the release number. */ "use strict"; const JSZM_Version={major:2,minor:0,subminor:2,timestamp:1480624305074}; function JSZM(arr) { var mem; mem=this.memInit=new Uint8Array(arr); if(mem[0]!=3) throw new Error("Unsupported Z-code version."); this.byteSwapped=!!(mem[1]&1); this.statusType=!!(mem[1]&2); this.serial=String.fromCharCode(...mem.slice(18,24)); this.zorkid=(mem[2]<<(this.byteSwapped?0:8))|(mem[3]<<(this.byteSwapped?8:0)); } JSZM.prototype={ byteSwapped: false, constructor: JSZM, deserialize: function(ar) { var e,i,j,ds,cs,pc,vi,purbot; var g8,g16s,g16,g24,g32; g8=()=>ar[e++]; g16s=()=>(e+=2,vi.getInt16(e-2)); g16=()=>(e+=2,vi.getUint16(e-2)); g24=()=>(e+=3,vi.getUint32(e-4)&0xFFFFFF); g32=()=>(e+=4,vi.getUint32(e-4)); try { e=purbot=this.getu(14); vi=new DataView(ar.buffer); if(ar[2]!=this.mem[2] || ar[3]!=this.mem[3]) return null; // ZORKID does not match pc=g32(); cs=new Array(g16()); ds=Array.from({length:g16()},g16s); for(i=0;i { if(ts==3) { y=v<<5; ts=4; } else if(ts==4) { y+=v; if(y==13) o+="\n"; else if(y) o+=String.fromCharCode(y); ts=ps; } else if(ts==5) { o+=this.getText(this.getu(this.fwords+(y+v)*2)*2); ts=ps; } else if(v==0) { o+=" "; } else if(v<4) { ts=5; y=(v-1)*32; } else if(v<6) { if(!ts) ts=v-3; else if(ts==v-3) ps=ts; else ps=ts=0; } else if(v==6 && ts==2) { ts=3; } else { o+="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*\n0123456789.,!?_#'\"/\\-:()"[ts*26+v-6]; ts=ps; } }; for(;;) { w=this.getu(addr); addr+=2; d((w>>10)&31); d((w>>5)&31); d(w&31); if(w&32768) break; } this.endText=addr; return o; }, getu: function(x) { return this.view.getUint16(x,this.byteSwapped); }, handleInput: function(str,t1,t2) { var i,br,w; // Put text str=str.toLowerCase().slice(0,this.mem[t1]-1); for(i=0;i(i=0,x.split("").filter(y => (i+=/[a-z]/.test(y)?1:/[0-9.,!?_#'"\/\\:\-()]/.test(y)?2:4)<7).join("")); br=JSON.parse("["+str.replace(this.regBreak,(m,o)=>",["+(m.length)+","+(this.vocabulary.get(w(m))||0)+","+(o+1)+"]").slice(1)+"]"); i=this.mem[t2+1]=br.length; while(i--) { this.putu(t2+i*4+2,br[i][1]); this.mem[t2+i*4+4]=br[i][0]; this.mem[t2+i*4+5]=br[i][2]; } }, highlight: ()=>[], isTandy: false, mem: null, memInit: null, parseVocab: function(s) { this.vocabulary=new Map(); if (s === 0) { // If the story file does not contain a dictionary.. this.regBreak=new RegExp("[^ \\n\\t]+","g"); // use the default word separators return; // and early exit. } var e; var n; n=this.mem[s++]; e=this.selfInsertingBreaks=String.fromCharCode(...this.mem.slice(s,s+n)); e=e.split("").map(x=>(x.toUpperCase()==x.toLowerCase()?"":"\\")+x).join("")+"]"; this.regBreak=new RegExp("["+e+"|[^ \\n\\t"+e+"+","g"); s+=n; e=this.mem[s++]; n=this.get(s); s+=2; while(n--) { this.vocabulary.set(this.getText(s),s); s+=e; } }, print: ()=>[], put: function(x,y) { return this.view.setInt16(x,y,this.byteSwapped); }, putu: function(x,y) { return this.view.setUint16(x,y&65535,this.byteSwapped); }, read: ()=>[], regBreak: null, restarted: ()=>[], restore: ()=>[], run: function*() { var mem,pc,cs,ds,op0,op1,op2,op3,opc,inst,x,y,z; var globals,objects,fwords,defprop; var addr,fetch,flagset,init,move,opfetch,pcfetch,pcget,pcgetb,pcgetu,predicate,propfind,ret,store,xfetch,xstore; // Functions addr=(x) => (x&65535)<<1; fetch=(x) => { if(x==0) return ds.pop(); if(x<16) return cs[0].local[x-1]; return this.get(globals+2*x); }; flagset=() => { op3=1<<(15&~op1); op2=objects+op0*9+(op1&16?2:0); opc=this.get(op2); }; const initRng = () => { this.seed = (Math.random() * 0xFFFFFFFF) >>> 0; }; init=() => { mem=this.mem=new Uint8Array(this.memInit); this.view=new DataView(mem.buffer); mem[1]&=3; if(this.isTandy) mem[1]|=8; if(!this.updateStatusLine) mem[1]|=16; if(this.screen && this.split) mem[1]|=32; this.put(16,this.savedFlags); if(!this.vocabulary) this.parseVocab(this.getu(8)); defprop=this.getu(10)-2; globals=this.getu(12)-32; this.fwords=fwords=this.getu(24); cs=[]; ds=[]; pc=this.getu(6); objects=defprop+55; initRng(); }; move=(x,y) => { var w,z; // Remove from old FIRST-NEXT chain if(z=mem[objects+x*9+4]) { if(mem[objects+z*9+6]==x) { // is x.loc.first=x? mem[objects+z*9+6]=mem[objects+x*9+5]; // x.loc.first=x.next } else { z=mem[objects+z*9+6]; // z=x.loc.first while(z!=x) { w=z; z=mem[objects+z*9+5]; // z=z.next } mem[objects+w*9+5]=mem[objects+x*9+5]; // w.next=x.next } } // Insert at beginning of new FIRST-NEXT chain if(mem[objects+x*9+4]=y) { // x.loc=y mem[objects+x*9+5]=mem[objects+y*9+6]; // x.next=y.first mem[objects+y*9+6]=x; // y.first=x } else { mem[objects+x*9+5]=0; // x.next=0 } }; opfetch=(x,y) => { if((x&=3)==3) return; opc=y; return [pcget,pcgetb,pcfetch][x](); }; pcfetch=(x) => fetch(mem[pc++]); pcget=() => { pc+=2; return this.get(pc-2); }; pcgetb=() => mem[pc++]; pcgetu=() => { pc+=2; return this.getu(pc-2); }; predicate=(p) => { var x=pcgetb(); if(x&128) p=!p; if(x&64) x&=63; else x=((x&63)<<8)|pcgetb(); if(p) return; if(x==0 || x==1) return ret(x); if(x&0x2000) x-=0x4000; pc+=x-2; }; propfind=() => { var z=this.getu(objects+op0*9+7); z+=mem[z]*2+1; while(mem[z]) { if((mem[z]&31)==op1) { op3=z+1; return true; } else { z+=(mem[z]>>5)+2; } } op3=0; return false; }; ret=(x) => { ds=cs[0].ds; pc=cs[0].pc; cs.shift(); store(x); }; store=(y) => { var x=pcgetb(); if(x==0) ds.push(y); else if(x<16) cs[0].local[x-1]=y; else this.put(globals+2*x,y); }; xfetch=(x) => { if(x==0) return ds[ds.length-1]; if(x<16) return cs[0].local[x-1]; return this.get(globals+2*x); }; xstore=(x,y) => { if(x==0) ds[ds.length-1]=y; else if(x<16) cs[0].local[x-1]=y; else this.put(globals+2*x,y); }; // Initializations init(); yield*this.restarted(); yield*this.highlight(!!(this.savedFlags&2)); // Main loop main: for(;;) { inst=pcgetb(); if(inst<128) { // 2OP if(inst&64) op0=pcfetch(); else op0=pcgetb(); if(inst&32) op1=pcfetch(); else op1=pcgetb(); inst&=31; opc=2; } else if(inst<176) { // 1OP x=(inst>>4)&3; inst&=143; if(x==0) op0=pcget(); else if(x==1) op0=pcgetb(); else if(x==2) op0=pcfetch(); } else if(inst>=192) { // EXT x=pcgetb(); op0=opfetch(x>>6,1); op1=opfetch(x>>4,2); op2=opfetch(x>>2,3); op3=opfetch(x>>0,4); if(inst<224) inst&=31; } switch(inst) { case 1: // EQUAL? predicate(op0==op1 || (opc>2 && op0==op2) || (opc==4 && op0==op3)); break; case 2: // LESS? predicate(op0op1); break; case 4: // DLESS? xstore(op0,x=xfetch(op0)-1); predicate(xop1); break; case 6: // IN? predicate(mem[objects+op0*9+4]==op1); break; case 7: // BTST predicate((op0&op1)==op1); break; case 8: // BOR store(op0|op1); break; case 9: // BAND store(op0&op1); break; case 10: // FSET? flagset(); predicate(opc&op3); break; case 11: // FSET flagset(); this.put(op2,opc|op3); break; case 12: // FCLEAR flagset(); this.put(op2,opc&~op3); break; case 13: // SET xstore(op0,op1); break; case 14: // MOVE move(op0,op1); break; case 15: // GET store(this.get((op0+op1*2)&65535)); break; case 16: // GETB store(mem[(op0+op1)&65535]); break; case 17: // GETP if(propfind()) store(mem[op3-1]&32?this.get(op3):mem[op3]); else store(this.get(defprop+2*op1)); break; case 18: // GETPT propfind(); store(op3); break; case 19: // NEXTP if(op1) { // Return next property propfind(); store(mem[op3+(mem[op3-1]>>5)+1]&31); } else { // Return first property x=this.getu(objects+op0*9+7); store(mem[x+mem[x]*2+1]&31); } break; case 20: // ADD store(op0+op1); break; case 21: // SUB store(op0-op1); break; case 22: // MUL store(Math.imul(op0,op1)); break; case 23: // DIV store(Math.trunc(op0/op1)); break; case 24: // MOD store(op0%op1); break; case 128: // ZERO? predicate(!op0); break; case 129: // NEXT? store(x=mem[objects+op0*9+5]); predicate(x); break; case 130: // FIRST? store(x=mem[objects+op0*9+6]); predicate(x); break; case 131: // LOC store(mem[objects+op0*9+4]); break; case 132: // PTSIZE store((mem[(op0-1)&65535]>>5)+1); break; case 133: // INC x=xfetch(op0); xstore(op0,x+1); break; case 134: // DEC x=xfetch(op0); xstore(op0,x-1); break; case 135: // PRINTB yield*this.genPrint(this.getText(op0&65535)); break; case 137: // REMOVE move(op0,0); break; case 138: // PRINTD yield*this.genPrint(this.getText(this.getu(objects+op0*9+7)+1)); break; case 139: // RETURN ret(op0); break; case 140: // JUMP pc+=op0-2; break; case 141: // PRINT yield*this.genPrint(this.getText(addr(op0))); break; case 142: // VALUE store(xfetch(op0)); break; case 143: // BCOM store(~op0); break; case 176: // RTRUE ret(1); break; case 177: // RFALSE ret(0); break; case 178: // PRINTI yield*this.genPrint(this.getText(pc)); pc=this.endText; break; case 179: // PRINTR yield*this.genPrint(this.getText(pc)+"\n"); ret(1); break; case 180: // NOOP break; case 181: // SAVE this.savedFlags=this.get(16); predicate(yield*this.save(this.serialize(ds,cs,pc))); break; case 182: // RESTORE this.savedFlags=this.get(16); if(z=yield*this.restore()) z=this.deserialize(z); this.put(16,this.savedFlags); if(z) ds=z[0],cs=z[1],pc=z[2]; predicate(z); break; case 183: // RESTART init(); yield*this.restarted(); break; case 184: // RSTACK ret(ds[ds.length-1]); break; case 185: // FSTACK ds.pop(); break; case 186: // QUIT return; case 187: // CRLF yield*this.genPrint("\n"); break; case 188: // USL if(this.updateStatusLine) yield*this.updateStatusLine(this.getText(this.getu(objects+xfetch(16)*9+7)+1),xfetch(18),xfetch(17)); break; case 189: // VERIFY predicate(this.verify()); break; case 224: // CALL if(op0) { x=mem[op0=addr(op0)]; cs.unshift({ds:ds,pc:pc,local:new Int16Array(x)}); ds=[]; pc=op0+1; for(x=0;x1 && mem[op0]>0) cs[0].local[0]=op1; if(opc>2 && mem[op0]>1) cs[0].local[1]=op2; if(opc>3 && mem[op0]>2) cs[0].local[2]=op3; } else { store(0); } break; case 225: // PUT this.put((op0+op1*2)&65535,op2); break; case 226: // PUTB mem[(op0+op1)&65535]=op2; break; case 227: // PUTP propfind(); if(mem[op3-1]&32) this.put(op3,op2); else mem[op3]=op2; break; case 228: // READ yield*this.genPrint(""); if(this.updateStatusLine) yield*this.updateStatusLine(this.getText(this.getu(objects+xfetch(16)*9+7)+1),xfetch(18),xfetch(17)); this.handleInput(yield*this.read(mem[op0&65535]),op0&65535,op1&65535); break; case 229: // PRINTC yield*this.genPrint(op0==13?"\n":op0?String.fromCharCode(op0):""); break; case 230: // PRINTN yield*this.genPrint(String(op0)); break; case 231: // RANDOM if (op0 <= 0) { // If 'op0' is non-positive, reseed the PRNG. if (op0 === 0) { initRng(); // If 0, seed using Math.random(). } else { this.seed = (op0 >>> 0); // If negative, seed with the specified value. } store(0); // Reseeding always returns 0. break; } this.seed = (1664525 * this.seed + 1013904223) >>> 0; // Linear congruential generator store(Math.floor((this.seed / 0xFFFFFFFF) * op0) + 1); // Return integer in range [1..op0] (inclusive). break; case 232: // PUSH ds.push(op0); break; case 233: // POP xstore(op0,ds.pop()); break; case 234: // SPLIT if(this.split) yield*this.split(op0); break; case 235: // SCREEN if(this.screen) yield*this.screen(op0); break; default: throw new Error("JSZM: Invalid Z-machine opcode"); } } }, save: ()=>[], savedFlags: 0, selfInsertingBreaks: null, serial: null, serialize: function(ds,cs,pc) { var i,j,e,ar,vi; e=this.getu(14); // PURBOT i=e+cs.reduce((p,c)=>p+2*(c.ds.length+c.local.length)+6,0)+2*ds.length+8; ar=new Uint8Array(i); ar.set(new Uint8Array(this.mem.buffer,0,e)); vi=new DataView(ar.buffer); vi.setUint32(e,pc); vi.setUint16(e+4,cs.length); vi.setUint16(e+6,ds.length); for(i=0;i