Home Posts Post Search Tag Search

LiveView 29 - Chapter 11: Build the Game Core
Published on: 2026-02-17 Tags: elixir, Blog, Side Project, LiveView, Game Site, Phoenix

Chapter 11: Build the Game Core

The Plan

We will have a set of Pentominoes that we will use to place on a board. Depending on the size of the board there will be solutions that work. For this there are 12 shapes for the Pentominoes.

:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t

The Game Board

The Board

• points: All of our puzzles will be rectangles of different shapes. The puzzle
shape will be a list of points that make up the grid of our puzzle board.
• palette: The set of pentomino shapes that must be placed onto the board
in order to complete the puzzle.
• completed_pentos: The pentomino shapes that have already been placed on
the board. This will update as the user places more shapes.
• active_pento: The pentomino from the palette that the user has selected and
is actively in the process of placing on the board.

The Pentominoes Pieces

The Pieces

• name: The type of shape, for example :i or :p.
• rotation: The number of degrees that the shape has been rotated.
• reflected: A true/false value to indicate whether the user flipped the shape
over to place it on the board.
• location: The location of the shape on the board grid.
• points: The five points that comprise the given shape.
• color: The color associated with the given shape.

The Pentominoes Points

We will build a set of points that we will use to place a piece on the board. It will leverage the Point module. It will use {x, y} tuples that will be the shape of the piece. Then we will have some reducer functions that will transform the points on the board when we place it, we will also need some rotation and reflection functions. Make sure to create this new module pento/lib/pento/game/point.ex

Represent a Shape With Points

While working with the initial Pentomino we will always try and place the piece within a 5x5 square at the top left of the board then move the piece into the right place. It will follow this sequence.

• Always plot each shape in the center of a 5x5 grid that will occupy the
top-left of any given game board.
• Calculate the location of each point in the shape given its rotation and
reflection within that 5x5 square.
• Only then will we apply the location to move the pentomino’s location on
the wider board.

Define the Point Constructor.

defmodule Pento.Game.Point do
  def new(x, y) when is_integer(x) and is_integer(y), do: {x, y}
end

Move Points Up and Down with Reducers

def move({x, y}, {change_x, change_y}) do
  {x + x_change, y + y_change}
end
iex> alias Pento.Game.Point
Pento.Game.Point
iex> Point.new(2, 2) |> Point.move({1, 0})
{3, 2}

iex> Point.new(2, 2) |> Point.move({1, 0}) |> Point.move({0, 1})
{3, 3}

Move Points Geometrically with Reducers

We will want to take care of 3 different movements for the pieces: reflect, flip, transpose.


Let’s first talk about the transpose/1 this will reflect the shape across a diagonal line from the top left to the bottom right. This is really just a swap of the x and the y coordinates.

def transpose({x, y}), do: {y, x}

Now let’s talk about a flip which is a reflect across a line that is horizontal and below the shape. So in this case the x stays the same but we need to change the y. Think about the shape being in the center of the 5x5, we will then need to take the current y and then subtract that from 6.

def flip({x, y}), do: {x, 6 - y}

Now its really just the same thing for the reflect expect that now we need to subtract the x from 6 and leave the y alone.

def reflect({x, y}), do: {6 - x, y}

Here are some tests within an elixir shell to see what we mean.

iex> Point.new(2, 2) |> Point.move({1, 0})
{3, 2}
iex> Point.new(1, 1) |> Point.reflect
{5, 1}
iex> Point.new(1, 1) |> Point.flip
{1, 5}
iex> Point.new(1, 1) |> Point.flip |> Point.transpose
{5, 1}

Combine Reducers to Rotate Points.

Here is where we get to combine what we have made so far and make some rotations. I won’t go into every detail about how this works but here is the functions.

def rotate(point, 0), do: point
def rotate(point, 90), do: point |> reflect |> transpose
def rotate(point, 180), do: point |> reflect |> flip
def rotate(point, 270), do: point |> flip |> transpose

Okay so let’s talk about one of the rotates (rotate(point, 90)). We are given a shape and we want to rotate it 90 degrees. Take the L.

|_
Reflect
_|
Transpose
--|

Here is some tests within the elixir shell

