About a month ago, I wrote a bit about how I made a word game, but I haven't talked much about how I built it, so let me go a little deeper there.
Phrazy was built on top of Blazor, which for the unfamiliar is a way to use familiar C# and .Net tech to compile to web assembly, a standard supported by all of the major browsers. My first real stab at building something with Blazor came toward the end of 2020, when I made a cloud based music player that I call MLocker. I've been using that app for more than a year almost every day, and while it's far from perfect, it's robust enough, and was easy enough to build, that I was compelled to build something else with it.
Inspired by the success of Wordle as a web-based game, I came up with Phrazy mostly because it was easy enough to imitate the classic hangman game. A lot of people see it and think of Wheel of Fortune, but hangman inspired that too. Blazor can generally be organized in much the same way as a React or Vue app, meaning that you can build a series of components nested within each other until you have a complete application.
Phrazy starts with a class called the "game engine." The game engine does what it sounds like it does, and acts as the centralized state for everything that the game has to track. Most components use dependency injection to interact with the one instance of the game engine, through its properties, methods and events. When it first loads, it does some initialization to see if there is a game in progress or it needs to fetch the day's puzzle from the server. It will also see where you ranked the last time you played. Each player is assigned a random identifier, and that identifier is the only thing sent to the server with your play results. It's used to record the results of your play and to rank all of the players by number of guesses, then time, once a day. I'm using local storage in the browser to store that state, because it's more robust than cookies.
The game engine has a state box that keeps track of the puzzle, which letters you've already guessed on the keyboard, the time you started (the clock is "ticking" even if you leave the game because it stores your start time), whether or not you're in solve mode, and the state of each letter on the board (not guessed, guessed, solving and typed a letter, solving and have not typed a letter). That state model is simply serialized to local storage a few times a second, so if you bail, the state is stored.
The structure of it all is straight forward. The Blazor "page" has a number of components on it, including the keyboard, the timer and the game board. The keyboard in turn has a bunch of key components, and the game board has a bunch of tiles. What gets rendered in each of these components largely depends on the game state stored in the game engine. Re-rendering or changing the state of these components depends entirely on the events that they subscribe to. The game engine has a bunch of events that correspond to things you would expect, like choosing a letter on the keyboard or pressing the solve button.
How does this work in practice? Let's look at the entirety of the Key component:
@using Phrazy.Client.Services @using Phrazy.Client.Models @inject IGameEngine GameEngine <button class="key @GameEngine.GameState.KeyStates[Letter]" @onclick="Click" style="@(GameEngine.GameState.IsGameOver ? "cursor:default;" : "")">@Letter</button> @code { [Parameter] public string Letter { get; set; } = null!; protected override void OnInitialized() { GameEngine.OnKeyPress += StateHasChanged; } private async Task Click() { await GameEngine.ChooseLetter(Letter); } }
There isn't much here to look at. When it initializes, it binds its StateHasChanged
method to the game engine's OnKeyPress
event. That means that if the engine calls OnKeyPress?.Invoke()
, for example in the engine's ChooseLetter()
method, everything subscribed to that event fires, which is literally every key. You can see that the button's CSS class is set to @GameEngine.GameState.KeyStates[Letter]
, and there are classes set to "not guessed," "hit" and "miss," meaning it ends up gray, green or red respectively.
That's how the whole game works. The game engine responds to input, changes its state, and the appropriate components are coaxed into re-rending in response to those changes via events. If you've spent most of your career writing code for a stateless web or transient data flowing through high volume systems, it's a lot of fun to have all of the state just sitting there!
No comments yet.