A framework for text-based games
Ever had an idea for a turn-based game you’d love to play with your friends? Worried about the hassle of:
- Choosing a language, framework, engine
- Writing and testing UI, networking, game logic
What if we could instead write game logic (with quasi-declarative rules), focusing on the “fun” and abstract away everything else?
I have been thinking about this over the holiday break, and I figured I’d “two birds, one stone” by writing up my thoughts (to remember and force reflection) and to share with the wider community (if anyone reads this).
Enter my idea, MBUI
, a lightweight alternative to:
Both of these applications are excellent, and if they work for you, even better. However, I’d challenge you to write a game; say, BlackJack - in a couple of hours, despite that BlackJack is inherently a simple game.
Concept
MBUI
, a placeholder name for now (standing for menu-based UI), makes a number of assumptions about games that make it possible to abstract them away (but limits what types of games can be expressed, alas a trade-off).
- The game must be turn-based. What is a turn is left to the developer, but loosely speaking, the game should not be real-time or use reaction speed as an input.
- The game must have every possible player action declared, ahead of time, as a set of possible (and unique) actions. Even the most complex turn-based games have a finite set of actions, but
MBUI
forces you to declare them. You can further filter and restrict what actions are available, but you can’t create new actions. - The game must be playable using two simple concepts: menus and views.
With these concepts, we can create something that my friend and colleague Kendal declared:
“like steam remote play but without the full display being sent over the wire”
With this architecture, the client is unaware of what game is being played, and instead is shown enough information to understand:
- What the game state is (see views)
- What the player can do
Because the latter is finite, it’s possible to create simple bots or AI agents that have no knowledge of the game either:
// A very simple example.
fn think(rng: &Rng, actions: &[Action]) -> &Action {
rng.pick(actions)
}
NOTE: This might work OK for Rock-Paper-Scissors.
However, it does fall apart for agents that need to reason about the game state. It’s possible we could create a more abstract representation for AIs, i.e. predictive scoring based on actions, but the reality is for nearly any non-trivial game, you’ll want an AI that understands the game details.
Actions
An action is something a player can do, typically during their turn. For the simplest of games, for example playing Dreidel, there isn’t much you can do other than spin:
enum Action {
Spin,
}
In Rock-Paper-Scissors, simple, but a few more options:
enum Action {
Rock,
Paper,
Scissors,
}
Of course this isn’t sufficient for every game.
What about Tic-Tac-Toe? You could model it like:
enum Action {
Draw0_0,
Draw1_0,
Draw2_0,
Draw0_1,
Draw1_1,
Draw2_1,
Draw0_2,
Draw1_2,
Draw2_2,
}
… not only is that cumbersome, but it would make presentation awkward. Imagine receiving a list of 9 possible actions with coordinates, not a great user experience.
Enter, parameterized actions:
enum Action {
Draw(Coordinate),
}
Now we have another problem: is Coordinate
finite? It is in the game, but how would the framework know that? It’s likely we’d want to have bounds, a combination of:
- Minimum and maximum values
- Known invalid values (like,
(1, 2)
is taken)
This complicates serialization, but that’s my idea so far.
Menus
The main navigation of the game will be done with menus.
A menu is as simple as:
- [R] Rock
- [P] Paper
- [S] Scissors
However, more complex games will want more, i.e. BlackJack:
- [B] Bet
- [F] Fold
- [V] View Table
- [S] Stand
- [H] Hit
- [D] Double Down
- [P] Split
The menu gets more complicated (and will require nesting) for more complex games and interactions, but the basic idea is you can always do any (valid) action through menus (+/- scalar arguments like
or
).
Views
Lastly, there needs to be way to view the state of the game, and because there isn’t a guarantee of what kind of UI client will be used (it could be as simple as a terminal), the views need to be abstract.
For now, I’m imagining a simple fallback plain-text view:
[ ] [ ] [X]
[ ] [X] [ ]
[ ] [ ] [O]
Later, there could be simple abstract widgets sent back:
- Column
- Row
- Grid
- Text
It will be a challenge getting every game to fit into basic text-based widgets, but like I said, this framework is a challenge :).
Going from here
Having tried to build a variant of this framework for a few days over the 2022 Holiday Break, I can say it’s not trivial (and I haven’t even introduced networking or asynchronous work into the equation yet).
Advice to myself if I continue to get stuck:
- Write games to get a better understanding of the patterns
- Pull out common code (menus for example) over time
- Look at UI frameworks like Iced for inspiration