Snowman 3.0 preview available!

Update (23 Oct 2023): As mentioned in a post below, I accepted an emergency-hire position right after this thread started and then finished my dissertation in early 2023. I’m slowly, after nearly a year, getting back into Twine work. I should, I hope, be back to Snowman work in a couple weeks.

Update (27 July 2022): After taking some feedback and concerns from existing authors using Snowman into consideration, the next version of Snowman will be 3.0 and not 2.2. There will still be a number of breaking changes between major versions, but these should not affect those who may want to stay with the 2.X branch.


After nearly three years of me dealing with a toxic work environment overlapping with COVID-19 issues, I am thrilled to announce Snowman 2.2 is now in a public preview!

Major Changes:

  • Sidebar: Snowman 2.2 now has support for a sidebar using the <tw-sidebar> element based on how Harlowe handles the same Twine 2 HTML element. It supports “undo” and “redo” user interactions with icons and story events. A reader can now “rewind” to the beginning of a story or to a previous navigated to passage and “redo” interactions again. This support is built into not only the history of the story, passages visited by a reader, but also a new global store, s, for authors to use.

  • State Management: Instead of asking authors to define an empty object as part of window.story.state, as some earlier versions did, Snowman 2.2 has a special proxied pseudo-global property, s. This is connected to a new state management system where the state of the story is constantly tracked, mirroring functionality present in other story formats. Each story navigation event generates a copy of the current state, allowing a reader to navigate backward and forward in not only the passages visited, but any associated values changed during those visits as well.

  • Event-Based Design: Most functionality now passes through an event-based state management system. Story events no longer generate browser events on window, as was the case with Snowman 2.0, but use an internal event emitter available to authors named State.events. This allows for an author to listen for events such as the story starting, start, or when a reader clicks on the icons associated with undo and redo functionality, undo and redo.

  • Storylets: Storylets are an approach to narrative planning where different parts of a story are divided up and become available to be played next based on changes to values during a reader’s interactions with the story. Snowman 2.2 now supports storylets using the passage tag ‘storylet’ and the special element <requirements> within the content of a passage. This mirrors the same approach the Harlowe story format took where passages are pre-processed when a certain macro is used within then. In Snowman 2.2, this is based on the passage tag ‘storylet’, which flags the passage for special processing.

  • Removed Markdown Support: Various markup for styling a passage and generating HTML to present text content was supported up to Snowman 2.0.3. However, because of assumptions all Markdown processing libraries make about documents, this would often cause major problems and could produce non-standard HTML when the contents of one passage was included in another. In Snowman 2.2, nearly all Markdown support is gone outside of the Twine 2 standard of using double square brackets, [[ and ]] for links between passages. Authors are strongly encouraged to use HTML to structure content in passages.

  • Moving away from Underscore: To maintain compatibility with past versions of Snowman, the library Underscore is still included. However, the handling of template code is now processed by EJS, a project under much more active development and usage. Template tag usage is now flipped: unescaped content should use <%- ... %> and escaped content use <%= ... %>.

  • Pseudo-Global Functions: In Snowman 2.0.3, multiple global functions were added to window. This is generally a poor pattern for any JavaScript libraries. In Snowman 2.2, template code now have access to a large number of pseudo-global functions passed into the rendering of template tags. These include Story.include(passageName) for including the content of one passage in another and access to History.hasVisited(passageName) for testing if a reader has visited a passage yet or not. Authors can also show or hide the new sidebar, Sidebar.show() and Sidebar.hide(), or put up a loading screen while data is being processed, Screen.lock() and Screen.unlock().

  • Extensive Testing: While Snowman 2.0.3 had a large number of unit tests, Snowman 2.2 now has hundreds of individual tests. Snowman 2.2 now tests against 47 entries from the Twine Cookbook. Of the 49 examples sets total in the Twine Cookbook, Snowman 2.2 does not have functionality for only two things: “Templates” (exclusive to SugarCube) and “Using Add-ons” (exclusive to SugarCube).

