Simulating a Parser in Twine? [split from The Art of Language Agnostic Design]

I’m revisiting this thread. Specifically, I’m intrigued by the possibility that Twine can be entered in ParserComp.

As reference, ScottKit works in this format: MATCH-COND-ACTION

MATCH is matching the commands given by the parser, such as OPEN DOOR . COND is checking the necessary condition that needs to be satisfied before action is taken, such as HERE DOOR .
ACTION is the code that needs to be executed in the game to reflect changes to the world model, such as SWAP DOOR OPENDOOR .

Now, I know Twine runs on HTML/JS, which presumably has Array and conditional output. So, I have this idea (taken from RPN calculator discussions, bonus for maximal score here @mla): what if we change the order around? What if we do: COND-MATCH+ACTION?

if (HERE DOOR) 
  LINK "OPEN DOOR" {
    SWAP DOOR OPENDOOR
    "You open the door."
  }

The first thing that comes to mind is that “You can’t do that, yet.” is no longer possible, as is any invalid command. The fact that Command is only visible/clickable if the condition is satisfied, means that Authors no longer have to worry about silly, non-sense commands. That helps a lot in producing IF works quickly. (fast)

Another thing to consider is that VERBs should be limited, unless you fancy scrolling through hundreds of choices! This helps to streamline the story and focus on the puzzles. (easy)

Then there’s the matter of not duplicating commands, daemon, etc. Issues that are common to implementing Design to Platform.

But what do you get in return?

  1. Fast development time. ScottKit is fast and easy.
  2. Multimedia that is HTML+CSS+JS
  3. Portable Online and Offline publications
  4. Possibility of serial IF publications!

Bonus: Entry in ParserComp 2022! :exploding_head::grin:

2 Likes

The first thing that comes to mind is that “You can’t do that, yet.” is no longer possible, as is any invalid command.

Why not send users to an “interpreter” passage that displays them the result of their command, then send them back or forward to the desired page?

For example:

<<if $verb neq "open" and $noun neq "door">>That's not something you can do.<<endif>>

With Sugarcube’s cycle link feature, you could probably do this without the need for any coding at all beyond what the engine offers, maybe even without the need for arrays if you embed widgets within each other.

You mean something like:

<<if not $doorOpen>>\
    <<link [[Open the door|door]]>>
        <<set $doorOpen to true>>
    <</link>>
<</if>>

:: door
You open the door.
[[Okay.|previous()]]

Doesn’t that require extra clickings? It’ll get tiresome real quick, I imagine.

I think it’s better to set each page for each room (acts like a clear_screen() command ), and simply append the printed interpreter messages to existing text on the page.

But I don’t know Twine, and if you think your way is better, then that’s the way to go!

I guess you could do it with replace or append, but I am not sure offhand whether Sugarcube checks the variables unless you go to another passage.

Anyway, in your start passage, put


You see a door. What do you want to do?

<<nobr>>
<<cycle "$verb" autoselect>>
	<<option "open">>
	<<option "close">>
<</cycle>>

<<cycle "$noun" autoselect>>
	<<option "carpet">>
	<<option "door">>
<</cycle>>

[[OK|check]]

<</nobr>>

Then in a passage called check:

<<if $verb eq "open" and $noun eq "door">>You open it; now you can go to [[this passage|Start]].
<<elseif $verb eq "close" and $noun eq "door">>You open it; now you can go to [[this passage|Start]].>>
<<else>>
That's not something you can do. [[Back|Start]]
<<endif>>

You could create a separate check passage for every room, but I think it would be better to put every possible outcome in a single passage because you will have some things that overlap, especially if you try and make an inventory system that spans across rooms.

So if you create just one check passage you can put <<set $room to "first room">> at the top of each room page. Then on the check page, wrap possible outcomes like so:

<<if $room eq "first room">>
<<if $verb eq "open" and $noun eq "door">>You open it; now you can go to [[this passage|Start]].
<<elseif $verb eq "close" and $noun eq "door">>You open it; now you can go to [[this passage|Start]].>>
<<else>>
That's not something you can do. [[Back|Start]]
<<endif>>
<<endif>>

