JS arrays in squiffy

According to what I'm reading, variables (and arrays) in JS are all global in nature. However, I'm not having a lot of luck seeing arrays in functions. An example:

[[Start]]:

    var Arr1 = [];
    Arr1[0]=50;

    set("test1",Arr1[0]); //works
test1={test1}

[[Next]]

[[Next]]:
    set("test2",Arr1[0]); //fails
test2={test2}

Any ideas on how to use an array in other places (either JS functions or other squiffy sections)?


Declaring a variable with var is optional, and makes it local to the current function.


Taking Var out of this code causes it to crash in Squiffy.

Actually, last night I poked around some more and found a conversation where someone talked about the same thing - JS variables and arrays do not cross into new sections - they are local to that section.

If you want to jump a variable to another section, you have to carry it across in a squiffy variable, using Set and Get commands. Which means arrays are out.

Which is okay. Last night in bed, I thought about it and figured a way to do it with just squiffy variables. I'm not sure how keen I am with this -an array would have been perfect. But you do what you can do with this application, I guess.


Actually, last night I poked around some more and found a conversation where someone talked about the same thing - JS variables and arrays do not cross into new sections - they are local to that section.

Technically, undeclared variables aren't even variables; they're properties.

If you just do Arr1 = [] without the var, it doesn't declare a local variable. It assumes Arr1 is shorthand for this.Arr1. But Squiffy does something a little odd (which I'd forgotten about), so even though this refers to something like squiffy.story.sections['Start'], setting an attribute on it doesn't work as expected.

The usual way of handling global variables in cases like this is to use an object you know to be global. For example, you can refer to window.Arr1, and know that it's the same variable everywhere. That's the easy way to use global JS variables in Squiffy.

But there's a downside. Squiffy attributes are stored in LocalStorage, so they're still there when the player closes the browser and opens it again. The same isn't true for JS variables, which only last as long as the window exists. So in this case, it still might be helpful to use a Squiffy attribute.

Which means arrays are out.

I don't see why it should be.

How about:

[[Start]]:
    var Arr1 = [0, 1, "foo"];
    set ("Arr1", JSON.stringify(Arr1));

The attribute Arr1 has been set to the string: '{Arr1}'.

OK, now try [[Next]].

[[Next]]:
    var Arr1 = JSON.parse(get("Arr1"))
    set("test2", Arr1[1])

The middle element of that array is {test2}.

JSON lets you turn a javascript data type into a string and back again. It's normally used when stuff like arrays and objects needs to be sent between a client and server; but it works just fine for storing arrays or other data in a Squiffy attribute. This method works fine whether it's arrays or plain objects you're dealing with. Or even arrays of objects.


An alternative I've used, to avoid having to explicitly use JSON everywhere, is to store them in a javascript variable, and put that into an attribute when saving. Something like (off the top of my head):

[[Start]]:
    // Redefining the save function, so that it will save our custom variable
    squiffy.story.save = function () {
        // This is what 'save' does by default:
        squiffy.set('_output', squiffy.ui.output.html());

        // Save our custom variable
        squiffy.set('myvariable', JSON.stringify(window.myvariable))

        // And abuse the 'transition' mechanism to make sure it's loaded again
        // by causing the javascript in a particular section to to be run as soon as this saved game is loaded
        squiffy.set('_transition', "squiffy.story.sections['load variables'].js")
    }
    window.myvariable = [];
    // Now we've guaranteed that window.myvariable exists, it will be accessible because it's in the context chain
    // so we don't need to look at it again

Now, take a look at the [[first section]].

[[load variables]]:
    window.myvariable = JSON.parse(get("myvariable"))

This section should never actually be visited, so it doesn't need text. It just makes sure that the variable is saved correctly.

[[first section]]:
    myvariable = ["apple", "banana"];

So what next?
Do you  like [more bananas], [cherries], or [peaches]?
Or would you like to see [[what the variable contains]]?

[more bananas]:
    myvariable.push("all the bananas")

Okay! Now what?

[cherries]:
    myvariable.push("cherry")

Okay! Now what?

[peaches]:
    myvariable.push("peaches")

