Ansible game making

Ever wondered what kind of games could be written with Ansible? Probably not but I did and successfully nerd sniped myself.

Reading about the history of video games I felt a sense of amazement about the creativity many people displayed while facing very harsh constraints compared to modern day tooling and technologies. At some point I began to equate the development conditions on the TRS-80 micro computer and working on non automation tasks with Ansible: no colours, no real control over text, awkward display, etc.

It doesn’t make a lot of sense now but the result is still there: I wrote a bunch of games and really enjoyed the flourishing of creativity resulting from the particular constraints I chose to impose on myself.

If you are more interested in the games than the technical explanations feel free to skip to the end.

Table of contents

  1. Interactivity
  2. Error handling
  3. Testing
  4. Games

Interactivity

It is my opinion that the very first step for something to be called a game is simply for it to be interactive, even in a minimalistic way.

Ansible is not exactly suitable for interactive tasks because its very goal is to automate, which can also be perceived as removing interactivity. Thankfully a few mechanisms are there to exploit.

vars_prompt

The interactive method most prominently documented by Ansible is the vars_prompt keyword, which allows to interactively asks the user for an input.

Here is an example of this mechanism used to implement the venerable Unix command wargames:

# wargames.yml
---
- hosts: localhost
  gather_facts: no
  connection: local
  vars_prompt:
    - name: game
      prompt: Would you like to play a game?
      private: no
  tasks:
    - stat:
        path: "./{{ game }}.yml"
      register: x
    - debug:
        msg: "ansible-playbook {{ game }}.yml"
      when: x.stat.exists
    - debug:
        msg: |
          A strange game.
          The only winning move is not to play.
      when: not x.stat.exists

There is one problem thought: this is a playbook specific keyword, meaning it can’t be used in an infinite loop.

A solution could be to have the player run ansible manually many times using --extra-vars as an input source and somewhat keep tracks of the game state in a persistent facts file.

$ ansible-playbook game.yml
...
TASK [debug]
ok: [localhost] => {
    "msg": "You find yourself in a dark, strange cave. Suddenly a hole opens up under your feet!"
}
...

$ ansible-playbook -e 'action=jump' game.yml
...
TASK [debug]
ok: [localhost] => {
    "msg": "You jumped just in time to avoid being swallowed by the earth."
}
...

While very cumbersome I initially explored this method for a while before finding a much better solution : the pause module.

pause

The pause module is used to wait for a for a certain amount of time before resuming the normal course of a series of tasks. It is useful as a sort of catch all when a system needs some preparation before being accessible but better methods like waiting for a TCP port to be opened or a text in a file being written are not possibles.

The not-really-hidden but game-changing feature is the prompt parameter allowing to wait until an input is entered. As this is a regular module it can be applied in a loop.

Here is a simple example of an infinite loop asking the player for instructions:

- hosts: localhost
  gather_facts: no
  connection: local
  tasks:
    - include_tasks: game-tasks.yml
# game-tasks.yml
---
- name: Ask player.
  pause:
    prompt: "What do you want to do?"
  register: guess

...

- include_tasks: game-tasks.yml

This combination of infinite loop and the pause module is the base layer I now use for all of my creations.

Error handling

The block keyword is very often used to group tasks together, allowing to share keywords such as become, when or tags. A lesser known feature is the counterpart rescue keyword which is used to execute tasks if an error occurred in the main block, without stopping the execution.

---
- block:
    - name: Verify user input.
      assert:
        that: action in allowed_actions
      quiet: yes

    - include_tasks: "do-{{ action }}.yml"
  rescue:
    - name: Error in user input.
      debug:
        msg: "I do not understand this strange word {{ action }}."

Note that a block can contain another block so complicated verifications steps can be imbricated.

Testing

As my games are written in Ansible I naturally reached to Molecule for testing and derived usable patterns out of the experience:

I described the first step in Running Molecule on builds.sr.ht so I am not going to discuss it much here.

Using Ansible as a verifier is logical for many reasons: there are no containers or VMs to connect to and it allows to exercise tasks directly from molecule using the include_tasks module. This is where splitting tasks is important: as Molecule messes a bit with standard input it is not exactly possible to use the pause module as described above.