If you are interested in trying Snowman 2.2, it can be found here:
Format link: https://videlais.github.io/snowman/builds/2.2/format.js

Currently, the Snowman documentation does not cover Snowman 2.2. I no longer have access to the original source code of the documentation and the open source project used to generate it, GitBook, is no longer being maintained. The next major part of working on Snowman 2.2 will be re-generating the documentation and then creating around 12 new entries for the Twine Cookbook in the coming weeks.

6 Likes

For those interested in playing around with Storylets in Snowman 2.2, here is an example:

:: StoryTitle
Storylets in Snowman 2.2

:: UserScript[script]
s.testing = true;

:: Start
[[Start Adventure]]

:: Start Adventure
<%
  // Retrieve only 1 passage object from 
  //  any available based on their requirements.
  const availablePassages = Storylets.getAvailablePassages(1);
%>
<%- Story.include(availablePassages[0].passage.name); %>

:: First[storylet]
<requirements>
{
  "testing": true
}
</requirements>
<p>First</p>

:: Second[storylet]
<requirements>
{
  "testing": true
}
</requirements>
<p>Second</p>

In the above code, the passages are considered storylets if they use the passage tag ‘storylet’ and include the <requirements> element with JSON content of the properties to test against the global store s. For example, two passages are available because they each have "testing": true", which matches the s.testing = true in the Story JavaScript (passage with ‘script’ tag).

More examples will follow in the official documentation once I have re-built it later this week.

2 Likes

That’s amazing! Thank you for this! I hope you find a better place to work, been there, done that. I know it can be draining.

I’m building my own Story Format, so I know how much work it takes. I used to use Snowman, which is my biggest inspiration, but I have departed in irreconcilable ways. I had overwritten a lot of the original Snowman implementation, at which point it just made sense to build my own format. This update addresses some of the qualms I had, such as better State Management, Lifecycle Events, and better History Management. That’s awesome, almost makes me want to go back :joy:.

In any case, if you allow me some feedback, I give it with the utmost consideration.


  • State: In my format, the State object is a redux-lite store (custom implementation). I just needed something to subscribe to changes, nothing too complex. But one good thing is that I stage changes and only commit when there’s navigation to the next passage. I also save this mutation object with the snapshot instead of a complete copy of the store. I can’t imagine that would be good for longer games with thousands of variables. I think this is how SugarCube does it, right? Still, I don’t think it’s a good idea.
    My format is something like this:

     let store = {};
    
     const StoreHandler = {
         subscribers: Map<string, Function>,
         stage: {},
         getState: () => cloneDeep(merge(store, this.stage)),
         commit: () => /* Updates store and clear stage. */,
         dispatch: (prop, value) => value,
         subscribe: (prop, cb) => UnsubscribeFunction,
         // Utility methods ...
     };
    
     const stateProxy = new Proxy(
         StoreHandler,
         {
             get: (store, prop) => /* Either prop in store, or in StoreHandler. */,
             set: (store, prop, newValue) => newValue,
         }
     );
    
     window.story.state = stateProxy;
    

    When navigating, the history entry will keep the pid of the last passage and the mutation object (stage object in StoreHandler). So when going back it just undoes the mutation.

  • Philosophy: Snowman’s design principle was to be minimal, not introducing any specific syntax. “Functions, not Macros” was the slogan. This is what attracted me most to this format. So, I really don’t see how the new Storylet API is an upgrade. (Keeping in mind Snowman is supposedly for advanced users.) Previously, I could write the passages and register them in an object, like so:

    const storylets = [
       { pid: string, available: () => boolean },
       ...
    ];
    

    There’s nothing preventing users from doing it this way. And there’s nothing particularly wrong with the new way. My point is that it feels like the format might be going in a direction no-so-minimal after all and deviating from its original mission statement. There’s no way to compete with SugarCube on its turf, so why try?

  • On that note, a feature I’ve found more useful and would welcome support from the format itself would be that of tunneling (Ink) or Subroutine (ChoiceScript), basically creating a “return point” dynamically. This is useful if I want to graft branches dynamically. It is somewhat similar to Storylets but more powerful. With Storylets I have to render (include) the storylet’s content in the current passage, and once the player navigates away, I can’t easily return them to the same point they were before the storylet.
    Say I have a small branch, and once it runs its course, I want to redirect the player back to the point they were before they entered this grafted branch. And I want them to be able to visit this branch from different points in the story.


    I implemented that in my format with a stack, last in, first out. Every time the story enters a “tunnel” it adds an entry to the stack with the current pid.

     :: Start
    
     <% if (!visited("Next")) { %>
         <a href="javascript: void 0;" onclick="tunnel.push('Next');">Enter Tunnel</a>
     <% } %>
    
     :: Next
    
     [[Next-2]]
    
     :: Next-2
    
     <!-- Return to Start -->
     <a href="javascript: void 0;" onclick="tunnel.pop();">Leave Tunnel</a>
    
  • Markdown: I don’t understand what was the problem with Markdown. But I think the Markdown support is very important. Writing long-form fiction in HTML is not quality of life.

  • Version: Considering that you introduced code-breaking changes (and ceased backward compatibility), according to SemVer this is a major version update, meaning, it should be Snowman 3 and not 2.2. I personally agree with it. You have implemented new state management, new history management, new API, new lifecycle system, removed markdown, changed the templating syntax, etc. It feels like Snowman 3. An unsuspecting author might update from their current version and see their game breaking.


