Skip to content
Go back

Learn Rust by Making Minesweeper - Part 1: Types

Edit page

Post OG Image

Table of Contents

Open Table of Contents

Introduction

In this series, I attempt to learn Rust by building a simple Minesweeper game. I will try to use functional programming whenever sensible and follow a type driven design approach. This first part covers the algebraic data types in my game and how they are used in the context of the game.

Motivation

I have always wanted to learn Rust, but I never had a good reason to do so. Lately, I have been thinking about how I would do the “Remake the Google Minesweeper Game” interview question in Rust, why not just use it as a reason to learn the language?

Google Minesweeper

Setting Up the Project

First, I created a new Rust project using Cargo:

cargo new minesweeper

cd minesweeper

This is pretty much it. However, to make my life easier to do FP in Rust, I added the itertools and im crates to my Cargo.toml file:

[dependencies]
itertools = "0.10"
im = "15.0"

itertools provides extra iterator adaptors and functions, while im provides immutable data structures.

Defining the Types

Record or Product type in Rust is defined using struct like in any other languages, and Union or Sum type is defined using enum. From a bottom-up perspective, our game needs a basic Coord type to represent the coordinates of a cell in the grid:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Coord {
    x: i32,
    y: i32,
}

A Coord is simply a pair of x and y coordinates, both are unsigned integers. I derived some useful traits for this type, such as Debug for printing, Clone and Copy for copying, PartialEq and Eq for equality comparison, and Hash for hashing. The derive macro is like decorators in Python, annotations in Java/Kotlin or typeclasses in Haskell.

Then, looking at the game, we can see that a tile can be in one of the following states:

This is a perfect example of a Sum type, we can define it using enum:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TileContent {
    Mine,
    Number(u8), // 1-8
    Empty,      // 0
}

Similarly, a tile can be in one of the following states:

This is another Sum type, we can define it using enum:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TileVisibility {
    Hidden,
    Revealed,
    Flagged,
}

We are halfway done defining the types for our game, now we can define the main Tile struct that encapsulates both the content and the visibility of a tile:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Tile {
    content: TileContent,
    visibility: TileVisibility,
}

With the tiles define, we need a type to represent the game board, which is a grid of tiles. We can use a 2D vector to represent the grid of tiles, but for simplicity, I will use a hash map where the key is the Coord and the value is the Tile. This allows us to easily access and update the tiles in the grid:

use im::HashMap;
struct GameBoard {
    board_size: i32,
    tiles: HashMap<Coord, Tile>,
}

Here, I used im::HashMap from the im crate to represent the grid, which is an immutable hash map. The key is a Coord and the value is a Tile. This allows us to easily access and update the tiles in the grid.

Finally, we need a type to represent the game state, which includes the game board, the number of mines, and the game status (ongoing, won, lost):

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GameStatus {
    Ongoing,
    Won,
    Lost,
}
struct GameState {
    board: GameBoard,
    num_mines: u32,
    status: GameStatus,
}

Conclusion

Zooming out, we have defined the basic types for our Minesweeper game using Rust’s struct and enum. We have a Coord type to represent the coordinates of a cell, a TileContent enum to represent the content of a tile, a TileVisibility enum to represent the visibility of a tile, a Tile struct to encapsulate both the content and visibility of a tile, a GameBoard struct to represent the grid of tiles, and a GameState struct to represent the overall game state. In the next part, we will implement the game logic in form of pure functions that operate on these types. I hope you found this post useful and interesting. Stay tuned for the next part of the series!

References


Edit page
Share this post on:

Next Post
Resolving Docker Push Errors: The Unexpected 400 Bad Request from AWS ECR