Here is an example task file designed to be executable interactively by the user and automatically by molecule:

# kill-monster.yml
---
- include_tasks: kill-monster-question.yml
  when: interactive|default(yes)

- name: Check if monster takes a fatal blow.
  fail:
    msg: "Oh no, you only harmed the beast!"
  ignore_errors: "{{ interactive }}"
  when: player_damage|int < monster_life

...

- name: Loot!
  set_fact:
    gold: "{{ gold|int + 10 }}"
  when: monster is killed

A corresponding Molecule playbook can then easily test this tasks file by switching the interactive variable off:

---
- hosts: localhost
  gather_facts: no
  vars:
    interactive: no
  tasks:
    - name: Create world parameters.
      set_fact:
        monster: alive
        player_damage: 6
        monster_life: 10
        gold: 0

    - name: Simulate the interraction.
      include_tasks: ../../kill-monster.yml

    - name: Player should not be able to kill the monster.
      assert:
        that:
          - monster is alive
          - monster_life|int == 4
          - gold|int == 0

Games

I consider this list of games as an artistic experiment about the disruption of specialised tools for entertainment purpose. The idea was also to pay tribute to the early history of video games with titles that were in retrospect the stepping stones of their time.

Mastermind

One of the oldest documented implementation of the game Mastermind (more commonly named bulls and cows in the Anglo-saxon world apparently) was Moo in 1968. This project represents the first wave of games published by aspiring developers back in the ‘60s and ‘70s when the profession didn’t even exist. Scores of board games, card games and pen-and-paper games were converted for institutional mainframes and microcomputers.

I picked Mastermind for its simple interface and its obvious single player quality compared to Battleship or tic-tac-toe.

This game is considered feature complete but I accept improvements and bug fixes anyway.

Repository:

Stats:

asciicast

Hamurabi

The venerable Hamurabi, while not the first of its kind, is of particular interest to me as I remember playing a translated version on my calculator during high school. While simple it represents the first original creations, games designed solely on and for the computer and not as mere adaptations.

Although Americans might better remember The Oregon Trail I personally only heard of it later in my life and never played it.

This game is not polished but still perfectly playable. Improvements and bug fixes are appreciated.

Repository:

Stats:

asciicast

Hunt the Wumpus

Before Hunt the Wumpus published on 1973 by Gregory Yob most games were either abstracts simulations, placed the player as some kind of god or only provided an “eagle-eyed” view of the situation. In this one the player is considered to be inside the game and not a mere puppet, truly crawling blindly in an horrible cave full holes, bats and the disgusting wumpus. The narrative side of games really started here.

This game is fully playable and a close reimplementation of Hunt the Wumpus version 1. There are still a few rough edges though and a couple of nice features appeared in versions 2 and 3 could be included.

Repository:

Stats:

asciicast

Colossal Cave Adventure

I envisioned this project as the pinnacle of this artistic performance: a reimplementation of Colossal Cave Adventure’s first scene, You are standing at the end of a road before a small brick building. It would be slow and awkward as well as a mess of complex Ansible code.

Although a proof of concept exists on my laptop I do not intend to release this project, due to lack of general interest.

Choose Your Own Adventure: Sleepy Cat

Choose Your Own Adventure, or CYOA for short, was a popular game medium before video games existed and still exists to this day.

I implemented a short but functional CYOA engine in 42 lines of Ansible code, sufficient to write a small story about my cat’s most typical day.

Stories can be written like this:

---
chapters:
  "intro":
    msg: |
      Hello! This is a simple demo of a Choose Your Own Adventure style of
      story written directly in Ansible.
    choices: [ "1" ]
  "1":
    msg: "This is the first chapter. It branches on two paths, chapters 2 and 3."
    choices: [ "2", "3" ]
  "2":
    msg: "This is the second chapter. It links to chapter 4."
    choices: [ "4" ]
  "3":
    msg: "This is the third chapter. It links to chapter 4."
    choices: [ "4" ]
  "4":
    msg: "This is the fourth chapter. It links only to the closing chapter."
    choices: [ "end" ]
  "end":
    msg: "This is the end of the demo, thanks :)"

Although very short this game is fully playable. I will perhaps add more content at some point but I am already happy with its current state.

Repository:

Changelog