[Edit]

There is one issue I tried fixing myself by forking Snowman. I don’t remember now why I couldn’t do it, I just remember that to fix the issue it would take a greater refactoring than I expected. I don’t know if it has been fixed in Snowman 2.2, or if you’re even aware.

Currently, when you use the Save API, Snowman reloads the current passage. But if the passage has side effects (such as incrementing the value of a variable), this will generate unexpected behavior. Here’s a reproducible code:

:: Start

<% s.count = 1; %>

Count is <%= s.count %>.

[[Next]]

:: Next 

<% s.count += 1; %>

<%= s.count > 2
	? `Count should be 2, but is ${s.count}.`
	: "Count is 2."
%>

<button onclick="story.save();">Save</button>
1 Like

I like that approach. One of the many issues I have encountered in updating Snowman is keeping the use of s (window.story.state) similar to how it is described in the Twine Cookbook.

I may ultimately do something like you have shown here, but probably not for this version, as I still have to re-build the documentation, and that will take me a couple weeks at least.

Yes… and no. The central issue here is the assumptions authors bring to a story format in 2022. Many people have and are learning to use Twine from story formats like Harlowe and SugarCube and then moving to Snowman. These people take with them assumptions about what core functionality should be present and are often not knowledgeable enough to create what they they think all story formats should have, like undo and redo functionality, for example.

There’s no way to compete with SugarCube? I think we are going to majorly disagree here.

Yes, having written a book on ink, I can see how they could be helpful. I’ll think about how such an approach could work in the future.

The primarily issue was accessibility. Markdown libraries assume any input they receive is part of a larger, single document. This means code would frequently generate paragraph tags around input, as it assumed any free standing text should be its own paragraph. By itself, this is fine. However, a major issue can occur if the content of one passage is included in another or this process goes multiple levels deep. In these cases, the processing could create non-standard HTML patterns where paragraphs would be inside paragraphs multiple levels deep. For screen readers and some other forms of accessibility tools, such patterns become hard to manage.

This may ultimately be Snowman 3.0. It started as 2.2, but much more has been added as I worked through the Twine Cookbook examples and made sure Snowman had functionality to match each set.

As for loading a story format and seeing a game break, this happens quite frequently, as Harlowe and SugarCube do this all the time with their own updates.

This is also why I am letting people know this is coming weeks in advance so I can get feedback and people can get ready. It has also been, you know, almost three years. Harlowe and SugarCube have broken many more stories than I will in the same time.

