Several projects I’ve been considering lately require Inform 7 to produce behavior typical of storylets. I am not a talented builder of code, but the problem’s been bothering me long enough that I’ve created a small prototype to test an implementation of storylets, which I have included below.
I’m sure I’ve made this more complicated than I need to—it feels redundant to have a bunch of storylet things AND a corresponding bunch of storylet scenes. Similarly, the process for evaluating whether each storylet is eligible for play is just a big “if” tree, and I suspect that there’s a better way to do this, so I’m turning to you. How can I improve on this implementation? What have I overlooked?
"Let There Be Storylets" by Jason Love
When play begins: say "Let's see if we can prototype some storylets, shall we?"
Detailing is an action out of world. Understand "DETAILS" as detailing.
Carry out detailing: say "[details-explainer]."
To say details-explainer:
say "This game models storylets as things which are moved into the Storylet Room if they're eligible for play, or into the Disqualified room if they should not currently occur. At the end of every turn, the Sortition rule evaluates each storylet's 'eval' property to decide whether to move it to Disqualified, or whether to move all other storylets to Disqualified instead. To hear a detailed report during storylet sortition, say 'XYZZY' to toggle those descriptions on and off".
The testerflag is a number that varies. The testerflag is 0.
Xyzzying is an action out of world. Understand "XYZZY" as xyzzying.
Carry out xyzzying:
if testerflag is 0:
say "Enabling evaluation descriptions...";
now the testerflag is 1;
otherwise:
say "Disabling evaluation descriptions...";
now the testerflag is 0.
Volume 1 - Storylets Machinery
A storylet is a kind of thing. A storylet has a number called the priority. A storylet has some text called the process. The process of a storylet is usually "An undescribed storylet occurs." A storylet has some text called the eval. The eval of a storylet is usually "[eval0]".
To say eval0:
say "This storylet still has the default evaluation process! This needs to be corrected".
Alpha is a storylet. The process of Alpha is "Storylet 'Alpha' has occurred: if the bulb did not burn out, this storylet should occur 50% of the time. Otherwise, it should occur 37.5% of the time.". The eval of Alpha is "[Alphaeval]".
To say Alphaeval:
if Alpha is in the Storylet Room:
if the switch is switched on:
if the testerflag is 1:
say "Alpha cannot run while the switch is on.";
move Alpha to Disqualified;
otherwise:
if the bulb is strong:
if a random chance of 1 in 2 succeeds:
if the testerflag is 1:
say "Alpha succeeded in its random chance of occurring, so no other storylet will occur.";
repeat with K running through storylets:
move K to Disqualified;
move Alpha to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Alpha failed in its random chance of occurring.";
move Alpha to Disqualified;
otherwise:
if a random chance of 3 in 4 succeeds:
if a random chance of 1 in 2 succeeds:
if the testerflag is 1:
say "Alpha succeeded in its random chance of occurring, so no other storylet will occur.";
repeat with K running through storylets:
move K to Disqualified;
move Alpha to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Alpha failed in its random chance of occurring.";
move Alpha to Disqualified;
otherwise:
if the testerflag is 1:
say "Alpha failed in its random chance of occurring.";
move Alpha to Disqualified.
Bravo is a storylet. The process of Bravo is "Storylet 'Bravo' has occurred: if the bulb did not burn out, this storylet should occur 33% of the time. Otherwise, it should occur 25% of the time.". The eval of Bravo is "[Bravoeval]".
To say Bravoeval:
if Bravo is in the Storylet Room:
if the switch is switched on:
if the testerflag is 1:
say "Bravo cannot run while the switch is on.";
move Bravo to Disqualified;
otherwise:
if the bulb is strong:
if a random chance of 1 in 2 succeeds:
if the testerflag is 1:
say "Bravo succeeded in its random chance of occurring, so no other storylet will occur.";
repeat with K running through storylets:
move K to Disqualified;
move Bravo to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Bravo failed in its random chance of occurring.";
move Bravo to Disqualified;
otherwise:
if a random chance of 3 in 4 succeeds:
if a random chance of 1 in 3 succeeds:
if testerflag is 1:
say "Bravo succeeded in its random chance of occurring, so no other storylet will occur.";
repeat with K running through storylets:
move K to Disqualified;
move Bravo to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Bravo failed in its random chance of occurring.";
move Bravo to Disqualified;
otherwise:
if the testerflag is 1:
say "Bravo failed in its random chance of occurring.";
move Bravo to Disqualified.
Charlie is a storylet. The process of Charlie is "Storylet 'Charlie' has occurred: if the bulb did not burn out, this storylet should occur 17% of the time. Otherwise, it should occur 12.5% of the time." The eval of Charlie is "[Charlieeval]".
To say Charlieeval:
if Charlie is in the Storylet Room:
if the switch is switched on:
if the testerflag is 1:
say "Charlie cannot run while the switch is on.";
move Charlie to Disqualified;
otherwise:
if the testerflag is 1:
say "Charlie is the default storylet, which only runs if every other storylet fails its conditional evaluation."
Illumination is a storylet. The process of Illumination is "Storylet 'Illumination' has occurred: this storylet is guaranteed to occur the first time the light is switched on, and 50% of the time while it remains on.". The eval of Illumination is "[Illuminationeval]".
To say Illuminationeval:
if Illumination is in the Storylet Room:
if the switch is switched on:
if the bulb is strong:
if the switch is not experienced:
if the testerflag is 1:
say "Illumination is guaranteed to occur while the light is on but this storylet has never occurred.";
repeat with K running through storylets:
move K to Disqualified;
move Illumination to the Storylet Room;
now the switch is experienced;
otherwise:
if a random chance of 1 in 2 succeeds:
if the testerflag is 1:
say "Illumination succeeded in its random chance of occurring.";
repeat with K running through storylets:
move K to Disqualified;
move Illumination to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Illumination failed in it is random chance of occurring.";
move Illumination to Disqualified;
otherwise:
if the testerflag is 1:
say "Illumination cannot occur while the bulb is burned out.";
move Illumination to Disqualified;
otherwise:
if the testerflag is 1:
say "Illumination cannot occur while the switch is off.";
move Illumination to Disqualified.
Burnout is a storylet. The process of Burnout is "Storylet 'Burnout' has occurred: this is the storylet occurs 50% of the time while the light is illuminated and not yet burned out. Once it occurs, it never happens again.". The eval of Burnout is "[Burnouteval]".
To say Burnouteval:
if Burnout is in the Storylet Room:
if the switch is switched on:
if the bulb is strong:
if the switch is not experienced:
if the testerflag is 1:
say "Burnout cannot occur while the lightswitch is on but Illumination hasn't happened yet.";
move Burnout to the Disqualified;
otherwise:
if a random chance of 1 in 2 succeeds:
if the testerflag is 1:
say "Burnout succeeded in its random chance of occurring.";
repeat with K running through storylets:
move K to Disqualified;
move Burnout to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Burnout cannot occur while the bulb is already burned out.";
move Burnout to Disqualified;
otherwise:
if the testerflag is 1:
say "Burnout cannot occur while the light switch is off.";
move Burnout to Disqualified.
Deadlight is a storylet. The process of Deadlight is "Storylet 'Deadlight' has occurred: this is the storylet that plays while the light switch is turned on, but the bulb is burned out.". The eval of Deadlight is "[Deadlighteval]".
To say Deadlighteval:
if Deadlight is in the Storylet Room:
if the switch is switched on:
if the bulb is strong:
if the testerflag is 1:
say "Deadlight cannot occur while the light bulb has not yet burned out.";
move Deadlight to Disqualified;
otherwise:
if the testerflag is 1:
say "Deadlight is the ONLY storylet that can occur while the light bulb is burned out and the switch is on.";
repeat with K running through storylets:
move K to Disqualified;
move Deadlight to the Storylet Room;
otherwise:
if the testerflag is 1:
say "Deadlight cannot occur while the switch is off.";
move Deadlight to Disqualified.
BulbInMemoriam is a storylet. The process of BulbInMemoriam is "Storylet 'Bulb in Memoriam' has occurred: once the bulb has burned out and the light switch is off, this storylet occurs 25% of the time.". The eval of BulbInMemoriam is "[BulbInMemoriameval]".
To say BulbInMemoriameval:
if BulbInMemoriam is in the Storylet Room:
if the switch is switched on:
if the testerflag is 1:
say "BulbInMemoriam cannot occur while the switch is on.";
move BulbInMemoriam to Disqualified;
otherwise:
if the bulb is strong:
if the testerflag is 1:
say "BulbInMemoriam cannot occur if the bulb has never burned out.";
move BulbInMemoriam to Disqualified;
otherwise:
if a random chance of 1 in 4 succeeds:
if the testerflag is 1:
say "BulbInMemoriam succeeded in its random chance of occurring, so no other storylet will occur.";
repeat with K running through storylets:
move K to Disqualified;
move BulbInMemoriam to the Storylet Room;
otherwise:
if the testerflag is 0:
say "BulbInMemoriam failed in its random chance of occurring.";
move BulbInMemoriam to Disqualified.
sceneAlpha is a recurring scene. sceneAlpha begins when Alpha is in the Storylet Room. sceneAlpha ends when the time since sceneAlpha began is 1 minute.
When sceneAlpha begins:
say the process of Alpha.
sceneBravo is a recurring scene. sceneBravo begins when Bravo is in the Storylet Room. sceneBravo ends when the time since sceneBravo began is 1 minute.
When sceneBravo begins:
say the process of Bravo.
sceneCharlie is a recurring scene. sceneCharlie begins when Charlie is in the Storylet Room. sceneCharlie ends when the time since sceneCharlie began is 1 minute.
When sceneCharlie begins:
say the process of Charlie.
sceneIllumination is a recurring scene. sceneIllumination begins when Illumination is in the Storylet Room. sceneIllumination ends when the time since sceneIllumination began is 1 minute.
When sceneIllumination begins:
say the process of Illumination.
sceneBurnout is a recurring scene. sceneBurnout begins when Burnout is in the Storylet Room. sceneBurnout ends when the time since sceneBurnout began is 1 minute.
When sceneBurnout begins:
now the bulb is dead;
say the process of Burnout.
sceneDeadlight is a recurring scene. sceneDeadlight begins when Deadlight is in the Storylet Room. sceneDeadlight ends when the time since sceneDeadlight began is 1 minute.
When sceneDeadlight begins:
say the process of Deadlight.
sceneBulbInMemoriam is a recurring scene. sceneBulbInMemoriam begins when BulbInMemoriam is in the Storylet Room. sceneBulbInMemoriam ends when the time since sceneBulbInMemoriam began is 1 minute.
When sceneBulbInMemoriam begins:
say the process of BulbInMemoriam.
This is the sortition rule:
repeat with G running through storylets:
move G to the Storylet Room;
repeat with item running through storylets:
if testerflag is 1:
say "Now evaluating whether [item] is the next storylet to play...";
say the eval of item;
if a storylet is in the Storylet Room:
let winner be a random storylet in the Storylet Room;
if testerflag is 1:
say "Conclusion: [winner] is the next storylet to play, out of a total of [number of storylets in the Storylet Room] possibilities.";
repeat with K running through storylets:
move K to Disqualified;
move winner to the Storylet Room;
otherwise:
say "No storylet was permitted to be the next storylet! This should be impossible!".
The sortition rule is listed last in the every turn rules.
Volume 2 - Rooms
Book 1 - Storylet Room
The Storylet Room is a room with description "This is the storylet room. In this basic storylet implementation, we should have one of three random storylets happening every turn (at varying probability levels), unless the player turns on the light, in which case the illumination storylet occurs. After the first turn of illumination, there should be a 50/50 chance of the light burning out, triggering a different storylet immediately and adding a fourth storylet to the random rotation. (To get more details about the specific implementation, say 'DETAILS'.)".
The bulb is in the Storylet Room. The bulb is fixed in place. The bulb can be strong or dead. The bulb is strong.
The switch is a device in the Storylet Room. The switch is fixed in place. The switch is switched off.
The switch can be experienced. The switch is not experienced.
Book 2 - Disqualified
Disqualified is a room. Disqualified is east of the Storylet Room.
Well well well… perhaps I do not understand what you are trying to do, but why not just roll 1D100 to choose which storylet will run ? In this game, Graham Nelson shows us how to implement such a mechanism (Go here to see the code). With a dice with 100 faces, isn’t it easier to manage % probabilities ? Sorry if my understanding is wrong.
Oops clicked too fast on the reply button, sorry. In the game mentionned above, the author proposes also a method to stock dicerolls for future purposes and to apply modifiers. Seems appropriate to control dependencies and cross-impacts between the rolls and the chance to a storylet to occur.
Interesting idea! I think the way I’d approach this would be:
Storylets are concepts (a new type of object which is not a thing), which means there can be relations between them
There’s a storylet-based rulebook that decides whether a particular storylet should be available or not, with a default rule checking some basic attributes and relations, but other rules can be added to customize this
There’s an activity on storylets that’s run when one is chosen, letting authors add before, for, and after rules to it
Ah, I realize now thanks to @Draconis that the question is more complex. I asked myself a bit the same question a few days ago, to manage conditional triggers of scenes and their interactions. I just understood that a scene could be a special case of storylet, hence my off-topic answer.
I think to use a table system in which I reference the scenes as well as their numerical thresholds of triggering, locking or definitive deactivation, evaluated each turn. The scenes managed in this way, as well as the actions of the player outside these scenes, can have the consequence of influencing the variables whose threshold crossing is evaluated. I want to do this to force myself to model the parameters that control the direction and the rhythm of the progression of the story. Of course, one of the parameters of the table could be based on a probability of triggering rather than a certainty.
Not tested yet because I am in the process of making my code cleaner with regard to the action processes. Perhaps a not-so-efficient method.
It may not be obvious from the prototype source, but this was actually the storylet behavior I started from: if none of the storylets had custom evaluation behavior during sortition, the game would have just chosen one at random. In this scenario, if we have 100 storylets in the game, it would behave much like rolling 1D100 and choosing a storylet from the result.
Since that part made intuitive sense to me, I wanted to figure out how to model weighted probabilities, conditional validity, and criteria that guarantee one specific storylet will play next. This isn’t a very exhaustive list of possibilities, but it felt like a good jumping-off point—now that I’ve gone through this process, I have a better idea of how I might approach creating quality-based narrative in an Inform 7 game.
Thank you for the links, though, and I look forward to hearing more about how your table-based approach works out!
@Draconis, I appreciate the suggestions. I need to revisit Inform’s documentation with these ideas in mind!
Thank you for your indulgence. In fact, I may not be far from a first naive prototype, because I use a table-based conversation system, including variables representing qualitative data: is the conversation friendly, if so to what extent? Is the subject taboo, if so with whom and in which region? Is the player empathetic, does he inspire confidence? Is he aware of such and such a clue? Does the interlocutor have a subject that he is passionate about? …
While writing these words, I realize that last night, I coded a series of sentences that are calculation formulas, some with probabilities, to weight these parameters and trigger this or that piece of conversation. I would have to apply this to the scenes. Disadvantage of the method however: a fairly substantial Every turn rule in the end, when it comes to managing scenes.
My little experiment of the evening, to explore some principles of what can be done in terms of storylet management with tables in a QBN approach. This is not optimized code, just a little personal research on the advantages and constraints of such a method. I tested most of the action orders but probably not all. I will let all this mature in my head.
"Quick very dirty tiny-QBN experience with tables and ghosts" by Monsieur HUT
After printing the banner text:
say "The Wacky the dog dog leaves the location when the player enters the area holding a stick. Its location alternates between the abandoned garden and the woods. The cat enters the abandoned garden if the Wacky the dog hasn’t been in the garden for 3 turns and if both of its cork toys are present. The cat leaves the garden and becomes untraceable if either of the previous conditions is no longer met. The gardener’s ghost enters the abandoned garden if the cat hasn’t been there for at least four turns (as he’s allergic to cats) and if 3 alcoholic beverages are placed on the table in the abandoned garden. The gardener’s ghost leaves the abandoned garden if either of the previous conditions is no longer met. The abandoned garden is cleaned if the gardener’s ghost remains there for 2 consecutive turns and the Wacky the dog dog isn’t present during this time. Finally, the Grey Lady’s specter enters the abandoned garden if it has been cleaned for 10 turns and her cat is there.";
say "[line break]";
The village square is a room.
Your house is north of the village square.
The wood is north of your house.
The main street is east of the village square.
The abandoned garden is east of the main street.
The hideout is a room.
A room can be clean or dirty.
The abandoned garden is dirty.
A stick is in the wood.
A toy is a kind of thing.
A cork mouse is a toy in the main street.
A cork butterfly is a toy in your house.
A bottle is a kind of thing.
A bottle of cider is a bottle in your house.
A bottle of wine is a bottle in your house.
A bottle of beer is a bottle in your house.
A small wrought iron table is a supporter in the abandoned garden.
Understand "table" as a small wrought iron table.
The Grey Lady's specter is a person.
The Old Gardener's ghost is a person.
Wacky the dog is a person in the abandoned garden.
Sneaky the cat is a person in the village square.
A storylet is a kind of object.
A storylet has a text called stl-event.
A storylet has a number called Qual01.
A storylet has a number called Trigger01.
A storylet has a number called Qual02.
A storylet has a number called Trigger02.
A storylet has a truth state called LogicalAnd.
Storylet01, Storylet02, Storylet03, Storylet04, Storylet05, Storylet06, Storylet07, and Storylet08 are storylets.
Table of Story-Storylets
Storylets stl-event Qual01 Trigger01 Qual02 Trigger02 LogicalAnd
Storylet01 "[dog-leaves]" 0 1 0 0 false
Storylet02 "[dog-comes]" 0 1 0 0 false
Storylet03 "[cat-leaves]" 0 1 0 1 false
Storylet04 "[cat-comes]" 0 3 0 2 true
Storylet05 "[gardener-leaves]" 0 1 0 1 false
Storylet06 "[gardener-comes]" 0 4 0 3 true
Storylet07 "[clean-garden]" 0 2 0 2 true
Storylet08 "[lady-comes]" 0 10 0 1 true
When play begins:
set storylets initial settings;
To set storylets initial settings:
repeat with LocalStorylet running through storylets:
choose row with a Storylets of LocalStorylet in the Table of Story-Storylets;
now the stl-event of LocalStorylet is stl-event entry;
now the Qual01 of LocalStorylet is Qual01 entry;
now the Trigger01 of LocalStorylet is Trigger01 entry;
now the Qual02 of LocalStorylet is Qual02 entry;
now the Trigger02 of LocalStorylet is Trigger02 entry;
To say dog-leaves:
say "As you enter the place, the dog, noticing the stick in your hand, quickly scurries away, keeping several meters between you. Yet, you have never struck anyone. He flees towards the village square.[line break]";
move Wacky the dog to the wood;
To say dog-comes:
say "As you enter the place, the dog, noticing the stick in your hand, quickly scurries away, keeping several meters between you. Again ! He flees towards the abandoned garden.[line break]";
move Wacky the dog to the abandoned garden;
To say cat-leaves:
if (Sneaky the cat is in the abandoned garden):
say "Suddenly, Sneaky the cat passes between your feet and rushes to head one of its favorite unknown places.[line break]";
move Sneaky the cat to the hideout;
To say cat-comes:
if (Sneaky the cat is not in the abandoned garden):
say "Sneaky the black cat calmly enters the garden. He lies down in the grass and begins to play with his cork mouse.[line break]";
move Sneaky the cat to the abandoned garden;
To say gardener-leaves:
if (the Old Gardener's ghost is in the abandoned garden):
say "The ghost of the gardener begins to sneeze violently, scattering ethereal droplets in a bluish halo. 'Cursed cat!' he rages, before leaving this place. 'Sure it's roaming in the area'[line break]";
move the Old Gardener's ghost to the hideout;
To say gardener-comes:
if (the Old Gardener's ghost is not in the abandoned garden):
say "The gardener's ghost enters the garden, whistling with a rake over his shoulder. He starts tidying up the garden, casting frequent glances at the bottles of wine he can no longer drink.[line break]";
move the Old Gardener's ghost to the abandoned garden;
To say clean-garden:
say "The garden now appears completely tended: the lawn is mowed, the hedges are trimmed, and the leaves are gathered. A faint scent of camphor lingers in the air.[line break]";
now the abandoned garden is clean;
reset Qual01 of storylet07;
reset Qual02 of storylet07;
say gardener-leaves;
repeat with LocalBottle running through bottles:
move LocalBottle to the hideout;
To say lady-comes:
say "A breeze stirs as a spectral figure with a slender silhouette drifts into the space, subtly gliding between the plants. It’s the Grey Lady, who rarely graces the villagers with her unsettling yet benevolent presence.[line break]";
end the story.
Every turn (this is the Every turn - tweaking triggers rule):
generate impact of carrying a stick;
generate impact of the location of the dog;
generate impact of toys in the garden;
generate impact of the location of the cat;
generate impact of bottles in the garden;
generate impact of the location of the gardener;
if the abandoned garden is clean, increase Qual01 of Storylet08;
advance the story;
To advance the story:
repeat with LocalStorylet running through storylets:
choose row with a Storylets of LocalStorylet in the Table of Story-Storylets;
if (Trigger02 Entry is 0):
if (Qual01 Entry >= Trigger01 Entry), say stl-event entry;
otherwise:
if (LogicalAnd Entry is true):
if (Qual01 Entry >= Trigger01 Entry) and (Qual02 Entry >= Trigger02 Entry), say stl-event entry;
if (LogicalAnd Entry is false):
if (Qual01 Entry >= Trigger01 Entry) or (Qual02 Entry >= Trigger02 Entry), say stl-event entry;
To generate impact of carrying a stick:
if (the player is carrying a stick) and (Wacky the dog is in the location):
if the location is the abandoned garden, increase Qual01 of Storylet01;
if the location is the wood, increase Qual01 of Storylet02;
otherwise:
reset Qual01 of Storylet01 and Storylet02;
To generate impact of the location of the dog:
if Wacky the dog is in the abandoned garden:
reset Qual01 of Storylet04;
increase Qual01 of Storylet03;
otherwise:
reset Qual01 of Storylet03;
increase Qual01 of Storylet04 and Storylet07;
To generate impact of toys in the garden:
reset Qual02 of Storylet03 and Storylet04;
repeat with LocalToy running through toys:
if LocalToy is in the abandoned garden:
increase Qual02 of Storylet04;
otherwise:
increase Qual02 of Storylet03;
To generate impact of the location of the cat:
if Sneaky the cat is not in the abandoned garden:
reset Qual02 of Storylet08;
increase Qual01 of Storylet06;
otherwise:
reset Qual01 of Storylet06;
increase Qual01 of Storylet07;
increase Qual02 of Storylet08;
To generate impact of bottles in the garden:
reset Qual02 of Storylet05 and Storylet06;
repeat with LocalBottle running through bottles:
if LocalBottle is on the small wrought iron table:
increase Qual02 of Storylet06;
otherwise:
increase Qual02 of Storylet05;
To generate impact of the location of the gardener:
if the Old Gardener's ghost is in the abandoned garden:
increase Qual02 of Storylet07;
otherwise :
reset Qual02 of Storylet07;
To increase Qual01 of (S - Storylet):
choose row with a Storylets of S in the Table of Story-Storylets;
now Qual01 entry is (Qual01 entry + 1);
[say "[line break]increase Qual01 of [S] : [Qual01 entry][line break]";]
To increase Qual01 of (S1 - Storylet) and (S2 - Storylet):
choose row with a Storylets of S1 in the Table of Story-Storylets;
now Qual01 entry is Qual01 entry + 1;
[say "[line break]increase Qual01 of [S1] : [Qual01 entry][line break]";]
choose row with a Storylets of S2 in the Table of Story-Storylets;
now Qual01 entry is Qual01 entry + 1;
[say "[line break]increase Qual01 of [S2] : [Qual01 entry][line break]";]
To increase Qual02 of (S - Storylet):
choose row with a Storylets of S in the Table of Story-Storylets;
now Qual02 entry is (Qual02 entry + 1);
[say "[line break]increase Qual02 of [S] : [Qual02 entry][line break]";]
To increase Qual02 of (S1 - Storylet) and (S2 - Storylet):
choose row with a Storylets of S1 in the Table of Story-Storylets;
now Qual02 entry is Qual02 entry + 1;
[say "[line break]increase Qual02 of [S1] : [Qual02 entry][line break]";]
choose row with a Storylets of S2 in the Table of Story-Storylets;
now Qual02 entry is Qual02 entry + 1;
[say "[line break]increase Qual02 of [S2] : [Qual02 entry][line break]";]
To reset Qual01 of (S - Storylet):
choose row with a Storylets of S in the Table of Story-Storylets;
now Qual01 entry is 0;
[say "[line break]reset Qual01 of [S] : [Qual01 entry][line break]";]
To reset Qual01 of (S1 - Storylet) and (S2 - Storylet):
choose row with a Storylets of S1 in the Table of Story-Storylets;
now Qual01 entry is 0;
[say "[line break]reset Qual01 of [S1] : [Qual02 entry][line break]";]
choose row with a Storylets of S2 in the Table of Story-Storylets;
now Qual01 entry is 0.
[say "[line break]reset Qual01 of [S2] : [Qual02 entry][line break]";]
To reset Qual02 of (S - Storylet):
choose row with a Storylets of S in the Table of Story-Storylets;
now Qual02 entry is 0;
[say "[line break]reset Qual02 of [S] : [Qual02 entry][line break]";]
To reset Qual02 of (S1 - Storylet) and (S2 - Storylet):
choose row with a Storylets of S1 in the Table of Story-Storylets;
now Qual02 entry is 0;
[say "[line break]reset Qual02 of [S1] : [Qual02 entry][line break]";]
choose row with a Storylets of S2 in the Table of Story-Storylets;
now Qual02 entry is 0;
[say "[line break]reset Qual02 of [S2] : [Qual02 entry][line break]";]
The night brings clarity, and while this experience has been both instructive and enjoyable for me, I believe that this approach is less readable (though this could easily be mitigated with intermediate phrases) but, more importantly, less powerful than using native rulebooks.
It excels in managing conversations because the fields of conversation tables (such as those presented in the official examples, to which I’ve added numerous parameters) have the same meaning, and their use can be directly integrated into the action processing of verbs like ask or tell without making the code cumbersome. However, for the general management of trigger condition parameters for narrative progression, “QBN rules is a rulebook” will likely be a better approach, which I plan to explore in the coming days using the same example.
"Quick very dirty tiny-QBN experience with rules and ghosts" by Monsieur HUT
After printing the banner text:
say "The Wacky the dog dog leaves the location when the player enters the area holding a stick. Its location alternates between the abandoned garden and the woods. The cat enters the abandoned garden if the Wacky the dog hasn’t been in the garden for 3 turns and if both of its cork toys are present. The cat leaves the garden and becomes untraceable if either of the previous conditions is no longer met. The gardener’s ghost enters the abandoned garden if the cat hasn’t been there for at least four turns (as he’s allergic to cats) and if 3 alcoholic beverages are placed on the table in the abandoned garden. The gardener’s ghost leaves the abandoned garden if either of the previous conditions is no longer met. The abandoned garden is cleaned if the gardener’s ghost remains there for 5 consecutive turns and the Wacky the dog isn’t present during this time. Finally, the Grey Lady’s specter enters the abandoned garden if it has been cleaned for 10 turns and her cat is there.";
say "[line break]";
Part 1 - Places, people and things
The village square is a room.
Your house is north of the village square.
The wood is north of your house.
The main street is east of the village square.
The abandoned garden is east of the main street.
The hideout is a room.
A room can be clean or dirty.
The abandoned garden is dirty.
A stick is in the wood.
A toy is a kind of thing.
A cork mouse is a toy in the main street.
A cork butterfly is a toy in your house.
A bottle is a kind of thing.
A bottle of cider is a bottle in your house.
A bottle of wine is a bottle in your house.
A bottle of beer is a bottle in your house.
A small wrought iron table is a supporter in the abandoned garden.
Understand "table" as a small wrought iron table.
The Grey Lady's specter is a person.
The Old Gardener's ghost is a person.
Wacky the dog is a person in the abandoned garden.
Sneaky the cat is a person in the village square.
Part 2 - Counters
NoDog is a number that varies.
NoDog is initially 0.
NoCat is a number that varies.
NoCat is initially 10.
GardenerWorking is a number that varies.
GardenerWorking is initially 0.
CleanGarden is a number that varies.
CleanGarden is initially 0.
HowManyToys is a number that varies.
HowManyToys is initially 0.
HowManyBottles is a number that varies.
HowManyBottles is initially 0.
Part 3 - Rules
Every turn (this is the Every turn - QBN rule):
follow the dog move rulebook;
follow the cat toys rulebook;
follow the cat move rulebook;
follow the nice bottles rulebook;
follow the gardener move rulebook;
follow the garden maintenance rulebook;
follow the revelation rulebook;
The dog move rules is a rulebook.
A dog move rule:
if (the player is carrying a stick) and (the player is in the abandoned garden) and (Wacky the dog is in the location):
perform storylet01;
A dog move rule:
if (the player is carrying a stick) and (the player is in the wood) and (Wacky the dog is in the location):
perform storylet02;
A dog move rule:
if (Wacky the dog is not in the abandoned garden):
now NoDog is (NoDog + 1);
A dog move rule:
if (Wacky the dog is in the abandoned garden):
now NoDog is 0;
The cat toys rules is a rulebook.
A cat toys rule:
now Howmanytoys is (the number of toys in the abandoned garden);
The cat move rules is a rulebook.
A cat move rule:
if (Sneaky the cat is not in the abandoned garden) and (NoDog >= 3) and (HowManyToys is 2):
perform storylet03;
A cat move rule:
if (Sneaky the cat is in the abandoned garden):
if (NoDog < 3) or (HowManyToys < 2):
perform storylet04;
A cat move rule:
if (Sneaky the cat is not in the abandoned garden):
now NoCat is (NoCat + 1);
A cat move rule:
if (Sneaky the cat is in the abandoned garden):
now NoCat is 0;
The nice bottles rules is a rulebook.
A nice bottles rule:
now HowmanyBottles is (the number of bottles on the small wrought iron table);
The gardener move rules is a rulebook.
A gardener move rule:
if (The Old Gardener's ghost is not in the abandoned garden) and (NoCat >= 4) and (HowmanyBottles >= 3):
perform storylet05;
A gardener move rule:
if (The Old Gardener's ghost is in the abandoned garden):
if (NoCat < 4) or (HowmanyBottles < 3):
perform storylet06;
otherwise:
now GardenerWorking is (GardenerWorking + 1);
The garden maintenance is a rulebook.
A garden maintenance rule:
if (GardenerWorking >= 5) and (NoDog >= 5):
perform storylet07;
A garden maintenance rule:
if (the abandoned garden is clean):
now CleanGarden is CleanGarden + 1;
The revelation is a rulebook.
A revelation rule:
if (CleanGarden >= 10) and (Sneaky the cat is in the abandoned garden):
perform storylet08;
Part 4 - Storylets
To perform storylet01:
say "As you enter the place, the dog, noticing the stick in your hand, quickly scurries away, keeping several meters between you. Yet, you have never struck anyone. He flees towards the village square.[line break]";
move Wacky the dog to the wood;
To perform storylet02:
say "As you enter the place, the dog, noticing the stick in your hand, quickly scurries away, keeping several meters between you. Again ! He flees towards the abandoned garden.[line break]";
move Wacky the dog to the abandoned garden;
To perform storylet03:
say "Sneaky the black cat calmly enters the garden. He lies down in the grass and begins to play with his cork mouse.[line break]";
move Sneaky the cat to the abandoned garden;
To perform storylet04:
say "Suddenly, Sneaky the cat passes between your feet and rushes to head one of its favorite unknown places.[line break]";
move Sneaky the cat to the hideout;
To perform storylet05:
say "The gardener's ghost enters the garden, whistling with a rake over his shoulder. He starts tidying up the garden, casting frequent glances at the bottles of wine he can no longer drink.[line break]";
move the Old Gardener's ghost to the abandoned garden;
To perform storylet06:
say "The ghost of the gardener begins to sneeze violently, scattering ethereal droplets in a bluish halo. 'Cursed cat!' he rages, before leaving this place. 'Sure it's roaming in the area'[line break]";
move the Old Gardener's ghost to the hideout;
now GardenerWorking is 0;
To perform storylet07:
now the description of the abandoned garden is "The garden now appears completely tended: the lawn is mowed, the hedges are trimmed, and the leaves are gathered. A faint scent of camphor lingers in the air.[line break]";
now the abandoned garden is clean;
repeat with LocalBottle running through bottles:
move LocalBottle to the hideout;
move the Old Gardener's ghost to the hideout;
if the player is in the abandoned garden:
try looking;
To perform storylet08:
if the player is not in the abandoned garden:
say "You hear something strange from the east garden";
if the player is in the abandoned garden:
say "A breeze stirs as a spectral figure with a slender silhouette drifts into the space, subtly gliding between the plants. It’s the Grey Lady, who rarely graces the villagers with her unsettling yet benevolent presence.[line break]";
end the story.
It’s very rudimentary, but I think it is still compliant with the QBN spirit. The story progresses based on how the player is able to modify the parameters and values of the world model.
Depending on the resulting configuration, certain events are triggered, deactivated, or countered by opposing events. The player does not navigate a fixed branching model, even if they can trigger irreversible events. Thinking about this, it seems to me that the rule-based orientation of Inform 7 predisposes and encourages this approach, especially for narrative aspects.
Additional lesson for myself: go easy on the tables, much stronger on the rules.
EDIT : special thanks to @Draconis for having mentioned the rulebooks.