The Teeny Tiny Mansion (TTTM) is a mockup text adventure game that is formally proven to have no "dead ends". I.e. all player actions will result in a state in which the game is still winnable. I started thinking about formal verification for adventures games after I half-jokingly wrote the following tweet, referring to a bug that has been discussed on the Thimbleweed Park Development Blog: https://twitter.com/oe1cxw/status/850861311709851650 TTTM is a proof-of-concept showing that formally verified adventure games are possible, and how they could be built. (Please note that TTTM does not aim to be a very interesting game. So keep your expectations low.) I'll first briefly describe why I think formal verification of adventure games is useful, then describe the TTTM game itself, and then describe how the formal proof I have implemented works. Finally I'll close with some additional remarks on how this techniques could be employed in real-world games. My aim is not only to explain how to to formally verify an adventure game, but also to sneak in some basic knowledge about formal verification and maybe get you interested in formal methods in general. This example uses cbmc as C model checker. The Makefile builds the game and runs all the formal tests necessary to prove that there are no dead ends in the game. Why Formal Verification for Adventure Games? -------------------------------------------- (Good) Adventure Games are not linear. The player has a choice in what order to solve puzzles and has certain freedoms in exploring the world. However, to much freedom can increase the complexity of a game significantly, and make it impossible for game designers and beta testers to make sure the game cannot get into a state that the designers did not anticipate. The most extreme example of this is a situation where the player gets stuck and cannot win the game anymore (without loading a previously saved state of the game). Not only can formal verification help finding those problems early in the design (before beta testing). I believe it would also enable game designers to create more open games, that would be impossible to audit thoroughly by staring at a story graph and by employing human beta testers. This project (TTTM) demonstrates a method that can be used to prove that an adventure game does not have a reachable state in which the game cannot be completed. The Teeny Tiny Mansion ---------------------- TTTM has 2 playable characters (Alice and Bob) and 4 rooms (Red Room, Blue Room, West Room, and East Room). There are also 3 doors: +----------------------------+ +----------------------------+ | | | | | | | | | | | | | | | | | | | | | Red | | Blue | | Room | | Room | | | | | | | | | | | | | | | | | +-------- ---------+ +--------- --------+ | | | | | Red Door | | Blue Door | | | | | +-------- ---------+ +--------- --------+ | | | | | | | | | | | | | | | | | |-----------| | | West Green East | | Room Door Room | | |-----------| | | | | | | | | | | | | | | | | | +----------------------------+ +----------------------------+ Alice's goal is to go to the Red Room and Bob's goal is to go to the Blue Room. There are 3 keys in the game: Red Key, Blue Key, and Green Key. A character can only walk through a door if she/he has the matching key in her/his inventory. (The doors automatically close behind them when they walk through.) Characters can pass keys to each other when they are in the same room. Initially Alice, Bob, and the three keys are randomly placed in the West Room and East Room. When a character reaches her/his destination room then the character will refuse to leave again. There are two possible scenarios for dead ends: 1. The random initial position might be unsolvable. For example, both characters could be placed in the West Room and all keys in the East Room. 2. A character can walk into her or his destination room without providing the necessary assistance to the other character first. The game avoids those scenarios (unless compiled with -D BAD_GAME_DESIGN) by adding the following features: 1. Only solvable initial configurations are created. 2. A character refuses to walk into her/his destination room when the other character still needs help from this character first to ultimately reach his/her destination room. Formal methods are used to confirm that the game with this two additional features indeed cannot reach an unwinnable state. My simple game engine --------------------- For simplicity TTTM (and its formal proofs) are written in plain C99. The game itself has two major data types: typedef ... game_state_t; typedef ... game_action_t; The function make_initstate() creates a pseudo-random init state for the game: game_state_t state; uint32_t initseed = ...; make_initstate(&state, initseed); The function query_actions() creates a list of possible actions that the user can perform in a given state: game_action_t action_list[MAX_ACTIONS]; int num_actions = query_actions(&state, action_list); And the function apply_action() applies a given action to a given game state: int user_input = ...; apply_action(&state, &action_list[user_input]); The game simply creates an initial state and then runs a loop that calls query_actions(), asks the user which action they want to perform, and then calls apply_action() for the selected action, until the game is won. Note that nothing in this interface is in any way specific to the TTTM game itself. Therefore the method for proving the absence of dead ends that I describe below is generic. Formally proving the game has no dead ends ------------------------------------------ For the formal proof we need to provide a few additional functions. Note that the proofs simultaneously prove the correctness of the actual game and those additional functions. So even though there is additional work required to write those additional functions, we don't create an additional surface for new bugs as all bugs in those additional functions would be caught by the formal checks we perform using them. (Depending on how formal_action_valid() is implemented there might be an exception to that rule for this function. See also Closing Thoughts below.) First we need to define a set of valid states. This is done by creating a function that evaluates set membership for this set: bool formal_state_valid(const game_state_t *state); I.e. this function returns true for all states that are members of the set of valid states, and false for all other states. The set of valid states must have the following properties: - It is an over-approximation of all reachable states. I.e. all states that are reachable from an initial state must be members of the set. - All valid states must be "alive", i.e. it must be possible to finish the game from a valid state. - The set membership must be an inductive invariant. That means a state reachable from a valid state via a valid action must also be a valid state. We also need a function that tells us if a given action is valid in a given state: bool formal_action_valid(const game_state_t *state, const game_action_t *action); In our case this function is trivial: It simply calls query_actions() and checks if the given action is a member of the list created by query_actions(). Then we need a rather complex function that takes a game state and produces an action that brings us closer to finishing the game: void formal_actor(const game_state_t *state, game_action_t *action); For long games we need an additional function that scores the current game state. This function is needed if the longest path from any valid state to the end of the game is too long for the naive implementation of the formal proof that I describe in the next section: typedef ... critic_score_t; critic_score_t formal_critic(const game_state_t *state); There must also be a transitive less-than operator for those scores that allows us to measure game progress: bool lt_scores(critic_score_t score1, critic_score_t score2); In TTTM critic_score_t is simply int and lt_scores() is implemented as score1
↧