You can also build on that to create a checkpoint system…instead of sending the player back to a previous passage when a check is rejected, you can send them back to the last $room’ed passage.

To save clicking, and if you don’t mind a silent rejection, you can also get rid of the “That’s not something you can do message” and just use “goto” to send the player back instantly.

Why are you checking the VERB NOUN? Twine isn’t ScottKit. You don’t want to check the VN MATCH in Twine. You want to check the COND Variables.

My examples check the COND, instead of MATCH. Why are you checking the MATCH?

I thought you wanted all three…without the noun and verb it isn’t very parser-like, it’s just toggling variables, unless I am missing something.

I am not familiar with ScottKit. Does COND mean checking whether the door is open/closed then setting it to open/closed in this example? You could do that within the check passage too of course, more or less what you suggested above with $doorOpen.

So in the check page: <<if $verb eq "open" and $noun eq "door">><<set $doorOpen to true>>Now the door is open.<<endif>>

Then in the room page you would have the <<if $doorOpen... part that responds to that.

What are you talking about? I have all three. Just the order that’s different. Instead of ScottKit MATCH-COND-ACTION, Twine version is COND-MATCH-ACTION. You know, just like the examples show.

Checking the condition means that you don’t have to deal with bad inputs. Only good ones. You still have VN. “OPEN DOOR”. It’s there on the examples, see?

I just don’t know what to say.

I’m clearly missing something or coming at this from a different angle. Let’s leave it there.

Go simple!

if (COND) {
  link MATCH // click link to ...
  do ACTION // set variables and print some text
}

Harlowe?

(if: $doorOpen is false)[(link: OPEN DOOR)[(set: $doorOpen to true) You open the door.]]

3 posts were merged into an existing topic: The Art of Language Agnostic Design

A post was merged into an existing topic: The Art of Language Agnostic Design

Will a Twine user answer the very simple question of whether or not this works? And if not, any suggestion what will? It doesn’t matter whether it’s Harlowe or Sugarcube.

<<if not $doorOpen>>
    <<click "Open the Door">>
        <<set $doorOpen to true>>
        <<print "You open the door.">>
    <</click>>
<</if>>

I just don’t see what’s so difficult about a simple conditional clickable text that would cause so much confusion. Is conditional hyperlink actually impossible to do with Twine? Please help me because Twine doesn’t run on Raspberry Pi, which is the computer that I use. TIA.

PS: OK if you can do it with pure Javascript as well.

Edited to add print

I think I see what you are getting at now. This is for SugarCube.

You first have to set a variable in a passage titled StoryInit. Let’s make the door closed by default.

<<set $doorOpen to false>>

Now for the link… put this in your Start page:

<<link "Open the door">>
	<<if $doorOpen eq false>>
		<<set $doorOpen to true>>
	<<elseif $doorOpen eq true>>
		<<set $doorOpen to false>>
        <</if>>
<</link>>

Note that this alone with just swap the variable silently and endlessly. There are a lot of ways to update the passage—either by refreshing it, by going to a new passage, or by targeted replacement of a div.

Here is one way to make it refresh the current passage with ‘goto’ then print the $doorOpen variable. It replaces the Start passage above.


<<link "Open the door">>
	<<if $doorOpen eq false>>
		<<set $doorOpen to true>>
	<<elseif $doorOpen eq true>>
		<<set $doorOpen to false>>
    <</if>>
<<set $this to passage()>>
<<goto $this>>
<</link>>


<<if $doorOpen eq true>>The door is open/You open the door.<<else>>The door is closed.<<endif>>

2 posts were merged into an existing topic: The Art of Language Agnostic Design

Various examples of Language Agnostic Design as presented from previous posts.

A post was merged into an existing topic: The Art of Language Agnostic Design

You may want to note in the original post that this thread is about implementing a Parser for a specific Twine story format, and not for Twine story formats in general, as all the examples are SugarCube based and each Twine story format has its own level of JavaScript support.

eg. Harlowe has been deliberately designed to restrict an Author’s ability to use JavaScript to enhance the core features of its engine, or those of a project based on it.