iex> alias Pento.Game.Point
iex> points = [{3, 2}, {4,2}, {3, 3}, {4, 3}, {3, 4}]
iex> Enum.map(points, &Point.rotate(&1, 90))
[{2, 3}, {2, 2}, {3, 3}, {3, 2}, {4, 3}]

Prepare a Point for Rendering

Okay so a piece will need to go through all it’s transformations and then be moved in the right spot on the board. Keep in mind that we are starting off the shape within a 5x5 grid that will have the piece in the {3, 3} so we will need to move that piece {-3, -3} in order to put it in the right spot.

def center(point), do: move(point, {-3, -3})

So now a normal set of transformation might look something like this

iex> [{3, 2}, {4,2}, {3, 3}, {4, 3}, {3, 4}] \
|> Enum.map(&Point.rotate(&1, 90)) \
|> Enum.map(&Point.reflect(&1)) \
|> Enum.map(&Point.move(&1, {5, 5})) \
|> Enum.map(&Point.center(&1))
[{6, 5}, {6, 4}, {5, 5}, {5, 4}, {4, 5}]

Okay so we have the right series of transformations now let’s add in a function that will do the reducing for us.

def prepare(point, rotation, reflected, location) do
  point
  |> rotate(rotation)
  |> maybe_reflect(reflected)
  |> move(location)
  |> center
end

def maybe_reflect(point, true), do: reflect(point)
def maybe_reflect(point, false), do: point

This will take a point and do all the work that we need to do to it. At the end it will move the shape to the right location.

Group Points Together in Shapes

Okay so now we need to define the shapes. Let’s create an other module for the shapes. pento/lib/pento/game/shape.ex

defmodule Pento.Game.Shape do
  defstruct color: :blue, name: :x, points: []
end

Here is some testing of the basic struct

iex> alias Pento.Game.Shape
Pento.Game.Shape
iex> Shape.__struct__
%Pento.Game.Shape{color: :blue, points: [], name: :x}

Okay from here I’ll just give you the hard coded colors and shapes for the rest of the Pentominoes.

defp color(:i), do: :dark_green
defp color(:l), do: :green
defp color(:y), do: :light_green
defp color(:n), do: :dark_orange
defp color(:p), do: :orange
defp color(:w), do: :light_orange
defp color(:u), do: :dark_gray
defp color(:v), do: :gray
defp color(:s), do: :light_gray
defp color(:f), do: :dark_blue
defp color(:x), do: :blue
defp color(:t), do: :light_blue

defp points(:i), do: [{3, 1}, {3, 2}, {3, 3}, {3, 4}, {3, 5}]
defp points(:l), do: [{3, 1}, {3, 2}, {3, 3}, {3, 4}, {4, 4}]
defp points(:y), do: [{3, 1}, {2, 2}, {3, 2}, {3, 3}, {3, 4}]
defp points(:n), do: [{3, 1}, {3, 2}, {3, 3}, {4, 3}, {4, 4}]
defp points(:p), do: [{3, 2}, {4, 3}, {3, 3}, {4, 2}, {3, 4}]
defp points(:w), do: [{2, 2}, {2, 3}, {3, 3}, {3, 4}, {4, 4}]
defp points(:u), do: [{2, 2}, {4, 2}, {2, 3}, {3, 3}, {4, 3}]
defp points(:v), do: [{2, 2}, {2, 3}, {2, 4}, {3, 4}, {4, 4}]
defp points(:s), do: [{3, 2}, {4, 2}, {3, 3}, {2, 4}, {3, 4}]
defp points(:f), do: [{3, 2}, {4, 2}, {2, 3}, {3, 3}, {3, 4}]
defp points(:x), do: [{3, 2}, {2, 3}, {3, 3}, {4, 3}, {3, 4}]
defp points(:t), do: [{2, 2}, {3, 2}, {4, 2}, {3, 3}, {3, 4}]

Okay so let’s create the constructor for the shapes. Make sure to add in the alias for the points.

alias Pento.Game.Point

...
  def new(name, rotation, reflected, location) do
    points =
      name
      |> points()
      |> Enum.map(&Point.prepare(&1, rotation, reflected, location))

    %__MODULE__{points: points, color: color(name), name: name}
  end

