Erlang In-Memory Database & GenServers

DANA 325 H E D A F U P X T Q W C G M R K S O N V I B

Objectives

  • Supervisor starts Gen Server and sets up ETS tables. Game is properly designed in an ECTO schema and has the create_game() get_game(id) and set_game(id, attrs) functions implemented. (2 Points)

  • (1 Point)

  • Opening a blank field shows a number with the mines in it's immediate vicinity. (2 Points)

  • Opening a mine highlights all mines and ends the game (no more moves possible) (2 Points)

Please watch the videos under Resources before class.

Creating a Game

You objective is to recreate a simple 1-player game (Minesweeper) on a 9x9 grid with 10 mines. If you have never played minesweeper before please play a round or two here before until you fully understand how it works:

Minesweeper Online

Notice that sometimes when you hit a field with 0 mines adjacent it will open up large areas of the board until all borders are filled with numbers.

Fields

For reasons of simplicity you don't have to implement that feature and you can instead simply show a 0 or otherwise indicate that the field has been opened.

You also don't have to implement a timer that displays the seconds passed since opening the first field.

Here is how a winning game state and a "Game over" gamestate can look like: Fields

For icons you can swoop the Flowbite Icon Library for something that resembles a mine.

To organize fields in form of a grid I highly recommend you to watch the "Tic Tac Toe" video from last Friday. Essentially we can use Tailwind's grid system:

<div class"grid grid-cols-9">
  <button class="size-4 ..." ...>...</button>
</div>

Since this is a bit more involved I recommend you what I always to when tackling challenging problems.

Create your modules blank and think about functions and what their inputs and outputs are before implementing them. A docstring is a great way to do that:

@doc """
Takes the id of a game and the new game state and updates it in the ETS table.
"""
def set_state(id, game) do
end

Finally to help you get started here are some recommendations on your games' schema fields:

embedded_schema do
  field :mine_map, {:array, :boolean}  # 81 entries, represents where mines are
  field :open_map, {:array, :boolean}  # 81 entries, represents what is opened
  field :last_opened, :integer         # index of last field opened
  field :finished, :boolean            # for convenience, not actually needed
end

Here is the ETS module code we used for the Tic Tac Toe game:

defmodule App.ETS do
  use GenServer

  alias App.Games.TicTacToe

  require Logger

  @name __MODULE__

  def start_link(_) do
    # Start the GenServer
    GenServer.start_link(@name, [], name: @name)
  end

  @doc """
  Create and populate in-memory data tables.
  """
  def init(_) do
    :ets.new(:tic_tac_toes, [:ordered_set, :named_table, :public, {:read_concurrency, true}])
    {:ok, []}
  end

  def create_game do
    game = TicTacToe.build_game()
    :ets.insert(:tic_tac_toes, {game.id, game})
    game
  end

  def join_game(game) do
    game = TicTacToe.join_game(game)
    :ets.insert(:tic_tac_toes, {game.id, game})
    game
  end

  def update_game(game, attrs) do
    game = TicTacToe.update_game(game, attrs)
    :ets.insert(:tic_tac_toes, {game.id, game})
    game
  end

  def get_game(id) do
    case :ets.lookup(:tic_tac_toes, id) do
      [{_slug, game}] -> game
      _ -> nil
    end
  end
end

Don't forget to include your ETS module in the list of chilren in your application.ex file:

children = [
  ...

  # Start a worker by calling: App.Worker.start_link(arg)
  # {App.Worker, arg},
  App.ETS, # <-- add this

  # Start to serve requests, typically the last entry
  AppWeb.Endpoint
]

Here is further some helper code discussed in class:

  @doc """
  Returns a list of 89 booleans with 9 of them mines at random positions.
  """
  defp create_random_mines do
    List.duplicate(true, 9)
    |> Kernel.++(List.duplicate(false, 72))
    |> Enum.shuffle()
  end

  def adjacent_count(game, index) do
    # use // and % to convert index to x and y coordinates:
    # x
    # y

    # use x and y to check all 8 neighbors:
    # example: Enum.at(game.mine_map, get_index(x-1, y)) --> true/false depending on mine

    # sum up all mines, returns 0-8
  end

  defp get_index(x, y), do: y * 9 + x

As always, have a lot of fun!

Copyright © 2025 Alexander Fuchsberger, Bucknell University. All rights reserved.