Using Quixe without the display layer?

I was wondering if it was possible to use Quixe without Glkote or anything affecting the DOM, so that it can be used in e.g. Node.js.

With a bit of luck, it would work like RemGlk: you give a JSON object to Quixe and get another one in return.

I looked at Quixe’s source to see if it was possible to modify it, but I had a hard time understanding it. Its working seemed convoluted to me, with gi_load.js, gi_dispa.js, glkapi.js…

In fact, what would be really great would be to decouple every component of Quixe and use JS import statements so that it can be used as a part of a larger JS projects. (Say you take only the interpreter part and not the display part).

It’s not urgent, I was just thinking of potential use cases, nothing concrete. One idea would be to use Quixe with NativeScript or React Native to make a mobile app. (I could also compile interpreters with RemGlk for Android and iOS, but I have no idea how to do that.) But again, these are only thoughts, nothing concrete.

Sure you can, though I’m not sure if anyone has it set up like that now. If you want it to work like RemGlk you obviously still need to provide it with a Glk library.

You might be interested in my glkote-term library which I use for testing.

tests/zvm.js shows how to join all the components of a JS Glk-based interpreter together. You should be able to replace new ZVM.ZVM() with Quixe and I think it should Just Work. It wouldn’t be hard to then make a RemGlk mode which takes the output of glkapi.js and writes to stdout and then waits on stdin and returns what it gets to glkapi.js

1 Like

If you remove glkote.js but leave in glkapi.js, you should have a setup that works exactly like RemGlk.

I say “should” because it hasn’t been thoroughly tested in that configuration. But I think I tested it a little, at some point.

Yes, the organization of the code is kind of ugly. The startup sequence starts at load_run() in gi_load.js and zig-zags through the other modules. But they are in fact meant to be modular; they can be used as Node modules in a Node environment. Each one refers to the others through well-defined APIs.

1 Like

Thanks for the answers!

With your help, I found that the function to submit an input is Glk.accept_ui_event (in glkapi.js) and the function that creates the update events is Glk.update (that we should modify to make it return the event in some way instead of passing it to GlkOte). Then it’s a matter of removing all calls to GlkOte.

In any case, I managed to retrieve the events by logging them in the console, so at least it seems to work.


To clarify what I meant by decoupling (which might in fact not be the right term): Having two different repos for Quixe the library and Quixe the playable webpage.

Instead of creating global namespaces, the library would export classes like Quixe, Glkote and so on. To start a story, you would write something like

story = new Quixe(Base64StoryFile, { outputEngine: new Glkote("#gameport") })

Then you could have multiple story each with their own state that you could control programmatically; something like

update = story.send(inputObject)

The Quixe library could even be published on NPM like that.

Then you would have the Quixe webpage, a separate project, which uses gi_load.js and the Quixe library for authors to have a plug-and-play solution to publish their work.

Like that, it would be easier to use Quixe in a larger JavaScript project using webpack and such, for example. But anyway, that’s another topic.


Thanks again, I’ll start tinkering when I’ll have time!

1 Like

Hi all, I’m brand new to IF, and I’m hoping to use Inform7 to create an interactive experience to use in a chatbot context. My project is written in Node.js, and, like the original poster, I’d like to use the interpreter part of Quixe without any of the UI components. Objects in, objects out.

What is the easiest way for me to run an Inform7 created story in Node.js? I’m happy to change the build outputs of the Inform7 project, just let me know what I need to do. Thanks in advance for any help! :pray:

Update: Emglken’s bin/emglken.js file has a run() method that does everything needed to run a story in Node.js. However, it plugs directly into the upstream system by reading from argv, accessing the filesystem, and talking to process.stdin/process.stdout.

After I try using emglken to run as is, my next step will be to tease apart the run() method and provide an API so a downstream user can use this library separately from the terminal environment.

If that doesn’t work, I’ll retreat to looking at decoupling Quixe.

The Emglken VMs are usable in any JS context, as long as you provide them with a GlkOte library to talk to. (As is Quixe FWIW.) But it doesn’t seem like you want that?

I can’t really tell what you’re trying to accomplish, but you might find it simpler to just run Glulxe (compiled against CheapGlk or RemGlk) as a subprocess.