Okay so now we have the constructor and we can now just pass in the shape that we want and then if we want it rotated etc.

iex> Pento.Game.Shape.new(:p, 90, true, {5, 5})
%Pento.Game.Shape{
color: :orange,
points: [{6, 5}, {5, 4}, {5, 5}, {6, 4}, {4, 5}],
name: :p
}

iex(4)> Shape.new(:x, 0, true, {1,1})
%Pento.Game.Shape{
  color: :blue,
  name: :x,
  points: [{1, 0}, {2, 1}, {1, 1}, {0, 1}, {1, 2}]
}

Track and Place a Pentomino

Okay so we have created the shape and a way to deal with the points now we need to have a way to keep track of a Pentomino.

Represent Pentomino Attributes

Let’s create an other module for that. pento/lib/pento/game/pentomino.ex

defmodule Pento.Game.Pentomino do
  @names [:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t]
  @default_location {8, 8}
  defstruct name: @names,
            rotation: 0,
            reflected: false,
            location: @default_location
end

Okay let’s test it

iex> alias Pento.Game.Pentomino
Pento.Game.Pentomino
iex> Pentomino.__struct__
%Pento.Game.Pentomino{
location: {8, 8},
name: :i,
reflected: false,
rotation: 0
}

Define the Pentomino Constructor

def new(fields \\ []), do: __struct__(fields)

Here is a test for that

ex> Pentomino.new(location: {11,5}, name: :p, reflected: true, rotation: 270)
%Pento.Game.Pentomino{
location: {11, 5},
name: :p,
reflected: true,
rotation: 270
}

Manipulate Pentominoes with Reducers

Okay so now that we have all of this we will start to build the Pentomino in a way that we can rotate flip or move the pentomino and then once the user is ready to place we can then call the Shape to get the correct shape and place it on the board. So in this case we need to be able to do the following:

• Rotate the pentomino in increments of 90 degrees
• Flip the pentomino
• Move the pentomino up, down, left, or right one square at a time

Every thing we do here will take a pentomino and do something to its values and return a new pentomino.


This will add a 90 to the rotation and then wrap around to 0 if it is 360.

alias Pento.Game.Point

...

  def rotate(%{rotation: degrees} = p) do
    %{p | rotation: rem(degrees + 90, 360)}
  end

This will swap the reflected tag.

  def flip(%{reflected: reflection} = p) do
    %{p | reflected: not reflection}
  end

These are all one movement up, down, left, or right.

  def up(p) do
    %{p | location: Point.move(p.location, {0, -1})}
  end

  def down(p) do
    %{p | location: Point.move(p.location, {0, 1})}
  end

  def left(p) do
    %{p | location: Point.move(p.location, {-1, 0})}
  end

  def right(p) do
    %{p | location: Point.move(p.location, {1, 0})}
  end

Convert a Pentomino to a Shape

Now this is where we will take a Pentomino and turn it into a shape with all the needed values. Here is the series of events that need to happen:

• Get the list of default points that make up the given shape.
• Iterate over that list of points and call Point.prepare/4 to apply the provided
rotation, reflection, and location to each point in the shape. Collect the
newly updated list of properly oriented and located points.
• Return a shape struct that knows its name, color, and this updated list
of points.

alias Pento.Game.Shape

...

  def to_shape(pento) do
    Shape.new(pento.name, pento.rotation, pento.reflected, pento.location)
  end

Test Drive the Pentomino CRC Pipeline

iex> Pentomino.new(name: :i) |> Pentomino.rotate |> Pentomino.rotate
%Pento.Game.Pentomino{
location: {8, 8},
name: :i,
reflected: false,
rotation: 180
}

iex> Pentomino.new(name: :i) \
|> Pentomino.rotate \
|> Pentomino.rotate \
|> Pentomino.down
%Pento.Game.Pentomino{
location: {8, 9},
name: :i,
reflected: false,
rotation: 180
}

iex> Pentomino.new(name: :i) \
|> Pentomino.rotate \
|> Pentomino.rotate \
|> Pentomino.down \
|> Pentomino.to_shape
%Pento.Game.Shape{
color: :dark_green,
points: [{8, 11}, {8, 10}, {8, 9}, {8, 8}, {8, 7}],
name: :i
}