Feel free to add Harlowe examples, or others you like. Whatever you want, buddy. Do it with Python, if you like. No one is stopping you.

As I stated, Harlowe doesn’t support using JavaScript to enhance a project, as it has no documented JavaScript API to access the internals of its engine. It also doesn’t allow using Standard JavaScript object methods (1) for value data-types like String, Number, Array or Data-map within its macro calls.

Any implementation would need to be Macros only code.

  1. unless called within a HTML <script> element, and only on values accessible within said element.

So I had a quick go at a Twine parser in Sugarcube based on the door example with three verbs; ‘look’, ‘open’ and ‘close’. This is how I would go about forming a skeleton that a real parser could be built around:

First is the main passage. This has the array ‘$objects’ that contains all the the information for the door. In a real project this ought to go in the ‘storyInit’ passage but I stuck to using just two passages total for this example:

:: Door Sim

<<silently>>

<<set $roomDescription = "You are in some kind of room. There is a door here.">>

<<set $objects = [{
	name: "door",
	description: "It's a door. The door is closed.",
	status: "closed",
	onOpen: {
		commentPos: "You open the door.",
		commentFail: "The door is already open.",
		posStatus: "closed",
		exec: '<<set $objects[0].description = "Thanks to you the door is now open.">><<set $objects[0].status = "open">>'
		},
	onClose: {
		commentPos: "You close the door.",
		commentFail: "The door is already closed.",
		posStatus: "open",
		exec:
			'<<set $objects[0].description = "Due to your heroic efforts the door is currently closed.">><<set $objects[0].status = "closed">>'
		}
}]>>

<</silently>>\
\
TWINEPARSER DOOR SIM 2022

$roomDescription

<span id="input"><<textbox "_input" "">> <<link "ENTER">><<parseInput>><</link>></span>

<div id="output"></div>

Then for the ‘parseInput’ widget (in a separate ‘widget’ tagged passage):

<<widget "parseInput">>

<<set _inputArr = _input.split(" ")>>
<<if _inputArr[0] == "help">>
	<<set _returnComment = "Valid verbs are: 'look', 'open', 'close'">>
<<elseif _inputArr[0] == "look">>
	<<if _inputArr.length == 1>>
		<<set _returnComment = $roomDescription>>
	<<else>>
		<<set _returnComment = "You can't see a " + _inputArr[1] + " in this room.">>
		<<for _i = 0; _i < $objects.length; _i++>>
			<<if _inputArr[1] == $objects[_i].name>>
				<<set _returnComment = $objects[_i].description>>
				<<break>>
			<</if>>
		<</for>>
	<</if>>
<<elseif _inputArr[0] == "open">>
	<<set _returnComment = "You can't open that!">>
	<<for _i = 0; _i < $objects.length; _i++>>
		<<if _inputArr[1] == $objects[_i].name>>
			<<if $objects[_i].status == $objects[_i].onOpen.posStatus>>
				<<print $objects[_i].onOpen.exec>>
				<<set _returnComment = $objects[_i].onOpen.commentPos>>
			<<else>>
				<<set _returnComment = $objects[_i].onOpen.commentFail>>
			<</if>>
			<<break>>
		<</if>>
	<</for>>
<<elseif _inputArr[0] == "close">>
	<<set _returnComment = "You can't close that!">>
	<<for _i = 0; _i < $objects.length; _i++>>
		<<if _inputArr[1] == $objects[_i].name>>
			<<if $objects[_i].status == $objects[_i].onClose.posStatus>>
				<<print $objects[_i].onClose.exec>>
				<<set _returnComment = $objects[_i].onClose.commentPos>>
			<<else>>
				<<set _returnComment = $objects[_i].onClose.commentFail>>
			<</if>>
			<<break>>
		<</if>>
	<</for>>
<<else>>
	<<set _returnComment = "I don't understand '" + _inputArr[0] + "'.">>
<</if>>

<<append #output>>_returnComment<br><</append>>

<</widget>>

The ‘parseInput’ widget could be expanded to include more verbs, the ‘$objects’ array fleshed out accordingly and so on. The input box and output text display would need some work to make it resemble a traditional parser engine. It’s certainly doable.

2 Likes