Having just successfully built my own front-end to Quixe and run a .ulx and blorb through it, I can tell you that it’s very much possible. After being pointed in the right direction by Zarf, here’s what I figured out:

  1. Get rid of whichever version of the gltoke.js file is already in your project.
  2. Create your own GlkOte object or, as I did, add “GlkOte = YOUR_OBJECT;” to one of your .js files
  3. The object you replace GlkOte with must look like this as a baseline:

replacementForGlkOte = {
    version: "2.2.5experimental",
    game: null, // Added by me for calls in other functions
    lastGenValue: 0, // Added by me for keeping i/o in sync as per https://eblong.com/zarf/glk/glkote/docs.html#input

    init:
        function (game) {
            this.game = game;
            if (this.game == null) {
                if (typeof Game === 'undefined') console.log("Game undefined");
                else this.game = Game;
            }

            this.game.io = this;
            this.game.accept({
                type: 'init',
                gen: 0,
                support: ['timer', 'graphics', 'hyperlinks'],
                metrics: { left: 0, top: 0, width: 0, height: 0 }
            });
        },

    update: function (obj) {
        switch (obj.type) {
            case "pass":
                console.log("Pass, do nothing.");
                break;
            case "error":
                console.log("Error: " + obj.message);
                break;
            case "retry":
                console.log("Retry, still working, come back later.");
                break;
            case "update":
                //console.log("Update:");
                //console.log("\t\t" + JSON.stringify(obj, null, 2));
                //console.log("\twindows: " + JSON.stringify(obj.windows, null, 2));
                //console.log("\tContent " + JSON.stringify(obj.content[0], null, 2));
                this.lastGenValue = obj.gen;
                if (obj.content != null) {
                    let outputList = [];
                    obj.content.forEach((updateContent) => {
                        for (let i = 0; i < updateContent.text.length; i++) {
                            let updateText = updateContent.text[i];
                            let content = updateText.content;
                            if (content != null) {
                                let style = null;
                                let line = [];
                                let segment = { style: null, text: null };
                                content.forEach((lineSegment) => {
                                     //console.log("Segment: " + lineSegment);
                                    if (typeof (lineSegment) == "string") {
                                        if (segment.style == null) segment.style = lineSegment;
                                        else {
                                            segment.text = lineSegment;
                                            line.push(segment);
                                            segment = { style: null, text: null };
                                        }
                                    } else {
                                        // TODO: Implement object processing per https://eblong.com/zarf/glk/glkote/docs.html#linedata
                                        console.log("Unknown " + typeof (lineSegment) + ": " + lineSegment);
                                    }
                                });
                                outputList.push(line);
                            }
                            else {
                                outputList.push([]);
                            }
                        }
                    });
                    // Call your own function to actually use the outputList
                    // processOutput(outputList);
                }

                if (obj.input != null) {
                    obj.input.forEach((inputEntry) => {
                        //console.log("\tinput: " + JSON.stringify(inputEntry));
                        // Inform your input mechanism to start waiting for user input
                        //readyForInput(inputEntry.id, inputEntry.type, inputEntry.maxlen, inputEntry.initial);
                    });
                }
                //console.log("\ttimer: " + JSON.stringify(obj.timer, null, 2));
                //console.log("\tdisable: " + JSON.stringify(obj.disable, null, 2));
                //console.log("\tspecialinput: " + JSON.stringify(obj.specialinput, null, 2));
                break;
        }
    },

    extevent: function (val) { 
        //console.log("extevent: " + val); 
    },

    getinterface: function () { 
        return this.game; 
    },

    getdomcontext: function () { 
        //console.log("getdomcontext"); 
    },

    setdomcontext: function () { 
        //console.log("setdomcontext"); 
    },

    save_allstate: function () { 
        //console.log("save_allstate"); 
    },

    log: function (msg) { 
        //console.log("log: " + msg); 
    },

    warning: function (msg) { 
        //console.log("warning: " + msg); 
    },

    error: function (msg) { 
        //console.log("error: " + msg); 
    }

};
2 Likes

You could also subclass my glkote-term’s GlkOte class:

I also made a RemGlk mode, which probably won’t be too useful. I’m mostly just using it for more complete testing with RegTest.