GemIF is a simple Interactive Fiction engine which runs as a Gemini server.
Check out my live GemIF instance with your favorite gemini client at gemini://gemif.fedi.farm or through a gemini web portal like the one at portal.mozz.us. Though be warned the web portal does terrible things to the ASCII art.
Compiled binaries for several architectures are automatically generated on each tagged commit. You can find the latest one and download it
Otherwise, you can clone this repo and use the regular golang toolchain to build (check
the Makefile
for details).
Once you have the binary compiled, you need a few files to make it run.
The first thing to do is generate your TLS cert in whatever way makes sense for you.
In production, you might use certbot in standalone mode to generate a LetsEncrypt cert.
Otherwise, since most (all?) Gemini clients use TOFU for TLS certificates, you can also feel free to generate a self-signed cert in whatever way is appropriate for you.
This repo comes with a couple of sample stories in ./stories
(also bundled as artifacts
alongside the binaries in the releases linked above). If you want to add, remove,
or change them, go ahead and do that now (see below for details on writing your own).
GemIF configures itself based on the contents of ./config.toml
.
Take sample.config.toml
and copy it. Change the configuration to match
your desired values.
That's it. Go ahead and run the binary.
$ ./bin/gemif-linux-amd64/gemif
Starting GemIF 0.1.1 (bd561ac) - built 2020.12.05.210341
[gig-debug] ⇨ gemini server started on [::]:1965
time="2020-12-05T21:29:40-05:00" path=/ status=20 duration=0.28
GemIF loads stories from YAML files (from the directory named in engine.stories_dir
in
config.toml
), but authors have the option to write their stories in GemIF format using
.gemif
files.
.gemif
files are a lighweight abstraction on top of the YAML files. Honestly it's still
not the most pleasant writing experience, but it's generally better than writing the YAML
by hand.
You can see some examples in stories/src/.
Each story directory will contain:
metadata.yml
- With story and author information.gemif
files - These files contain the rooms and exitsEach .gemif
contains one or more different rooms. Room metadata is presented as YAML
frontmatter (or, uh midmatter in the case of multiple rooms in a file) while each
description template is presented as plaintext.
# metadata.yml
---
id: simple
name: Simple Demo
description: A simple demo
author: Norm MacLennan
# room.gemif
---
room_id: inside
room_name: Inside
exits:
- exit_id: leave
exit_description: You're right, let's seize the day!
destination_id: outside
---
You're inside, but it's such a lovely day out.
---
room_id: outside
room_name: Outside
---
You made it outside. Now you can get on with your life.
THE END
Once you're ready to test your story, you can use gemifc
to "compile" it to YAML.
$ gemifc ./path/to/my/story/ ./my/compiled/stories/
Compiling story ./path/to/my/story/ to ./my/compiled/stories/
Finished loading story:
Name: Simple Demo
Author: Norm MacLennan
Descriptions: A simple demo
Number of Rooms: 2
Serializing and writing to disk...
Done!
Then you can load it up with gemif
as usual.
Whether you're writing in gemif format or right in a YAML file, here are the object specifications for the things you're building.
metadata
comes from the metadata.yml
file in gemif format or the metadata
key in YAML:
id
: is a unique id (within your instance) for the storyname
: is a human-friendly title of the storydescription
: is a brief description of the storyauthor
: a name or other identifier of the author(s)rooms
objects are described by your .gemif
files or as the rooms
object in your YAML.
Each room has:
room_id
: a story-unique id for the roomroom_name
: a human-readable nameroom_description
: a description of the room
.gemif
format this is the plaintext part of each room while the rest
of the properties live in the frontmatterexits
: an array of exit objectsEach exit contains:
exit_description
: a description of the exit (the text of the exit link)desination_id
: the id of the room this exit takes you toexit_id
: a unique id for the exit, I recommend a GUIDset_condition
: condition tag to attach to the game state if the user takes this exitif_condition
: condition tag the user must posses to use this exitnot_condition
: conditoin tag to the user must NOT posses to use this exitConditions are simple tags attached to the user's state. Condition usage will vary from story to story, and some will not require conditions at all, but you might use them to:
Conditions are set as part of using exits and can be used to control which exits a user can use in the future as well as used within room descriptions to offer conditional passages.
Speaking of which, room descriptions are rendered as golang text/template
templates with the active gamestate in scope. This allows you to perform a
little bit of logic with regards to what you display to the user, how, and
when.
The following struct is in scope within your template:
type GameState struct {
StoryID string
CurrentRoom string
Conditions []string
}
StoryID and CurrentRoom (RoomID) probably aren't very interesting, but Conditions can be used to perform conditional rendering.
GameState
offers some (currently one) convenience function to help
simplify your rendering when it comes to conditions.
ConditionMet(condition string) bool
can help you quickly tell if a user
meets a specific condition:
- room_id: the_end
room_name: The End
room_description: |
Though because this is a simple demo, you always end up in the same place.
{{- if .ConditionMet "choice_a"}}
oh cool, you made choice a! nice work.
{{- end}}
{{- if .ConditionMet "choice_b"}}
oh you made choice b, that's too bad :(
{{- end}}
The sample in the repo contains a walking tour of my home and another contains some examples of the use of conditions. But here's a simpler sample:
rooms:
- room_id: the_beginning
room_name: The Beginning
room_description: >
This is the beginning of the story!
exits:
- exit_description: This is boring, flip to the end
destination_id: the_end
exit_id: exit_a
- room_id: the_end
room_name: The End
room_description: >
And they lived happily ever after.
THE END
Hopefully between these two examples, you can get an idea of how you can build simple linear or branching stories.
I plan to continue to extend the features available to offers to add a little more flexibility, but part of the idea is that it's meant to be simple so I'm trying to be careful of how much I add.
Currently focused on refactoring and solidifying the foundation of the application so it is performant and maintainable.
MIT © Norm MacLennan