Track a Game in a Board

Okay so now that we have all of this we can now, create a pentomino perform any transformation, then turn it into a shape with all the correct information. Now we need to talk about the board. Create the following module pento/lib/pento/game/board.ex

Represent Board Attributes

The board will have the following attributes:

• points: The points that make up the shape of the empty board that the
user will fill up with pentominoes. All of our shapes will be rectangles of
different sizes.
• completed_pentos: The list of pentominoes that the user has placed on the
board.
• palette: The provided pentominoes that the user has available to solve the
puzzle.
• active_pento: The currently selected pentomino that the user is moving
around the board.

Let’s create the module with the following to start.

defmodule Pento.Game.Board do
  alias Pento.Game.{Pentomino, Shape}

  defstruct active_pento: nil,
            completed_pentos: [],
            palette: [],
            points: []
end

This is used to set the values for the size of the board.

def puzzles(), do: ~w[default wide widest medium tiny]a

Define the Board Constructor

This is used to create the board size and define the shapes that can be used.

  def new(palette, points) do
    %__MODULE__{palette: palette(palette), points: points}
  end

  def new(:tiny), do: new(:small, rect(5, 3))
  def new(:widest), do: new(:all, rect(20, 3))
  def new(:wide), do: new(:all, rect(15, 4))
  def new(:medium), do: new(:all, rect(12, 5))
  def new(:default), do: new(:all, rect(10, 6))

Define Constructor Helper Functions

This will take the values given and make a board of the right size.

  defp rect(x, y) do
    for x <- 1..x, y <- 1..y, do: {x, y}
  end

For this we need to determine what kinds of pentominoes can be used.

  defp palette(:all), do: [:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t]
  defp palette(:small), do: [:u, :v, :p]

Okay let’s test this out.

iex> alias Pento.Game.Board
Pento.Game.Board
iex> Board.new(:tiny)
%Pento.Game.Board{
  active_pento: nil,
  completed_pentos: [],
  palette: [:u, :v, :p],
  points: [{1, 1},{1, 2},{1, 3},{2, 1},{2, 2},{2, 3},...{5, 3}]
}

Manipulate The Board with Reducers

We want the user to be able to do:

• choose: Pick an active pentomino from the palette to move around the
board. This should update a board struct’s active_pento attribute.
• drop: Place a pentomino in a location on the board. That should update
the list of placed pentominoes in the completed_pentos attribute of a given
board struct.

We also want the board to be able to test some things before a Pentomino is placed. Also to see if the board is completed or not.

• legal?: Returns true if the location a user wants to drop a pentomino in is
in fact on the board. 
• droppable?: Returns true if the location a user wants to drop a pentomino
in is in fact unoccupied by another piece.
• status: Indicates if all of the pentominoes in the palette have been placed
on the board. In other words, are all of the pentominoes in the palette
listed in the completed_pentos list of placed pieces?

Some of this will have to wait but we can at least gather the pieces and show the active piece.

Translate a Board into Shapes for Presentation

Okay so now we need to take the information about the board and return an active board.

  def to_shape(board) do
    Shape.__struct__(color: :purple, name: :board, points: board.points)
  end

For this we need to be able to put the board at the bottom of the stack so the we need to render that first.

  def to_shapes(board) do
    board_shape = to_shape(board)

    pento_shapes =
      [board.active_pento | board.completed_pentos]
      |> Enum.reverse()
      |> Enum.filter(& &1)
      |> Enum.map(&Pentomino.to_shape/1)

    [board_shape | pento_shapes]
  end

Okay to wrap this all up we did the following:

• Convert the board into the single shape representing the puzzle
• Construct a list of the board’s active pento and completed pentos
• Reverse the order of those items
• Strip out the nils in case the active pento is not set
• Convert them into shapes
• Assemble the final list of shapes in the correct order for rendering

Highlight the Active Pento

Okay so last but not least we want to have the active pento be highlighted before we place it.

  def active?(board, shape_name) when is_binary(shape_name) do
    active?(board, String.to_existing_atom(shape_name))
  end

  def active?(%{active_pento: %{name: shape_name}}, shape_name), do: true
  def active?(_board, _shape_name), do: false