First, launch the game library and play the Tutorial game, which teaches you the basics of writing a Fictive.
Fictive views your interactive fiction game as a state machine. Think of a Choose-Your style book: each page in that book is a "state", and from that page, you can "transition" to other states.
Fictive is slightly different, in that the user types in their choices, which may be expressed in terms of commands you make up. This allows a lot more flexibility in both design and interaction.
You will define your state machine as a set of YAML files. YAML is a simple markup language with some powerful features that makes it an ideal choice for this kind of thing. Wikipedia is a good starting point for learning more about YAML.
When defining commands the user can enter, you will need to use regular expressions (regex). The theory is useful, as is the specifics on Python regex.
To start writing a Fictive, you must do the following:
- Create a folder inside of a game directory
- Create a
manifest.yamlfile - Create at least one other
yamlfile which is where you'll define your game.
E.g:
games/
my_new_game/
manifest.yaml
game.yaml
The Manifest provides a set of metadata about your game. It is a YAML dictionary. It should contain these keys:
title: The Title of Your Game
author: This is you!
slug: A quick synopsis of your game
files:
- a list
- of the files
- in this game
- order matters!
tests:
- a list
- of files
- containing testsThe game files will be merged into a single file, and then parsed. This is to better support YAML references across files. More on that later.
Inside of those files, you need to have an execute key, which will contain the state machine that drives your game. You may also have a state_bag key, which lets you initialize your state bag variables.
A game is made up of states and transitions.
States must have a tag and a description.
You could define a series of states like so:
my_game_states: &my_game_states # this is a YAML reference, it lets us refer to this section later
- state:
tag: start
description: The start of a game
- state:
tag: end
description: The end of a gameTransitions define how we move from each state to the next. Transitions have a condition key, which lets us define under what conditions a transition may fire. The one you'll use the most is the match condition, which uses a regex to parse user input. For example:
my_game_trans: &my_game_trans
- transition:
from: start
to: end
condition:
match: (next|go|leave)Create a folder test under games. In that folder, create a manifest.yaml, and put the following:
title: My Simple Game
author: <Your Name>
slug: A simple game to learn Fictive
files:
- main.yamlIn the same folder, add main.yaml
Copy the my_game_states and my_game_trans into it. Then add:
main_machine: &main # define your machine
startTag: start # specify where this state machine starts
endTag: end # optionally, specify where it ends
states:
- *my_game_states
transitions:
- *my_game_trans
execute: *main # tell Fictive to start the game by executing itLaunch Fictive and you should see your game in the menu. Load it and play.
If for some reason, your game doesn't work as expected- it gives you an error when you try and load the game- don't fret.
Run uv python -m fictive games -t test. That will at least give you some errors about what went wrong. The errors are not, at this time, very good. I'm sorry.
The state bag gives your game memory. User input can be recalled in future states, and we can create counters and other variables. For example, let's say you have a lever in your game. You wish to track if the user has pulled the lever or not.
lever_states: &lever_states
- state:
tag: lever_up
description: |
There is a lever here. It is up in the "on" position.
on_enter:
set:
key: lever
value: on
- state:
tag: lever_down
description: |
There is a lever here. It is in the "off" position.
on_enter:
set:
key: lever
value: offThe on_enter trigger allows you to manipulate the state bag. Here you see the use of set.
You can include state bag entries in your description (or any where else you control text output). To do so, simply reference the key in a future state.
- state:
tag: seen_lever
description: You remember putting the lever in the {lever} position.
on_enter: revert # more on this laterOnce you've set a key in an on_enter, you can check that key as part of a transition. For example, if in another state, a secret door is only available.
- transition:
from: big_room
to: secret_door
condition:
eq:
key: lever
value: onIf the user is in the big room and they hit enter, it will move them to the secret door. Otherwise, some other transition may fire.
The match condition can also add state bag entries using regex capture groups.
- transition:
from: pick_weapon
to: weapon_picked
condition:
match:
matcher: (get|take|use) (axe|sword|whip)
keys: [_, player.weapon]Each capture group can be saved as a key. The _ key is for values we don't actually want to capture. Depending on what the user entered, "axe", "sword", or "whip" will be stored in "player.weapon". Future states and transitions will be able to leverage that.
You can have an array of functions in your condition (notated in YAML by putting a - at the start of each line).
condition:
- eq:
key: key
value: value
- match: user inputAll conditions must pass for the condition to pass. This is an and operation, not an or.
Using condition: always creates a transition which will always fire when the user hits enter. Useful for breaking up text across multiple screens, and moving through game sections with no meaningful choices.
Good games use this sparingly!
The match condition can be executed with a single parameter, thusly: match: some regex
Or it accepts two parameters. The matcher and the keys. This allows you to save user input into your state bag.
match:
matcher: some regex (with capture)
keys: [captured values]Compares a key against a value, or two keys, and allows a transition to pass only if they're equal.
eq: # compare a key against a value
key: someKey
value: someValue
# or
eq: # compare a key against another key
key: someKey
other: someOtherkeyIn addition to eq, there are gt (greater than), gte (greater than or equal), lt (less than) and lte (less than or equal) functions. They operate the same way as eq, but do what the name suggests.
All comparsions will do a numeric comparison if you've stored a numeric value in the key, otherwise it's a lexical (text based) comparison.
revert takes no parameters. When use in an on_enter, this state will become a "transient"- it will print a message, but not otherwise update the game.
Example:
- state:
tag: help
description: This is a help message
on_enter: revertset takes two parameters: the key to set, and the value.
set:
key: someKey
value: someValueinc and dec can increment or decrement a single key. They only take one parameter, and can be invoked thus:
inc: someNumericKeyThese only work on numeric values. They combine well with the comparison conditions in transitions to make flexible transitions.
There are three banner functions, which control the banner displayed on the various screen areas. banner sets the text on the main state pane. subbanner sets the text on the substate pane, only visible in states with substates. Finally, transbanner sets the banner on transient states- states which were reverted. These states act as pop-up messages, useful for displaying help.
banner: My banner textThese functions do support templates, so you can access state bag variables.
banner: You Have a {player.weapon}You can create multiple machines in your game. For example here's a simple machine:
lever_machine: &lever_machine
states:
- state:
tag: lever_on
description: The lever is on.
on_enter:
set:
key: lever
value: on
- state:
tag: lever_of
description: The lever is off.
on_enter:
set:
key: lever
value: off
transitions:
- transition:
from: lever_on
to: lever_off
condition:
match: (flip|switch|lever)
- transition:
from: lever_off
to: lever_on
condition:
match: (flip|switch|lever)
startTag: lever_onNow, we could have another state, in another machine, that might look like this:
- state:
tag: lever_room
description: In the center of this room is a gigantic lever.
sub_machine: *lever_machineThis will display both the main state description and the sub state description. If you type flip, the lever will change positions. The main state may have its own transition commands.
This allows you to create mini-games and puzzles within your main game. Sub-state machines can behave just like full state machines. They just are attached to a single state. Think of it like a Choose-Your book inside a single page of a Choose-Your book.
NB: Sub-state machines and state machines share the same statebag. Be careful when naming keys, as you may end up in situations where two different parts of your game use the same keys.
It's best to split your game across multiple files, and then reference the files in your manifest.yaml. The important fact here is that those files are loaded in order, and references have to be declared (&some_name declares a reference) before they're used (*some_name links a reference).
Testing your game is important! You can create simple test scripts in YAML like so:
my_test:
- input: Some simulated user input
- assert:
- tag: some_state_tag
- eq:
key: someKey
value: someValueA test starts with a key (my_test, above) and then is an array of statements. You can have any number of test files alongside your game files in your game folder.
In your manifest, add a tests key, and specify where your test files are loaded:
tests:
- tests/file1.yaml
- tests/file2.yamlYou can have as many test scripts as you like in a single file- the key value you use (my_test above) will differentiate between them.
To run your tests, run:
uv run python -m fictive path_to_games -t your_game_folderIt's also helpful to use this command even if you haven't written any tests, because if you have errors in your YAML, this will provide better and more useful output.
This covers everything you need to know about writing Fictive games. Check the example and tutorial games out to see how they were implemented.