(removed)

content is king :smiley:
i think twine did a great job of that because you can just jump right in to using the editor. even if the feature set is relatively limited.

Hello, I just started looking at your game engine, and even the quick intro game feels polished and I was sucked in immediately. I want to try this out for a real game, thanks for creating this!

2 Likes

Thank you! :smiley:

Feel free to ask if you have any questions.

I have a small update for Sadako. Mostly a few bug fixes plus an added feature.

You can find it here: https://github.com/Tayruh/sadako/releases/tag/0.13.0

For the newly added feature, I have added the sadako.settings object. This object is used for configuration settings or tracking ending flags or the like. In other words, persistent data that you wouldnā€™t want saved to a single save file. Itā€™s automatically loaded with the game and you can save the object to storage using the sadako.saveSettings() function.

For bug fixes, there was an issue in how scene data was loaded from the save file which caused it to stomp on default scene settings if the save file was older and didnā€™t contain the scene. Sadako now correctly merges the old save data with the new scene entries.

I also think this was just a brainfart and not an intentional design, but I had ~ else if conditions verbatim. As in, there had to be exactly one space between else and if or else it would treat it as an else statement. Oops! I have now gone the complete opposite direction and you can have any number of spaces or tabs (including none) between each item. Meaning both of these are valid:

~elseif1 == 1
~   else     if   (1 == 1)

I have also updated the VS Code syntax extension to reflect this change.

2 Likes

I created a sandbox for Sadako for messing with the script right inside the browser. It displays the sadako script and javascript in editable text boxes alongside the actual game, and you can edit the text and then hit the refresh or restart buttons in the menu bar to update the page with your changes.

I have included some examples to mess with. Theyā€™re intentionally complex because they were some of my own test code examples for trying out new features. I plan on adding more examples soon.

Also, when you toggle the sadako script edit box on, it should jump to the area of the code that is being viewed, both in the examples and when editing your own code.

Please give it a try and tell me what you think.

You can find it here: https://tayruh.github.io/sandbox/

2 Likes

the syntax for the script parts looks a bit scaryā€¦
what are all the ~~~ or --- >> symbols for?

        ++ {get_up} [Get up]
            ~~~ if %.main_room.get_up === 1
                You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.
                Either way, you're going to need assistance to stand.
            ~~~ else
                You're not getting up on your own. Maybe there's something around in the darkness that can help support your weight.
            --- >> go_back

+ are choices. The script below it is the result of selecting that choice.

~ are condition blocks (if, else, else if, for, while). The script below it is the result of the condition being true.

The total count of the +, ~, and - tokens are how ā€œdeepā€ the block is. So ++ is level 2 and ~~~ and --- are level 3, meaning that theyā€™re deeper than (in other words ā€œinsideā€) the + choice block.

The - token is unique among other depth tokens in that its only purpose is to force the depth to be a certain level. In this example, it breaks out of the ~ else block.

