0 votes
by (160 points)

I am conducting a trial next week where I am comparing two different case-based learning approaches (branched medical case presentations and traditional linear medical case presentations) in a clinical pharmacology class I have teached. I have run into a problem where my very limited javascript programming skills unfortunately doesn't suffice. 

I want to log the participants individual passage history (like the history macro in harlowe) as well as time spent on each passage. I have succesfully managed to transfer variable-input into google sheets. The reason I don't use Harlowe is because I use a lot of text-inputs in each passage.

I it possible (and how) to assign the individual passage history and time spent on each passage to an array?

I have tried to follow these previous questions and answers, but without luck unfortunately:



Thank you in advance :-)


1 Answer

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

This problem can be broken down into 3 smaller ones:

1. How to store the information you need to track.

You need to track at least two things:
a. The time that the participant started.

I suggest using a numeric story variable to do this, eg.$started

b. The name of each Passage they visited and the time spent viewing that passage.

I suggest using an Array of Generic Object instances, where each Generic Object represnts a Passage visited.

The code to define these story variabes within your project's StoryInit special passage would look something like the following.

<<set $history to []>>
<<set $started to 0>>

2. How to update the story variables.

There is a special passage named PassageReady whos contents gets processed after each Passage Transition, you can use code like the following in it to update the above variables.

/* Note: time is measured in the number of milliseconds since Unix Epoch. (January 1, 1970 00:00:00 UTC) */

/* Update the time of the previous history record if there is one. */
<<if $history.length gt 0>>
	<<set $history.last().time to Date.now()>>
	/* Record the time the first passage was shown. */
	<<set $started to Date.now()>>

/* Add current passage's history record to the array, unless it has a 'no-history' passage tag. */
<<if not tags().includes('no-history')>>
	<<set $history.push({
		"passage": passage(),
		"time": 0

The Generic Object being added (pushed) to the $history Array consists of two properties:
passage - used to store the Name of the current passage being shown.
time - number of elapsed milliseconds, starts as zero but is updated when the next passage is shown.

The above code example uses a number of features of SugarCube and JavaScript:
 <array>.length; <array>.last(); Date.now()tags(); <array>.includes(); not operator; <array>.push(); and passage()

3. How to determine then number of seconds between each history record.

This can further be broken for into 2 sub-parts:

a. How to calculate the number of seconds between to millisecond values.

The following JavaScript defines a setup.toSeconds() function which accepts two millisecond values and returns the number of whole seconds between then. This code should be placed within your project's Story Javascript area.

setup.toSeconds = function (from, to) {
	/* Determine the number of milliseconds between the two times. */
	var milliseconds = from - to;

	/* Convert the milliseconds into whole seconds, trancating any remainding time. */
	var seconds = Math.abs(Math.trunc(milliseconds / 1000));

	return seconds;

The above code example uses a two JavaScript features: Math.trunc() and Math.abs()

b How to loop through the history records to display their contents.

note: The Passage containing the following code should be assigned a no-history Passage Tag so that it is exclude from the history tracking.

	<<set _last to $started>>
	<<for _event range $history>>
		<br>Passage: <<= _event.passage>>, Seconds: <<= setup.toSeconds(_last, _event.time)>>.
		<<set _last to _event.time>>

The _last variable is initially assigned the time that the participant started and then later updated to be the time associated with the previously displayed history record, doing this allows the calculation of the elapsed seconds between each point in time.

by (160 points)
This is absolutely brilliant! Thank you for taking the time to answer!

I only have one follow-up question. I have set up a short story which goes:

Start --> Next1 --> Next2 --> Next3 --> end

I have used your "no history" code in the passage "end".

From "Start" to "Next1" I get no errors but from "Next"1 and until "end" I get the error: <<set>>: bad evaluation: State.variables.history.last is not a function

The passages nor the times from "Next2" to "end" are not recorded in the history variable. Only from "Next1".

Do you have any idea what might be the problem and the solution to this?

Thank you again for your help. It is much appreciated!
by (154k points)

When using the Passage Tags to state the story format you are using you need to state both the name and full version number, as answers can vary based on this information.

Which version of SugarCube 2.x are you using?
And are you using the Decktop release or the Web-browser based release of the Twine 2.x application?

Because the <array>.last() function has only been avaiable since v2.27.0

If you are using a version of SugarCube earlier than that then you have two options:

1. Upgrade your SugarCube 2 to the latest version. (recommend)

You can find instructions on how to do that on the SugarCube 2 web-site.

2. You can replace code that calls the <array>.last() function.

/* Replace the following line of code in PassageReady. */

<<set $history.last().time to Date.now()>>

/* With this line of code. */

<<set $history[$history.length - 1].time to Date.now()>>


by (160 points)

Thank you! You solved it!.I had an old version of Sugarcube installed.

My last problem is this: I managed to connect and send the data to google sheets using 

This with the array-fix you suggested in this thread

But data from the history array displays as the following in google sheets:

[object Object],[object Object],[object Object],[object Object]

I suspect that it is because I use the harlowe.State.variables, but I am unsure.

var sendData = JSON.stringify({
"next": harlowe.State.variables['next'], 
"next1": harlowe.State.variables['next1'], 
"next2": harlowe.State.variables['next2'], 
"history": harlowe.State.variables['history']

I tried to change the "history" variable to the following (according to this thread) but without success. Probably because my programming skills are failing me:

var sendData = JSON.stringify({
"next": harlowe.State.variables['next'], 
"next1": harlowe.State.variables['next1'], 
"next2": harlowe.State.variables['next2'], 
"history": State.active.variables.history

Again I really appreciate the kind help!

by (154k points)

There are (at least) two issues with the means you are using to send the contents of the $history story variable to a Google Spreadsheet:

1. The examples included with the answer referenced by the first two links in last comment are for Harlowe, they can't be used as is in a SugarCube based project.

2. The data within the $history variable is an Array of Generic Object instances, and you can't store that data-structure as is within the cell of a Google spreadsheet.

The following example is a Sugar equivalent of your above Harlowe one, it uses the <<script>> macro to execute the JavaScript; the State API to a access the variables; and the JSON.stringify() function to convert the data-structure into a String value.

	var sendData = JSON.stringify({	
		"next":		State.variables['next'], 
		"next1":	State.variables['next1'], 
		"next2":	State.variables['next2'], 
		"history":	JSON.stringify(State.variables['history'])
		url: "YOUR URL TO script.google.com GOES HERE",
		dataType: "json",
		data: sendData
	}).done(function() {});

note: Obviously I don't have the URL requred to send the data to your Google spreadsheet, so you will need to change the url String value in object being passed to the above $.arax() function call.

Welcome to Twine Q&A, where you can ask questions and receive answers from other members of the community.

You can also find hints and information on Twine on the official wiki and the old forums archive.

See a spam question? Flag it instead of downvoting. A question flagged enough times will automatically be hidden while moderators review it.