add zork game

Signed-off-by: Sean Cross <sean@xobs.io>
This commit is contained in:
Sean Cross 2023-12-11 15:15:19 +08:00
parent 180ad998f3
commit ee336e05f5
7 changed files with 1067 additions and 5 deletions

239
lib/co/index.js Normal file
View File

@ -0,0 +1,239 @@
/**
* slice() reference.
*/
var slice = Array.prototype.slice;
/**
* Expose `co`.
*/
module.exports = co['default'] = co.co = co;
/**
* Wrap the given generator `fn` into a
* function that returns a promise.
* This is a separate function so that
* every `co()` call doesn't create a new,
* unnecessary closure.
*
* @param {GeneratorFunction} fn
* @return {Function}
* @api public
*/
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
/**
* Convert an array of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Array} obj
* @return {Promise}
* @api private
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
/**
* Convert an object of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Object} obj
* @return {Promise}
* @api private
*/
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return Object == val.constructor;
}

98
lib/zork/index.d.ts vendored Normal file
View File

@ -0,0 +1,98 @@
declare class JSZM {
constructor(arr: ArrayLike<number>);
/**
* A generator function you define, which will be called to update the highlighting mode.
* fixpitch (if the argument is true) or normal (if argument is false).
*/
highlight(fixpitch: boolean): void;
/** Normally false. Set it to true to tell the game that it is a Tandy computer; this affects some games. */
isTandy: boolean;
/**
* A generator function that you must define that will be called to print text. You must implement
* wrapping, buffering and scripting yourself. The second argument is true if it should be
* copied to the transcript or false if it should not be.
*/
print(text: string, scripting: boolean): void;
/**
* A generator function which you must define 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)
*/
read(maxlen: number): void;
/**
* 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.
*/
restarted(): void;
/**
* A generator function you define, 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.
*/
restore(): void;
/**
* 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.
*/
run(): IterableIterator<any>;
/**
* 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.
*/
save(buf: Uint8Array): void;
/** The serial number of the story file, as six ASCII characters. */
serial: string;
/**
* 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.
*/
screen(window: number): void;
/**
* 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.
*/
split(height: number): void;
/**
* False for score/moves and true for hours/minutes. Use this to determine the meaning of arguments to updateStatusLine.
*/
statusType: boolean;
/**
* 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.
*/
updateStatusLine(text: string, v18: number, v17: number): void;
/**
* 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.
*/
verify(): void;
/** The ZORKID of the story file. This is what is normally displayed as the release number. */
zorkid: number;
static version: {
major: number;
minor: number;
subminor: number;
timestamp: number;
};
}
export = JSZM;

1
lib/zork/index.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./jszm')

682
lib/zork/jszm.js Normal file
View File