Yes, that was one of the problems I tried to fix. In Snowman 2.0.3, it still used the HTML 5 History API and the use of hashes in the URL. This meant it would reload things and preform other side effects, as mentioned.

The new system uses window.localStorage to store values and does not reload anything upon using the save or loading system.

2 Likes

Ah, I see now. That’s a real concern. Maybe let the author know and trust them to embed passages more judiciously? In any case, great job and thank you again.

A better solution would be to use a “buffer” element node while constructing the HTML element structure being generated while processing the content of the “current” Passage. And whenever something like the content of a “child” passage needs to be added to the buffer, you walk up the ancestor chain to make sure one of them isn’t a <p> element, and if it is then you replace that ancestor <p> element with something else.

eg. Say the “buffer” currently contains the following …

<tw-passage ...>
	<p>First paragraph of "current" Passage...</p>
	<p>
		Second paragraph of "current" Passage...

		<!-- Processing of Passage content has reached this point...

			Next instruction is a request to include a 'child' Passage...
		 -->
	</p>
</tw-passage>

…and the element structure generated for the ‘child’ Passage as simple as…

<p>Content of the child Passage</p>

Which in theory would result in an invalid structure like the following…

<tw-passage ...>
	<p>First paragraph of "current" Passage...</p>
	<p>
		Second paragraph of "current" Passage...

		<p>Content of the "child" Passage</p>
	</p>
</tw-passage>

…so therefore one of the ancestor <p> elements needs to be replaced like so…

<tw-passage ...>
	<p>First paragraph of "current" Passage...</p>
	<section>
		Second paragraph of "current" Passage...

		<p>Content of the "child" Passage</p>
	</section>
</tw-passage>

note: Exactly what to replace that ancestor <p> element with is a different question…

As for loading a story format and seeing a game break, this happens quite frequently, as Harlowe and SugarCube do this all the time with their own updates.

Even if Harlowe or SugarCube have broken games with a minor version change, the semver system still requires backward compatibility in this case. Removing the entire markup system is hardly an accidental breakage, which is the most people are likely to expect from a minor update. The likelihood they’ll read the release notes before a minor update is also low.

1 Like

I had missed this post. It’s great to see new developments in Snowman! I’m not working with it at the moment, but I have a huge Snowman codebase to return to once I finish what I’m writing, so this is important to me.

I’m not sure about my comments, because, though I’ve done large and ellaborate games in Snowman, I’m not a typical user. I don’t use Twine itself but tweego, and I process my source with text replace tools like sed and awk before feeding it to Snowman, so my actual source has little JS and a lot of personal pseudocode. And my games are so customized that I discard basic Twine features altogether.

Manual passage rendering: passage links are a basic feature I don’t use. My WIP has a game loop that updates the current content by rendering other passages, while never navigating out of the current passage, and all my games extensively use the rendering of one passage inside another. This forces me to manage my own history, because functions like render() or rendertoselector() don’t add entries to the history. Also, I don’t think there are events associated to manual rendering. If rendering functions could optionally generate history entries and/or events, it would make some things easier to program for me and perhaps enable some new feature.

Underscore: will you eventually remove it altogether? I understand that Underscore was there mainly for its templating system, and EJS is a better replacement for that. I use extensively the rest of Underscore functions (specially the array and object manipulation tools) and I will probably have to add it manually (no problem).

This is all I can think of. Thanks!

I’d like to note that, with the release of Twine 2.4, the API for code colouring is now in place. The example Twine format repo now has a simplistic code highlighter included. Since the code highlighting uses CodeMirror internally, it’s well documented, even if the StoryFormat example is bare bones. A few more notes on the highlighter are included in the 2.4.0 release notes.

1 Like

Since there has been some confusion, and I have not updated for a couple weeks, and that has contributed to the confusion, let me hopefully clarify some additional changes that have happened during Snowman development since the first post.

Yup, I agree with you @mcd. People do not generally read release notes.

