Rust for Javascript Engineers - Connect-4 Interactivity
In the last post we looked at setting up the Connect-4 game board and transfering data back and forth between the JavaScript and WASM boundary. This allowed us to render a randomized board to the screen, however for it to be a real game we need allow users to make moves.
In this post let's take a look at adding a bit of interactivity to allow players to play a full game, and at the very end calculate the winner!
Interactivity
The first piece of interactivity we can add to our connect-4 is a way to switch between players. Let's add a new enum that encodes the player variants, and then add it to our larger game state which is the Board struct.
- end_player_turn
The end_player_turn function just flips the player turn state on the overall game state between red and black variants.
- select_col
The select_col function is where majority of the game logic will go. Once all the required state has been updated, it'll call end_player_turn and the other player can make their move.
JavaScript to match
Now let's update our JavaScript to accomodate for these changes, by first adding another div that would display the player turn, updating the rendering logic, and adding an event handler.
That's a wall of code, however most of it is just a small refactor to ensure that we can render after each user event and not just once when the page loads.
- Render refactor
We extract the rendering logic into it's own function so that after each user interaction we can update the entire screen to display the updated state.
- Adding event listeners
The event handler is applied to the entire grid, it extract the column index from the data attribute and calls the select_col function that we added to the rust code earlier, and finally calls render to update the screen.
If you rebuild and launch the app now you'll see that clicking anywhere on the grid updates the Player turn back and forth between R and B.
Here's the full commit.
Making Moves
So far we've been using random tiles, now let's replace those with empty tiles.
- private Vec field
Only simple types are directly exposed across the Wasm/JS boundary, so vector fields cannot be public. have to be converted into typed arrays.
- Initializing Vec to Empty
Here we initialize entire board to TileType::Empty using the vec! macro instead of a randomized board.
Now let's update the select_col function to update the state based on user's selected column.
- Bounds Checking
As we're storing the entire grid as a single array, checking if the selected index is valid is trivial.
- Chunking
Rust Vecs support non overlapping chunks out of the box.
- Reversing the rows
As Connect-4 fills bottom up, we have to find the last row that's empty to replace the tile.
There's another way to combine the chunking and reversing via self.boards.rchunks which gives us an iterator over already reversed chunks but I wanted to break this down for clarity.
- Updating the board
Once an empty row is found we can map over the board and replace it after updating the appropriate tile. If we go through all the rows without finding an empty row, we can just return false to signify that no update was made.
That should be it for interactivity!
Winning Move
As this requires changes in a lot of places I'll only go over the crucial bits that use Rust features we haven't explored before. The full diff can be found here. Currently the game never ends even if one of the player connects 4 of their tiles. The win condition requires a player to connect four of their tiles either horizontally, vertically, or diagonally. Let's look at the first one.
As the state only changes when a player makes a move we can narrow things down to a player, a row, and a column.
Similarly we can check if four tiles in a column belong to the expected player, I won't write the code here but it's part of the commit.
There are several ways of encoding the winning player but as we don't advance the player turn after the winning move, it's just simpler to combine the player_turn and is_game_over.
- PartialEq trait
The partial eq trait is necessary to perform equality checks on enum variants. The derive macro conveniently implements the trait for us here as the Player enum variants are trivial.
- Option type
Rust has no null values, the idiomatic way to encode the absence of a value is to use an Option of the appropriate type. In this case Option can either store Some(Player::Red) or Some(Player::Black) or None. We initialize the winner to None when the game starts and replace it with the appropriate player that makes the winning move.
On the JavaScript side None shows up as undefined.
- does_belong_to
As we have more TileType variants than Player variants, we need a way to map whether a tile belongs to a player.
The JavaScript changes are trivial (updating text and disabling click events after game ends) and I'll skip those here.
This concludes all of the gameplay for Connect-4, we've exposed the smallest amount of API required to play a full game. There's no clean-up required as refreshing the page frees the memory allocated in WASM and reallocates the memory when the game restarts.
Discussion in the ATmosphere