@ -0,0 +1,682 @@
/*
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<cs.length;i++) {
cs[i]={};
cs[i].local=new Int16Array(g8());
cs[i].pc=g24();
cs[i].ds=Array.from({length:g16()},g16s);
for(j=0;j<cs[i].local.length;j++) cs[i].local[j]=g16s();
}
this.mem.set(new Uint8Array(ar.buffer,0,purbot));
return [ds,cs,pc];
} catch(e) {
return null;
}
},
endText: 0,
fwords: null,
genPrint: function*(text) {
var x=this.get(16);
if(x!=this.savedFlags) {
this.savedFlags=x;
yield*this.highlight(!!(x&2));
}
yield*this.print(text,!!(x&1));
},
get: function(x) { return this.view.getInt16(x,this.byteSwapped); },
getText: function(addr) {
var d; // function to parse each Z-character
var o=""; // output
var ps=0; // permanent shift
var ts=0; // temporary shift
var w; // read each 16-bits data
var y; // auxiliary data for parsing state
d=v => {
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<str.length;i++) this.mem[t1+i+1]=str.charCodeAt(i);
this.mem[t1+str.length+1]=0;
// Lex text
w=x=>(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(op0<op1);
break;
case 3: // GRTR?
predicate(op0>op1);
break;
case 4: // DLESS?
xstore(op0,x=xfetch(op0)-1);
predicate(x<op1);
break;
case 5: // IGRTR?
xstore(op0,x=xfetch(op0)+1);
predicate(x>op1);
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;x<mem[op0];x++) cs[0].local[x]=pcget();
if(opc>1 && 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<ds.length;i++) vi.setInt16(e+i*2+8,ds[i]);
e+=ds.length*2+8;
for(i=0;i<cs.length;i++) {
vi.setUint32(e,cs[i].pc);
vi.setUint8(e,cs[i].local.length);
vi.setUint16(e+4,cs[i].ds.length);
for(j=0;j<cs[i].ds.length;j++) vi.setInt16(e+j*2+6,cs[i].ds[j]);
for(j=0;j<cs[i].local.length;j++) vi.setInt16(e+cs[i].ds.length*2+j*2+6,cs[i].local[j]);
e+=(cs[i].ds.length+cs[i].local.length)*2+6;
}
return ar;
},
screen: null,
split: null,
statusType: null,
updateStatusLine: null,
verify: function() {
var plenth=this.getu(26);
var pchksm=this.getu(28);
var i=64;
while(i<plenth*2) pchksm=(pchksm-this.memInit[i++])&65535;
return !pchksm;
},
view: null,
vocabulary: null,
zorkid: null,
};
JSZM.version=JSZM_Version;
try {
if(module && module.exports) module.exports=JSZM;
} catch(e) {}

View File

@ -2,6 +2,8 @@ import { FarpatchWidget, makeNavView } from "../interfaces";
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SerializeAddon } from '@xterm/addon-serialize';
import JSZM from "../../lib/zork";
var co = require("../../lib/co/index.js");
export class DebugWidget implements FarpatchWidget {
name: string;
@ -12,6 +14,10 @@ export class DebugWidget implements FarpatchWidget {
view: HTMLElement;
navItem: HTMLElement;
zork: Generator | undefined = undefined;
zorkCallback: (value: string | PromiseLike<string>) => void = () => { };
terminalLine: string = "";
terminal: Terminal;
fitAddon: FitAddon;
serializeAddon: SerializeAddon;
@ -35,25 +41,61 @@ export class DebugWidget implements FarpatchWidget {
onInit(): void {
console.log("Initialized Debug Widget");
}
readLine(): Promise<string> {
return new Promise((resolve, reject) => { });
}
onFocus(element: HTMLElement): void {
console.log("Displaying Debug Widget");
if (!this.initialized) {
// Ensure the parent frame doesn't get any scrollbars, since we're taking up the whole view
element.style.overflow = "hidden";
console.log("Initializing xterm.js");
// var terminalContainer = document.createElement("div");
// this.view.appendChild(terminalContainer);
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(this.serializeAddon);
this.terminal.onKey((e) => {
console.log("Key pressed: " + e.key);
this.terminal.write(e.key);
if (e.key === '\h') {
if (this.terminalLine.length > 0) {
this.terminalLine = this.terminalLine.substring(0, this.terminalLine.length - 1);
}
}
if (e.key === '\r') {
this.terminal.write('\n');
var zcb = this.zorkCallback;
var tl = this.terminalLine;
this.zorkCallback = () => { };
this.terminalLine = "";
zcb(tl);
}
else {
this.terminalLine += e.key;
}
});
this.terminal.open(this.view);
this.terminal.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n');
var zorkTerminal = this.terminal;
var debugWidget = this;
fetch("/zork1.z3").then((response) => {
response.arrayBuffer().then((buffer) => {
this.zork = co.co(function* () {
var zork = new JSZM(new Uint8Array(buffer));
zork.print = function* (str: string) {
str = str.replace("\n", "\r\n");
zorkTerminal.write(str);
};
zork.read = function* (maxlen: number): Generator {
// console.log("Zork: read " + maxlen);
var val = yield new Promise((resolve, reject) => {
debugWidget.zorkCallback = resolve;
});
return val;
};
yield* zork.run();
});
})
});
this.initialized = true;
}
element.appendChild(this.view);

BIN
static/zork1.z3 Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"target": "ES5",
"target": "ES2015",
"module": "CommonJS",
"outDir": "./build",
"strict": true