Skip to content

Instantly share code, notes, and snippets.

@masak
Created November 17, 2012 16:35
Show Gist options
  • Save masak/4097407 to your computer and use it in GitHub Desktop.
Save masak/4097407 to your computer and use it in GitHub Desktop.
Adventure games, the eventful way

Adventure games, the eventful way

Two ways to model the world

Taking a Towers-of-Hanoi game as an example

  • Hanoi
    • Moving disks around
    • Move them all over to the other side, and you win
    • Move them away again, and you un-win
    • You can also remove the smallest disk; we'll see why
  • Nouns
    • Start thinking about the shape the data should have
    • Make sure all actions can be carried out on the data
    • This is a very common approach
    • Works well with databases, too!
    • Data can be shaped in many ways
    • This thinking tends to favor DRY and normalized data
    • Sometimes we realize that the data should really have a different shape
    • Maybe because new requirements (and new actions) were added
    • Changing the shape of the data is often tricky because all the actions depend on it
  • Verbs
    • Start thinking about the actions we need to support
    • Model these actions in sufficient detail as events
    • Think about which commands (method calls) will trigger which events
    • Think about all sad paths; which exceptions are thrown?
    • Events, command, and exceptions all have parameters; model them
    • Don't model the shape of the data
    • Start writing tests, TDD-style, modeling commands to events or exceptions
    • This will feel very familiar to people who do BDD-style testing
    • (BDD is great because it forces you to think about sad paths)
    • We're testing behavior: commands leading to events or exceptions
    • Sometimes new insights make us extend or change our commands or events
    • This is now trivial in most cases
    • Our tests read like stories
  • So, what's the big deal?
    • It's all bits (or electrons) at the bottom
    • I don't want to program there; want to abstract to a higher level
    • Thinking about data is still too close to bits
    • I want to think about what you can do with the system I'm designing
    • The shape of the data is only relevant insofar as it supports the modeled actions
    • If I make the shape of the data part of my public interface, I lose
    • Encapsulate the shape of the data; expose commands, events, and exceptions

Demo of Hanoi

  • The disk display? It's generated every time from the event queue
  • I could have cached it and just replayed the latest events
  • But in the end I didn't bother; the delay isn't noticeable

How well does this work in practice?

  • Coke++ tested the game for me
  • He found three bugs
  • Bug 1: wrong exception back when removing a non-existent disk
    • Fair enough
  • Bug 2: no visual feedback when winning the game
    • Easy; just fix the UI
  • Bug 3: you can't win by adding the small disk at the end
    • Real braino on my part
    • Shows how easy it is to just focus on your own use cases
    • The fix was easy
  • So yes, there were bugs
  • But overall a very positive experience

This adventure game

  • clearing
    • There's a car here, with some equipment you'll need
  • hill
    • If you look among the grass and bushes, you'll find a door
  • chamber
    • A sign with the word LEAVE but a letter appears to be missing
  • hall
    • Solve the Hanoi subgame, and the floor'll tip and reveal an opening
  • cave
    • The fire is so hot, you can't walk past it without putting it out
  • crypt
    • The treasure is on a pedestal, but if you take it, the whole cavern collapses after three moves — very Indiana Jones

Last year

  • I made classes (like Door) and roles (like Openable) for everything
  • Some classes were even for abstract concepts (like Doom), just to stick game logic somewhere
  • No separation between "adventure game" and "this adventure game, crypt"
    • No flexibility
  • Besides, and worse, I kept breaking the game
    • There was no easy way to test the game, because it was all tied to I/O
  • "Hey," I thought, "I know how to fix that!"
  • Let's try this again, but with TDD and event-based programming
  • A chance to compare two approaches to writing the same application

Adventure games

  • Mechanics
    • An adventure game goes through a setup phase and a game phase
    • Most of the commands in the setup phase are useful in the game phase, too
    • But the adventure game restricts access to them
    • Eight compass directions; up/down, in/out
    • Rooms are connected during the setup phase; they can be disconnected and reconnected later
    • Rooms contain things
    • Things can have various properties, like openable, container, platform, readable, hidden, carryable, implicit, light source
    • Rooms can have the property dark
    • Each time we talk about properties like this, we're really talking about events that give things/rooms these properties, and the events enabled by this
    • It was when I designed this that I realized that in last year's game, you could actually put containers inside themselves: put helmet in helmet
      • Yes. I suck at programming
      • X::Adventure::YoDawg
    • We can attach arbitrary logic to things and rooms by providing them with hooks: on_examine, on_put, on_open, on_remove_from, on_take, on_try_exit

crypt

Architecturally, we have this:

+-------------------------+
|          crypt          |
|                         |
| +-----------+ +-------+ |
| | adventure | | hanoi | |
| |  engine   | +-------+ |
| +-----------+           |
+-------------------------+

Basically, crypt forwards most commands to the adventure engine. It mediates some things to the Hanoi subgame.

Event-based TDD black-box testing

  • Example: How do we test that dropping stuff actually works?
  • In black-box testing, the way you test if A succeeded is by doing something that assumes A succeeded
  • We already have a test that says you can't pick something up that you're already holding
  • So to test dropping, we drop something and then try to pick it up again
  • If dropping didn't work, we can't pick it up

Lessons learned

  • The real world is messy
    • Adventure games mimic the real world
    • Arbitrary inputs; people are inventive
    • So, adventure games get messy, too
    • Having a really good underlying model doesn't change that
  • Event confusion
    • Internally, a command is validated, and then events are applied/returned
    • So until the end, the game is in the state prior to the command
    • Sometimes I, the programmer, tended to assume the events had already been applied
    • Example: walking into a new room
      • Two new events are generated: PlayerWalked, PlayerLooked
      • Because of event confusion, initially the game reported the things in the old room
    • I think there is a fix here, but I haven't figured it out yet
  • I think I need sagas
    • Also known as process managers
    • There's something slightly wrong with chains of events
    • Too much manual handling
    • Too error-prone
  • That said...
  • It's so nice
  • Do this!
  • Writing things with an event focus makes for a much cleaner architecture

Demo of crypt

If time permits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment