0 votes
by (1.6k points)

I have developed a turn-based game in Twine 2.2.1, and Harlowe 2.1.0. It has a rather extensive set of variables, arrays, and datamaps, and I'm running into a peculiar problem with the built-in game save feature. 

The game has an eight-slot combined save and load function passage, which lets the user perform either action on the slots as appropriate. Either action pops an 'alert' with confirmation (or error), and the page is redisplayed. A link on the bottom allows the user to proceed when they're finished. The page consists of a set of HTML table cells coded as follows. The $loadOnly flag may be previously set to prevent saving (if the passage is called immediately upon startup, for example). Each occupied slot is labeled with the game 'day', and the character weight (which is important to the game). 

<td style='padding:15px'>(if: (saved-games:) contains "A")[(print:(saved-games:)'s "A") <br /><div class="button2">(link: "Load")[(alert: "Game slot 1 Loaded!")(load-game:"A")(goto: "SaveGame")]</div>(if: $loadOnly is 0)[ - <div class=
"button2">(link: "Replace")[(if:(save-game:"A", "Day " + (text:$day) + ", " +(text:(floor: $weight)) + "lbs") )[
    (alert: "Game Saved!")
  ](else: )[
    (alert: "Error - Game NOT saved!")
  ](goto: "SaveGame")]</div>]](elseif: $loadOnly is 0)[
  <div class="button2">(link: "Save to slot 1")[(if:(save-game:"A", "Day " + (text:$day) + ", " +(text:(floor: $weight)) + "lbs") )[
    (alert: "Game Saved to Slot 1!")
  ](else: )[
    (alert: "Error - Game NOT saved!")
  ](goto: "SaveGame")]</div>
  ](else:)[Slot 1 Empty]</td>

The game consists of daily "turns", and can last several hundred days of in-game time. Once the current game reaches longer durations, the chances of a saving error increases. This is an example of a progression from recent testing:

I started out with 8 filled slots from previous games at various stages. I started a new game, and regularly saved to a specific slot (in my case, slot 2). Note that this is with a published HTML file, running in Chrome. (Since testing natively in the Windows client butchers the gamesave/load functionality, and it must be tested live.)

- By about day 400, I hit my first save failure. I was able to replace a *different* slot first, and then replace the original slot immediately after.
- By about day 500, I had to try a couple other slots before I found one that would still allow an overwrite, but then oddly could still jump back and write the original slot as well.
- On day 568, no slots were allowing overwrites. I cleared my session data (and flushed all saves). Once cleared, I could save the game to the first four slots. It failed on the fifth.
- On day 600, after clearing all data again, I could only use three slots before it failed to set the fourth.

In the short term, I suppose I could reduce the number of available slots, or perhaps impose more restrictions on game duration. My suspicion is that the game history array is growing to immense size, and overflowing some kind of session storage limit.

Any thoughts on how I can approach this problem? Thanks in advance.

1 Answer

+1 vote
by (158k points)
selected by
 
Best answer

...game history array is growing to immense size

clarification: Harlowe doesn't use an Array to store each of the Moments (passage name / story variable state pairs) of it's story History. It uses an Generic Object hierarchy where each previous Moment is assigned as the prototype of the next more recent one.
eg. It is hard to visualise but the following example should give a general idea.

Current Moment object:
	variable1
	variable2
	variable3
	prototype: Previous Moment object
		variable1
		variable2
		variable3
		prototype: Previous Moment object
			variable1
			variable2
			variable3
			prototype: Previous Moment object
				variable1
				variable2
				variable3
				prototype: etc...

... as you can hopefully see this can lead to an Object that has a very deep prototype hierarchy.

A Save is basically a snapshot of the story History at the time the save was created.

...overflowing some kind of session storage limit.

The saves are stored within you web-browser's Local Storage area which has a default maximum size of between 5-10MD (per defined area) depending on the web-browser being used, this default can generally be manually changed within the web-browser's settings.

What constitutes a defined area is up to the developers of the particular web-browser, and it can also depend on if the HTML file being viewed is hosted on a web-server or just stored on the local machine.


Generally the best way to fix this type of issue is to be more fugal with the data being stored within story variables.
eg. If the value of a story variable doesn't change for the entirety of the story's play-through cycle then it may be better not to store that value within a story variable. The same goes for Object property values that don't change.

The following demonstrates one method that can be used to store constant values within Java-script.

a. Place the following within the Story Javascript area.

window.GE = window.GE || {
	"constantA": "some value",
	"constantB": 10,
	"constantC": true
}

b. Place the following within a Passage.

constant A: (print: GE.constantA)

10 times Constant B is: (print: 10 * GE.constantB)

Is Constant C equal to true? (if: GE.constantC)[Yes](else:)[No]

WARNING: The above should NOT be considered an example of best practices for: setting up a namespace; naming a namespace; nor for implementing constants. It is just a quickly assemble prototype to show what is potentially possible.

by (1.6k points)

Thanks for the detailed response, as always!

I do have some static content sitting in populated arrays -- good candidates for handling differently to reduce save data. I'm not convinced it will be enough to resolve everything but I'll definitely try it first.

Basically I have an on-screen message box that pulls a from one of several array pools of "flavor text"  based on the player's character size:

(if: $weight < 300)[
	(set: $flavorText to (either: ...$flavorText250))
](elseif: $weight >= 300 and $weight < 400)[
	(set: $flavorText to (either: ...$flavorText300))
](elseif: $weight >= 400 and $weight < 500)[
	(set: $flavorText to (either: ...$flavorText400))
] <!-- etc. -->

 

It seems to me my best bet would be to set up and (display:) a passage for each grouping instead, containing something like:

(set: $flavorText to (either: "message 1", "message 2", "etc."))

 

Assuming this approach doesn't cause some obvious performance penalty -- I should be able to do without sending any values to Java-script, but I'll be back if I need guidance.

As follow-up question: As a result of some early (and dubious) design choices, I do have quite a few (like 40 or 50) variables that serve as simple true/false flags. These are individually set with 0/1 numeric values. Would I likely see better storage efficiency if these were compiled into, say, a datamap? Does Harlowe allow storing an explicit boolean value, as in your JS example?

by (158k points)

 better storage efficiency ... a datamap?

This isn't a simple thing to answer:

  • on the one hand there is the overhead required to add each of the variables as a property of the variable State object,
  • on the other hand the object type return by (data-map:) is what is known as a 'Heavy' value (object) thus it requires more storage than a Boolean literal (true / false) does.
I would generally say using the Boolean literals over the data-map would be more efficient.
 
Another technique I forgot to mention is to use temporary variables instead of story variables whenever possible, because temporary variables aren't stored in History. 

eg. If the value of the $flavorText variable in your example is determine and used within the displaying of the current Passage (and any related Passages that are (display:)'ed within it), and if that particular value isn't needed by subsequent Passages then it makes more sense to use a temporary variable instead
by (1.6k points)
Thanks for the clarification. I've attempted to use temporary variables in Harlowe since their introduction, but almost always experienced unexpected problems. I suspect I'd been mishandling their scope. It may be time to try them out again.

I also feel like a bit of a moron for never realizing I could (set:) boolean literals to a variable. For some reason, I thought Harlowe only used them in the evaluation of operators. I'd swear I tried it early on, but probably did something silly like put "true" in quotes. This is why I've been using numeric values for flags.
...