Okay! Now what?

[[what the variable contains]]:
    set ('testvariable', myvariable.join(', '))

That variable contains: {testvariable}.

This means you have all the flexibility of a JS variable, which is accessible anywhere as long as you explicitly refer to it as window.myvariable the first time. And it persists into a saved game as well.


I think that might do it. I'm going to have to play with it a bit. One thing I did see - I'm expecting it to be pretty large (800+). It (your first example) doesn't seem to like leaving the array open - does it need to be allocated?

[[Start]]:
    var Arr1 = [];
    set ("Arr1", JSON.stringify(Arr1));

    Arr1[1]=5;
    
The attribute Arr1 has been set to the string: '{Arr1}'.

OK, now try [[Next]].

[[Next]]:
    var Arr1 = JSON.parse(get("Arr1"))
    set("test2", Arr1[1])

The middle element of that array is {test2}.

In this case, I get back true, not 5.


doesn't seem to like leaving the array open

Not sure what you mean there.

does it need to be allocated

No; javascript is a high level language. An array is created by using [], whether or not you put values in them.

In this case, I get back true, not 5.

When you do set ("Arr1", JSON.stringify(Arr1));, you're setting the squiffy attribute to the string "[]" - the current values in that array. You can then add an element to the array, but the array is just a local variable.

The line in the second function, Arr1 = JSON.parse(get("Arr1")), restores that variable to the exact values it had when set ("Arr1", JSON.stringify(Arr1)) was called. In this example, an empty array.

So your second section, you look at Arr1[1] and get back undefined because the array is empty. I'm not sure what weirdness of Squiffy's text processor causes this to render as "true", but I think I've come across that quirk before.

If you want to add values to the array, you need to do that before the call to JSON.stringify.

That's one of the reasons I prefer the second option. 8 lines of script at the start of your game, and then you can trust Squiffy to save the values after each section.


[[Start]]:
    var Arr1 = [];
    set ("Arr1", JSON.stringify(Arr1));
    var Arr2 = [];
    set ("Arr2", JSON.stringify(Arr2));
    Arr1[1]=5;
    Arr2[1]=42;
    set("Arr2_1",JSON.stringify(Arr2));
    
The attribute Arr1 has been set to the string: '{Arr1}'.

The attribute Arr2 has been set to the string: '{Arr2}'.

The attribute Arr2_1 has been set to the string: '{Arr2_1}'.

OK, now try [[Next]].

[[Next]]:
    var Arr1 = JSON.parse(get("Arr1"))
    set("test2", Arr1[1])
    var Arr2 = JSON.parse(get("Arr2"))
    set("test3", Arr2[1])
    var Arr2_1 = JSON.parse(get("Arr2_1"))
    set("test3", Arr2[1])
    set("test4", Arr2_1[1])

The middle element of that array is {test2}.

The middle element of that array is {test3}.

The middle element of that array is {test4}.

Animated GIF

Peek 2021-01-03 20-53


So what am I doing wrong here? I thought (kinda) that I understand what you are doing (saving the location of the array, rather than the array). But I only get trues for values on the second section.

[[Start]]:
    var Arr1 = [];
    set ("Arr1", JSON.stringify(Arr1));

    Arr1[1]=5;
    
    Arr1[10]=50;

OK, now try [[Next]].

[[Next]]:
    var Arr1 = JSON.parse(get("Arr1"))
    set("test1", Arr1[1]);
    set("test2", Arr1[10]);

test1 {test1}

test2 {test2}

You can't modify the array after you've "stringified" it into JSON. Well, you can, but it has no affect on the "stringified" array. You have to "stringify" into JSON after the array is the way you want it.

squiffy-bluevoss-2021-01-05


Okay, I understand. I needed a way to keep track of things beyond the player's knowledge, but that involves them changing. Looks like I'll have to go with "plan B"


saving the location of the array, rather than the array

No, that's only in the version with the more complex function at the beginning.

In this version, stringify saves a snapshot of what the array's values currently are, and parse restores the array from the snapshot. You need to put the stringify line after the code that changes the array.