You can think of it like this, if I were to have used brackets instead of depth counts:

        + {get_up} [Get up] {
            ~ if %.main_room.get_up === 1 {
                You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.
                Either way, you're going to need assistance to stand.
            } 
            ~ else {
                You're not getting up on your own. Maybe there's something around in the darkness that can help support your weight.
            } 
            >> go_back 

As for >>, thatā€™s a jump command. You can jump to a page or label (the label being ā€œgo_backā€ in this instance), and you can return from there with <<.

I hope I explained that well. :x I havenā€™t figured out a good way to explain it clearly yet.

iā€™m confused why you need the >> as well as the command? since go_back is presumably the actual command. so then i need to remember both, and they have to match upā€¦ was it > or >>> or ~~~ ? itā€™s hard.

ā€œgo_backā€ is the name of the label itā€™s jumping to, not a command. The actual command is the >> part.

Aside from a few reserved words used in combinations with tokens (like if in the ~ if statement), all actions are done through the tokens without any words.

The design concept behind this is that it makes your story script a lot easier to read without having to mentally ignore a ton of words reserved for the engine. The downside is itā€™s confusing to look at if you donā€™t know what things do though.

Hopefully if you check out the reference itā€™ll start to make sense. https://github.com/Tayruh/sadako/blob/master/reference.md

Edit: >> is not one of the depth tokens, so itā€™s just >> no matter what depth it is. (You can also write ++ as + + or however many spaces, but >> is only ever >>). I could see how thatā€™s confusing thoughā€¦ It actually didnā€™t even cross my mind that it could be confused for one. I wonder if I should change it. :thinking: I chose that token because it reminds me of a fast forward button. It made sense at the time.

ok! so itā€™s kind of likeā€¦ APL? very symbolic but concise once you know the notation. that imposes quite a learning curveā€¦

Wellā€¦ Youā€™re not wrong. Hopefully, it looks more intimidating than it actually is. I think itā€™s pretty simple, but Iā€™m the one who wrote it, so obviously my opinion on that is a bit biased.

What I should probably do is write a bunch of comparison examples of like ā€œThis is how you do it in Twine, and this is how you do it Sadakoā€, since I imagine thatā€™s the direction most people are coming from. Maybe with some for Ink also, for the small portion of people familiar with that language.

1 Like

Actually, having looked at APL, mine isnā€™t anywhere near as complex as that. This right is here is probably the most complex it gets (unless youā€™re purposely trying to make something obtuse):

+ {kitchen_door} [Enter the {:#.kitchen::kitchen::first door:}.];; >> #kitchen

While I admit that can be kind of confusing looking, itā€™s actually pretty simple. The goal of this example is to display a choice link that alters its display depending on whether we have already visited the kitchen, and then send us there when clicked.

Code Explanation
  • + means itā€™s a choice (at a depth of 1).
  • The {kitchen_door} portion gives the label ā€œkitchenā€ to this line, which means we can jump to the section later (like weā€™re forcing the choice) or check to see how many times itā€™s been chosen using %.page_name.kitchen_door.
  • Text within [ ] brackets doesnā€™t get shown on the next page, but text outside the brackets does. (Itā€™s slightly more complicated than that, but you can check that out here.) So this is preventing the choice text from being displayed after the choice is selected.
  • The {: :} condition text block takes two or three parameters separated by ::. The first is the condition to check, the second is the text to display if itā€™s true, and the third is the text to display if itā€™s false. False conditions wonā€™t display any text if there is no third parameter.
  • The #.kitchen condition is checking how many times the page ā€œkitchenā€ has been visited. In this instance we only care if itā€™s more than 0.
  • ;; splits a line, just like how using a ; in JS lets you do multiple commands on one line. In this instance, the following portion of the line is actually the result of clicking the choice, since itā€™s part of the choice script block. I admit that part is confusing, but itā€™s basically saying + Choice;; result is the same as:
    + Choice
        result
    
  • >> #kitchen jumps to the page named ā€œkitchenā€. # is for pages and % is for labels.

But thatā€™s really as complex as youā€™d ever get in a normal story (or at least thatā€™s the most complex my Monster demo ever got). The rest is all just normal choices and if/else statements.

To follow up to the thought I had yesterday, I decided to do a side-by-side comparison of Sadako and other hypertext fiction engines so you can kind of see how it works and which are better in which areas. (Iā€™ll add this to github later, but I figured here would be fine for now.)

These produce almost exactly the same output, with the exception that Sadako and Twine can do links and ChoiceScript and Ink canā€™t.

You can see a demo of this game running here so you can understand the code better: https://tayruh.github.io/example/index.html

Sadako
## start
	[:& $.on_floor = true:]
	>> #room


## room
    You're in a dimly lit room.
    It's really dark in here. You can see that the windows are covered in cardboard with light just barely seeping out from the corners; not enough to light the room. <>
    In the corner of the room seems to be a [:body @: large mass:] of some sort.
    Now that your eyes have adjusted to the darkness, you spot what appears to be a [:mop:] laying barely within reach. :: #.room > 1 && !%.mop.take_mop

    + [Examine the mass.];; >> #body
    + [Examine the mop.] :: #.room > 1 && !%.mop.take_mop;; >> #mop


## body
    ~ if ($.on_floor)
        The mass is on the other side of the room. You'll have to get up in order to reach it.
        ++ {stand} [Get up.]
            ~~~ if (%.body.stand === 1)
                You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.
                Either way, you're going to need assistance to stand.
            ~~~ else
                You're not getting up on your own. Maybe there's something around in the darkness that can help support your weight.
            +++ [Go back.];; >> #room
        ++ [Go back.];; >> #room
    ~ else
        As you hobble your way to lump on the floor, it becomes increasingly clear that what you're looking at is a body. Are they dead, or just unconscious?

        ++ [Poke it.]
            You gently poke the body with the end of the mop. It stirs slightly.
            +++ [Poke it again.];; >> chat
    - << END

    = chat
    The person rolls over and reveals itself to be a young woman. She groans.
    + "Are you alright?"
        It takes her a moment to respond. "I think so..?" <>
    + "Hey. Get up."
        "Ugh. Hold on," she groans back at you.
    - She pushes herself up just enough to rest on her elbow and surveys her surroundings. "Where are we?"
    + "I don't know. What do you remember?"
    + "I can't remember anything."
    - "I remember.. falling."
    <b><i>End of Demo</i></b>


## mop
    It's no wonder you didn't see the mop at first. Only the head of it manages to escape the darkness thanks to the smallest ray of light escaping the masked windows.
    + {take_mop} [Take the mop.]
        [:& $.on_floor = false:]
        Laying on your side, you reach out as far as you can and manage to grab the head of the mop. You pull it towards yourself.
        You remove the mop head the handle and brace the handle against the floor. You pull and then push down on the handle as you unsteadily rise to your feet. You continue to rest against it as a makeshift cane.
        ++ [Go back.];; >> #room
    + [Go back.];; >> #room
Twine (SugarCube)
:: Start
<<set $on_floor = true>>
<<goto "room">>


:: room
You're in a dimly lit room.

It's really dark in here. You can see that the windows are covered in cardboard with light just barely seeping out from the corners; not enough to light the room. \
In the corner of the room seems to be a [[large mass|body]] of some sort.\
<<if visited() > 1 && !visited("take_mop")>>

Now that your eyes have adjusted to the darkness, you spot what appears to be a [[mop]] laying barely within reach.
<</if>>\

<ul>
<li>[[Examine the mass.|body]]</li>
<<if visited() > 1 && !visited("take_mop")>>\
<li>[[Examine the mop.|mop]]</li>
<</if>>\
</ul>

:: body
<<if $on_floor>>\
The mass is on the other side of the room. You'll have to get up in order to reach it.

* [[Get up.|stand]]
* [[Go back.|room]]
<<else>>\
As you hobble your way to lump on the floor, it becomes increasingly clear that what you're looking at is a body. Are they dead, or just unconscious?

* [[Poke it.|poke]]
<</if>>


:: poke
You gently poke the body with the end of the mop. It stirs slightly.

* [[Poke it again.|chat]]


:: chat
The person rolls over and reveals itself to be a young woman. She groans.

* [['"Are you alright?"'|chat_2][$choice = "ask alright"]]
* [['"Hey. Get up."'|chat_2][$choice = "get up"]] 

:: chat_2
<<if $choice === "ask alright">>\
"Are you alright?"

It takes her a moment to respond. "I think so..?" \
<<else>>\
"Hey. Get up."

"Ugh. Hold on," she groans back at you.

<</if>>\
She pushes herself up just enough to rest on her elbow and surveys her surroundings. "Where are we?"

* [['"I don\'t know. What do you remember?"' |chat_3][$choice = "don't know"]]
* [['"I can\'t remember anything."' |chat_3][$choice = "can't remember"]]

:: chat_3
<<if $choice === "don't know">>\
"I don't know. What do you remember?"
<<else>>\
"I can't remember anything."
<</if>>\

"I remember.. falling."

<b><i>End of Demo</i></b>


:: stand
<<if visited() === 1>>\
You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.

Either way, you're going to need assistance to stand.
<<else>>\
You're not getting up on your own. Maybe there's something around in the darkness that can help support your weight.
<</if>>\

* [[Go back.|room]]


:: mop
It's no wonder you didn't see the mop at first. Only the head of it manages to escape the darkness thanks to the smallest ray of light escaping the masked windows.

* [[Take the mop.|take_mop]]
* [[Go back.|room]]

:: take_mop
<<set $on_floor = false>>\
Laying on your side, you reach out as far as you can and manage to grab the head of the mop. You pull it towards yourself.

You remove the mop head the handle and brace the handle against the floor. You pull and then push down on the handle as you unsteadily rise to your feet. You continue to rest against it as a makeshift cane.

* [[Go back.|room]]
Ink
VAR on_floor = true
--> room


=== room ===
    You're in a dimly lit room.
    It's really dark in here. You can see that the windows are covered in cardboard with light just barely seeping out from the corners; not enough to light the room. <>
    In the corner of the room seems to be a large mass of some sort.
    {room > 1 && !mop.take_mop:
        Now that your eyes have adjusted to the darkness, you spot what appears to be a mop laying barely within reach.
    }
    
    + [Examine the large mass.] -> body
    + {room > 1 && !mop.take_mop} [Examine the mop.] -> mop


=== body ===
    {on_floor:
        The mass is on the other side of the room. You'll have to get up in order to reach it.
        ++ (stand) [Get up]
            {stand == 1:
                You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.
                Either way, you're going to need assistance to stand.
            - else:
                You're not getting up on your own. Maybe there's something around in the darkness that can help support your weight.
            }
            +++ [Go back.] -> room
        ++ [Go back.] -> room
    - else:
        As you hobble your way to lump on the floor, it becomes increasingly clear that what you're looking at is a body. Are they dead, or just unconscious?
       ++ [Poke it.]
            You gently poke the body with the end of the mop. It stirs slightly.
            +++ [Poke it again.] -> chat
    }
    
    = chat
    The person rolls over and reveals itself to be a young woman. She groans.
    + "Are you alright?"
        It takes her a moment to respond. "I think so..?" <>
    + "Hey. Get up."
        "Ugh. Hold on," she groans back at you.
    - She pushes herself up just enough to rest on her elbow and surveys her surroundings. "Where are we?"
    + "I don't know. What do you remember?"
    + "I can't remember anything."
    - "I remember.. falling."
    <b><i>End of Demo</i></b>
    -> END


=== mop ===
   It's no wonder you didn't see the mop at first. Only the head of it manages to escape the darkness thanks to the smallest ray of light escaping the masked windows.
    + (take_mop) [Take the mop.]
        ~ on_floor = false
        Laying on your side, you reach out as far as you can and manage to grab the head of the mop. You pull it towards yourself.
        You remove the mop head the handle and brace the handle against the floor. You pull and then push down on the handle as you unsteadily rise to your feet. You continue to rest against it as a makeshift cane.
        ++ [Go back.] -> room
    + [Go back.] -> room
ChoiceScript
*create seen_room 0
*create on_floor true


*label room
*set seen_room + 1

You're in a dimly lit room.

It's really dark in here. You can see that the windows are covered in cardboard with light just barely seeping out from the corners; not enough to light the room.
In the corner of the room seems to be a large mass of some sort.

*if ((seen_room > 1) and on_floor)
  Now that your eyes have adjusted to the darkness, you spot what appears to be a mop laying barely within reach. 
*choice
  #Examine the mass.
    *goto body
  *if ((seen_room > 1) and on_floor) #Examine the mop.
    *goto mop


*label body
*if on_floor
  The mass is on the other side of the room. You'll have to get up in order to reach it.
  *choice
    #Get up.
      You struggle to stand but your legs are incredibly weak and battered. When did this happen? What happened? You can't remember anything.

      Either way, you're going to need assistance to stand.
      *choice
        #Go back.
          *goto room
    #Go back.
      *goto room
*else
  As you hobble your way to lump on the floor, it becomes increasingly clear that what you're looking at is a body. Are they dead, or just unconscious?
  *choice
    #Poke it.
      You gently poke the body with the end of the mop. It stirs slightly.
      *choice 
        #Poke it again.
          *goto chat


*label chat
The person rolls over and reveals itself to be a young woman. She groans.

*fake_choice
  #"Are you alright?"
    "Are you alright?"

    It takes her a moment to respond. "I think so..?"
    *goto chat_2
  #"Hey. Get up."
    "Hey. Get up."

    "Ugh. Hold on," she groans back at you.

    *goto chat_2

*label chat_2
She pushes herself up just enough to rest on her elbow and surveys her surroundings. "Where are we?"

*fake_choice
  #"I don't know. What do you remember?"
    "I don't know. What do you remember?"
  #"I can't remember anything."
    "I can't remember anything."

"I remember.. falling."

[[ End of Demo ]]
*finish


*label mop
It's no wonder you didn't see the mop at first. Only the head of it manages to escape the darkness thanks to the smallest ray of light escaping the masked windows.
*choice
  #Take the mop.
    *set on_floor false
    Laying on your side, you reach out as far as you can and manage to grab the head of the mop. You pull it towards yourself.
    
    You remove the mop head the handle and brace the handle against the floor. You pull and then push down on the handle as you unsteadily rise to your feet. You continue to rest against it as a makeshift cane.
    *choice
      #Go back.
        *goto room
  #Go back.
    *goto room

Edit: Added Ink and ChoiceScript versions and link to demonstration.

2 Likes

I released a new version of Sadako. Itā€™s nothing to get excited over, sadly. Just a few bug fixes and optimizations. One bug (uncommon but easily triggered) regarding a combination of condition blocks and choices was show-stopping, so updating is highly recommended.

The new release can be found here: https://github.com/Tayruh/sadako/releases/tag/0.13.7

Iā€™m unfamiliar with twine, choicescript, ink, undum, etc. except for the names, but, yours is so very punctuation heavy. It reminds me of Perl.

It feels like you could completely drop all colons from your syntax everywhere and it would still be unambiguous to the human. Double colons become single colons ofc.

I dunno, just thinking out loud.

1 Like

The majority of the tokens that I chose were because you can embed javascript right into your story script and I didnā€™t want them to clash with common JS. JS uses colons everywhere, but two colons in a row would be a syntax error, therefore making it a good token to use for my language.

Althoughā€¦ A lot has changed since I first created the engine. It used to do a wide sweep in replacing or executing token blocks, but that has since been overhauled and it does pairing of open/close tokens to ignore token blocks inside of token blocks now. In other words, the original reason for the odd choices of tokens may not exist anymore. Hmmmā€¦ :thinking:

1 Like

I donā€™t know. After experimenting some, Iā€™m not that convinced. For example, hereā€™s the original example code (that prints ā€œMy name is Bob. Iā€™m a/an young/old boy/girl/man/womanā€ based on age and gender). This code was chosen because itā€™s probably the worst offender that Iā€™ve written.

    [:&
        _.name_check = "[:= '<:character::' + $.name + ':>':]";
        $.age = 30;
        $.gender = "male";
        $.name = "Bob";
    :]

    My name is _:name_check. I'm a{:$.age > 25::n old:: young:} {:$.gender=="female"::{:$.age < 18::girl::woman:}::{:$.age < 18::boy::man:}:}.

I donā€™t want to change the double colons because it avoids conflicts with JS and is also easier to see the separation of arguments at a glance. I could change {: to {{, [: to [[, and <: to <<. Then itā€™d look like this:

    [[&
        _.name_check = "[[= '<<character::' + $.name + '>>']]";
        $.age = 30;
        $.gender = "male";
        $.name = "Bob";
    :]

    My name is _:name_check. I'm a{{$.age > 25::n old:: young}} {{$.gender=="female"::{{$.age < 18::girl::woman}}::{{$.age < 18::boy::man}}}}. 

In some cases itā€™s easier to read but in others itā€™s more difficult. The }}}} part is especially nasty. I think :}:} looks better because you can easily count how many tokens that is without your eyes crossing. Not mention that ]] could be triggered by JS easily. I know I said the JS was walled off, but thatā€™s not entirely true since you can write it directly into the text with My name is $:person[ids[0]].name..

I guess to summarize, what Iā€™m saying is that my code may look a bit ugly at first glance, but I didnā€™t choose the tokens at random. I really did try to weigh aesthetics vs function, and unfortunately function won out in some cases, but I really do think it works best this way. Iā€™m sorry that itā€™s such an immediate turn off to everyone.

Overall, I must agree with Ron that Sadakoā€™s punctuation looks weird. It seems to me that youā€™re forced to sacrifice readability in order to guarantee that JavaScript can be safely embedded in Sadako.

But, in light of that, itā€™s not clear to me who Sadakoā€™s target audience is.

Is it intended for authors who are already comfortable with JS? In my experience, IF authors who are comfortable with a general-purpose programming language (and JS, specifically) typically prefer not to use off-the-shelf IF platforms, but instead prefer to implement their own platform, just like you did.

Developing a work of choice-based IF is often a novice programmerā€™s second program, literally right after ā€œhello world.ā€ Itā€™s one of the recommended projects in JavaScript for Kids for Dummies. (Chapter 16: Choose Your Own Adventure)

On the other hand, if JS developers arenā€™t your target audience, then sacrificing readability in order to support embedding JS doesnā€™t really make sense to me.

I wonder if the opposite approach wouldnā€™t make more sense: embed a DSL in JS template strings. Tagged template literals can do some pretty fancy stuff. They require no build step at all on modern browsers, and can be transpiled if IE11 support is required.

import kayako from 'kayako'

let playerName = "Passepartout"

kayako`
LONDON, 1872
-> london

=== london ===
Monsieur ${await fetch('./monsieur-name.txt')} returned home early from the Reform Club, and in a new-fangled steam-carriage, besides!  
"${playerName}," said he. "We are going around the world!"

+ "Around the world, Monsieur?"
    I was utterly astonished. 
    -> astonished
+ [Nod curtly.] -> nod
`

A related idea: take a look at Raconteur by @Sequitur.

Itā€™s based on Undum by Ian Millington.

Raconteur is based on CoffeeScript and CommonJS, but if I were implementing Raconteur today, I would use ES2020 and ESM. (And it might be based on Ink instead of Undum, for that matter.)

Youā€™re probably right in that a full rewrite is the only way to get people to actually use it. I guess thatā€™s just how it is. But I donā€™t think I have it in me to start over, so I guess Iā€™ll just end up being itā€™s only user.