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
- 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
- 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
- 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
- A sign with the word
- 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
- I made classes (like
Door
) and roles (likeOpenable
) 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
- 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
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.
- 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
- 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
- Two new events are generated:
- 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
If time permits.