If you don't want to save and load the variables in every JS section that uses them, you should use the version with the function. Here's a more compressed version without the comments; which should be easier to adapt if there's more than one array you want to save:

@start Start

[[initialise variables]]:
    Object.defineProperties (window, Object.fromEntries(["variable1", "Array2", "carbuncle"].map(attr => [attr,{get: () => JSON.parse(squiffy.get(attr) || ""),set: (v) => squiffy.set(attr, JSON.stringify(v))}])))

[[Start]]:
    squiffy.story.save = eval("("+squiffy.story.save.toString().replace('}', ";squiffy.set('_transition', \"squiffy.story.sections['initialise variables'].js\")}")+")")
    squiffy.story.sections['initialise variables'].js()

You can start your game as normal here, and trust in the Javascript Magic above to automatically save and restore your variables. All you need to do is change the list of variables.

Note: You can't do this with javascript objects that contain functions, objects referring to a network or stream handle, prototypes, or any variable which is already a property of the window object. Apart from that, you can paste in a couple of lines and not have to worry about it any more. Just change the list of variable names which I've highlighted in blue in the code.


In this version, stringify saves a snapshot of what the array's values currently are, and parse restores the array from the snapshot. You need to put the stringify line after the code that changes the array.

This is how I wanted to explain it, but I had to use the example to show it instead. (Hehehe.)

If you don't want to save and load the variables in every JS section that uses them, you should use the version with the function.

Here's what I was wondering: Which approach would slow the game down worse: going back and forth from JSON to an array, or having a huge array? It seems like that array would be getting created (in some form or fashion) each time that JSON string is parsed; no? (Seriously asking -- not rhetorical at all.)


Here's what I was wondering: Which approach would slow the game down worse: going back and forth from JSON to an array, or having a huge array? It seems like that array would be getting created (in some form or fashion) each time that JSON string is parsed; no? (Seriously asking -- not rhetorical at all.)

In theory you want to minimise the number of times data is serialised or deserialised.

In ranked order of efficiency:

  • Saving data
    1. in a beforeunload event handler (when the page is closed; best)
    2. when the data has been changed (set binding)
    3. manually at the end of each section that changes the variable
    4. in squiffy.story.save (after every section; worst)
  • Loading data
    1. the first time data is accessed in a diven session (get binding)
    2. when the game is loaded
    3. manually at the start of each section that uses the variable (may be better than 2 if the variable isn't always needed)

But the time taken to evaluate a string like [undefined,1,2,5] is very small. Unless you're talking about hundreds of variables, or an array with thousands of elements, I don't think the player is ever going to notice.

Still, here's a more efficient version of that code:

[[initialise variables]]:
    var jsvars = {};
    ["variable1", "vaariable2", "myvariable3"].forEach(attr => {
        Object.defineProperty(window, attr, {
            get: () => {
                if (jsvars[attr]) {
                    return jsvars[attr];
                } else {
                    let stored = squiffy.get(attr);
                    return jsvars[attr] = stored ? JSON.parse(stored) : undefined;
                }
            },
            set: (val) => {
                if (val !== jsvars[attr]) {
                    jsvars[attr] = val;
                }
            }
        });
    });
    window.addEventListener('beforeunload', (event) => {
        $.each(jsvars, (key, value) => squiffy.set(key, JSON.stringify (value)));
        squiffy.set('_transition', "squiffy.story.sections['initialise variables'].js");
    })

[[Start]]:
    squiffy.story.sections['initialise variables'].js();

Start your game as normal…

This is only saving the variables when the browser window is closed; which means that some data might be lost if the browser crashes or if the unload event is prevented (some popup blockers prevent this event from firing normally). Note that we can't use set to check when a variable is changed, because this only happens when a new value is assigned to it. You can add values to an existing array or object without calling its property set method.

It also defines the properties, but only calls JSON.parse the first time each property is accessed. This could be more efficient, but the difference is likely too small to notice in most cases.


After a lot of thought about this, I think I'll just handle it through function calls that select the correct variable. As for the moving items (spaceships) I'll just have to come up with a set of variables that combine location and velocity in one number.

Thanks for your help!


Log in to post a reply.

Support

Forums