The next version of Snowman will not be 2.2, but 3.0. While I would have liked to continue the 2.X branch, I have decided to make some other changes that would make the latest version even more incompatible with previous work.

In what is now 3.0, authors can define whatever events they want, but if a specific event, say, render, would help, I can add it.

I have now removed Underscore from Snowman. In most cases, anything Underscore helped with, people can do using newer ES6 methods or using jQuery. However, if there are specific Underscore methods you want to see, I’d consider adding them as utilities functions. I’ve already added random() and delay() functions, as the Cookbook mentions them.

Thank you for mentioning this! I did not know there was documentation on extending Twine. I would like to see this in the official documentation on story formats, but I know such changes are very slow to appear from my previous work on such resources.

Snowman Development Updates:

Markdown is back. I have done some changes to how rendering happens and have re-introduced Markdown support. I am still testing various issues and so have not made an announcement on this until now.

Documentation is slowly being re-built. I have now re-built the 1.X and 2.X documentation. Working on what is now 3.X has been slower, as I have decided to change how concepts are introduced. My hope was to finish this by the end of July 2022, but a more realistic deadline is now mid-August 2022, as I have had to meet some other deadlines before I return to Snowman work.

Snowman 3.0 closer to end-of-August 2022. While development was nearly done with what was 2.2, I am still working on some remaining functionality for what will become 3.0. I would also like the documentation to be done before I officially release the new version.

4 Likes

I think the best resource at the moment is twine-2-story-format-starter/src/editor-extensions at main · klembot/twine-2-story-format-starter · GitHub. This project has an example set of editor extensions, though they don’t actually do anything useful.

I don’t think Underscore is solving problems for me that a lot of other authors are going to have. It’s a very complicated engine with a lot of objects and I use lots of _.where, _.without, _.sample, _.uniq, _.flatten, _.contains, _.uniq and others. Even if you added some of them I’d have to add Underscore anyway for the rest. So I’m ok with including manually Underscore in my game (or finding other ways to do all that stuff).

Ah, okay. That makes sense. Given your existing workflow, you can more easily re-integrate Underscore than I can trying to supply multiple existing functions.

Hello @videlais ! I’d like to ask the current status on Snowman 3. Would you recommend using it over Snowman 2 for a long term project? Are there any serious bugs still unsolved? I gather it’s still undocumented, but other than that, do you think it’s ready?

Thank you very much!

Are there any serious bugs still unsolved? I gather it’s still undocumented, but other than that, do you think it’s ready?

I apologize to you and others for being silent for so long. For others seeing this, I accepted a job very soon after starting this thread. Part of the requirements was moving 1200 miles and being ready to start a week after signing the contract in mid-August. This consumed the end of last year. Then, facing down some academic deadlines, I needed to finish my PhD dissertation over about six months, ending in July of this year.

I’m finally more stabilized, and I have been returning to Twine-related work. In the last few weeks, I’ve been working on Extwee, adding to the Twine Specs files (Twine 1 HTML, Twine 1 TWS, Twine 2 Archive HTML, and Twine 2 JSON), and building a database of older story formats.

Snowman development is scheduled next! The Twine Specs work is wrapping up, and the next version of Extwee should be coming out soon.


Current Status

Most of Snowman 3 works well, but it’s not production ready. I did a test a few weeks ago and found out Babel and other tools have changed how they work. I will need to adopt to these new changes and review my notes on where I left off.

Hopefully, I can return to work on it in a couple weeks, early November-ish.

1 Like

Thanks! No apologies needed, this is the curse of projects maintained by one person.

I guess that porting a work in progress from Snowman 2 to Snowman 3 once it’s working wouldn’t be trivial, would it?

It really depends on what functions you are using. But, generally, no. The shift will probably not be trivial for many people moving from Snowman 2 to Snowman 3.

My hope is those starting with Snowman 3 will find it easier to get started, though. It more closely matches naming conventions and functionality of those who have used Harlowe or SugarCube in the past.

3 Likes