We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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