Home Posts Post Search Tag Search

LiveView 30 - Chapter 11: Your Turn
Published on: 2026-02-17 Tags: elixir, Blog, Testing, LiveView, Game Site, Phoenix, Framework

Your Turn

Give It a Try

• Write tests for the core. Focus on writing unit tests for the individual
reducer functions of our core modules. Maybe reach for the reducer testing
pipelines we used in Chapter 10, Test Your Live Views, on page 311 to keep
your tests clean, flexible, and highly readable.
• Add a skewed rectangle1 puzzle, and a function called Board.skewed_rect/2,
like Board.rect/2 to support it.
• Add an optional direction argument to Pentomino.rotate/1 that allows both
clockwise and counterclockwise rotation.

Lets start with the tests

defmodule Pento.Game.PointTest do
  use ExUnit.Case, async: true

  alias Pento.Game.Point

  @valid_x 2
  @valid_y 3

  @invalid_x "a"
  @invalid_y "b"

  setup [:create_point]

  describe "new" do
    test "valid x, y" do
      assert {2, 3} = Point.new(@valid_x, @valid_y)
    end

    test "invalid x, y" do
      assert_raise FunctionClauseError, fn ->
        Point.new(@invalid_x, @invalid_y)
      end
    end
  end

  describe "transpose" do
    test "transposes x and y", %{point: point} do
      assert {3, 2} = Point.transpose(point)
    end

    test "flip", %{point: point} do
      assert {2, 3} = Point.flip(point)
    end

    test "reflect", %{point: point} do
      assert {4, 3} = Point.reflect(point)
    end

    test "center out of bounds", %{point: point} do
      assert {-1, 0} = Point.center(point)
    end

    test "center" do
      good_point = Point.new(4, 5)
      assert {1, 2} = Point.center(good_point)
    end

    test "move", %{point: point} do
      assert {3, 4} = Point.move(point, {1, 1})
    end

    test "rotate 0", %{point: point} do
      assert {2, 3} = Point.rotate(point, 0)
    end

    test "rotate 90", %{point: point} do
      assert {3, 4} = Point.rotate(point, 90)
    end

    test "rotate 180", %{point: point} do
      assert {4, 3} = Point.rotate(point, 180)
    end

    test "rotate 270", %{point: point} do
      assert {3, 2} = Point.rotate(point, 270)
    end

    test "rotate 360", %{point: point} do
      assert {2, 3} = Point.rotate(point, 360)
    end
  end

  describe "prepare" do
    test "all", %{point: point} do
      test_point =
        point
        |> Point.prepare(90, true, {1, 1})

      assert {1, 2} = test_point
    end

    test "rotation: 90", %{point: point} do
      test_point =
        point
        |> Point.prepare(90, false, {0, 0})

      assert {0, 1} = test_point
    end

    test "rotation: 180", %{point: point} do
      test_point =
        point
        |> Point.prepare(180, false, {0, 0})

      assert {1, 0} = test_point
    end

    test "rotation: 270", %{point: point} do
      test_point =
        point
        |> Point.prepare(270, false, {0, 0})

      assert {0, -1} = test_point
    end

    test "360 rotation", %{point: point} do
      test_point =
        point
        |> Point.prepare(360, false, {0, 0})

      assert {-1, 0} = test_point
    end

    test "move", %{point: point} do
      test_point =
        point
        |> Point.prepare(0, false, {1, 1})

      assert {0, 1} = test_point
    end
  end

  describe "idempotent" do
    test "rotate 360", %{point: point} do
      assert {2, 3} = Point.rotate(point, 360)
    end

    test "reflect twice", %{point: point} do
      assert {2, 3} = point |> Point.reflect() |> Point.reflect()
    end

    test "transpose twice", %{point: point} do
      assert {2, 3} = point |> Point.transpose() |> Point.transpose()
    end

    test "flip twice", %{point: point} do
      assert {2, 3} = point |> Point.flip() |> Point.flip()
    end
  end

  defp create_point(_context) do
    {:ok, %{point: Point.new(@valid_x, @valid_y)}}
  end
end
defmodule Pento.Game.ShapeTest do
  use ExUnit.Case, async: true

  alias Pento.Game.Shape

  @valid_color :orange
  @valid_name :p
  @valid_points [{5, 4}, {6, 5}, {5, 5}, {6, 4}, {5, 6}]

  describe "new" do
    test "creates a shape no transformation" do
      shape = Shape.new(@valid_name, 0, false, {5, 5})

      assert shape.points == @valid_points
      assert shape.color == @valid_color
    end

    test "creates a shape with rotation" do
      shape = Shape.new(@valid_name, 90, false, {5, 5})

      assert shape.points == [{4, 5}, {5, 4}, {5, 5}, {4, 4}, {6, 5}]
      assert shape.color == @valid_color
    end

    test "creates a shape with rotation: 180" do
      shape = Shape.new(@valid_name, 180, false, {5, 5})

      assert shape.points == [{5, 6}, {4, 5}, {5, 5}, {4, 6}, {5, 4}]
      assert shape.color == @valid_color
    end

    test "creates a shape with rotation: 270" do
      shape = Shape.new(@valid_name, 270, false, {5, 5})

      assert shape.points == [{6, 5}, {5, 6}, {5, 5}, {6, 6}, {4, 5}]
      assert shape.color == @valid_color
    end

    test "creates a shape with reflection" do
      shape = Shape.new(@valid_name, 0, true, {5, 5})

      assert shape.points == [{5, 4}, {4, 5}, {5, 5}, {4, 4}, {5, 6}]
      assert shape.color == @valid_color
    end

    test "creates a shape with rotation and reflection" do
      shape = Shape.new(@valid_name, 90, true, {5, 5})

      assert shape.points == [{6, 5}, {5, 4}, {5, 5}, {6, 4}, {4, 5}]
      assert shape.color == @valid_color
    end

    test "invalid name" do
      assert_raise FunctionClauseError, fn ->
        Shape.new(:invalid, 0, false, {5, 5})
      end
    end

    test "invalid rotation" do
      assert_raise FunctionClauseError, fn ->
        Shape.new(@valid_name, 45, false, {5, 5})
      end
    end
  end
end
defmodule Pento.Game.PentominoTest do
  use ExUnit.Case, async: true

  alias Pento.Game.Pentomino
  alias Pento.Game.Shape

  describe "new" do
    test "creates a shape with the correct points" do
      pento = Pentomino.new(name: :l, rotation: 90, reflected: true, location: {5, 5})
      shape = Shape.new(pento.name, pento.rotation, pento.reflected, pento.location)

      assert_points_equal(shape.points, [{7, 5}, {6, 5}, {5, 5}, {4, 5}, {4, 4}])
    end

    test "invalid name" do
      pento = Pentomino.new(name: :invalid, rotation: 0, reflected: false, location: {5, 5})

      assert_raise FunctionClauseError, fn ->
        Pentomino.to_shape(pento)
      end
    end
  end

  describe "rotate" do
    test "rotates the pentomino 90 degrees" do
      pento = Pentomino.new(rotation: 0)
      rotated = Pentomino.rotate(pento)

      assert rotated.rotation == 90
    end

    test "rotates the pentomino 180 degrees" do
      pento = Pentomino.new(rotation: 90)
      rotated = Pentomino.rotate(pento)

      assert rotated.rotation == 180
    end

    test "rotates the pentomino 270 degrees" do
      pento = Pentomino.new(rotation: 180)
      rotated = Pentomino.rotate(pento)

      assert rotated.rotation == 270
    end

    test "rotates the pentomino back to 0 degrees" do
      pento = Pentomino.new(rotation: 270)
      rotated = Pentomino.rotate(pento)

      assert rotated.rotation == 0
    end

    test "rotates counterclockwise" do
      pento = Pentomino.new(rotation: 0)
      rotated = Pentomino.rotate(pento, :counterclockwise)

      assert rotated.rotation == 270
    end

    test "rotates counterclockwise from 270 to 180" do
      pento = Pentomino.new(rotation: 270)
      rotated = Pentomino.rotate(pento, :counterclockwise)

      assert rotated.rotation == 180
    end
  end

  describe "flip" do
    test "flips the pentomino" do
      pento = Pentomino.new(reflected: false)
      flipped = Pentomino.flip(pento)

      assert flipped.reflected == true
    end

    test "flips the pentomino back" do
      pento = Pentomino.new(reflected: true)
      flipped = Pentomino.flip(pento)

      assert flipped.reflected == false
    end
  end

  describe "move" do
    setup [:create_pento]

    test "moves the pentomino up", %{pento: pento} do
      moved = Pentomino.up(pento)

      assert moved.location == {8, 7}
    end

    test "moves the pentomino down", %{pento: pento} do
      moved = Pentomino.down(pento)

      assert moved.location == {8, 9}
    end

    test "moves the pentomino left", %{pento: pento} do
      moved = Pentomino.left(pento)

      assert moved.location == {7, 8}
    end

    test "moves the pentomino right", %{pento: pento} do
      moved = Pentomino.right(pento)

      assert moved.location == {9, 8}
    end
  end

  describe "to_shape" do
    test ":i" do
      pento = Pentomino.new(name: :i, rotation: 0, reflected: false, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{5, 3}, {5, 4}, {5, 5}, {5, 6}, {5, 7}])
    end

    test ":l" do
      pento = Pentomino.new(name: :l, rotation: 90, reflected: true, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{7, 5}, {6, 5}, {5, 5}, {4, 5}, {4, 4}])
    end

    test ":t" do
      pento = Pentomino.new(name: :t, rotation: 180, reflected: false, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{6, 6}, {5, 6}, {4, 6}, {5, 5}, {5, 4}])
    end

    test ":y" do
      pento = Pentomino.new(name: :y, rotation: 270, reflected: true, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{3, 5}, {4, 4}, {4, 5}, {5, 5}, {6, 5}])
    end

    test ":n" do
      pento = Pentomino.new(name: :n, rotation: 0, reflected: true, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{5, 3}, {5, 4}, {5, 5}, {4, 5}, {4, 6}])
    end

    test ":p" do
      pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{4, 5}, {5, 4}, {5, 5}, {4, 4}, {6, 5}])
    end

    test ":w" do
      pento = Pentomino.new(name: :w, rotation: 180, reflected: true, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{4, 6}, {4, 5}, {5, 5}, {5, 4}, {6, 4}])
    end

    test ":u" do
      pento = Pentomino.new(name: :u, rotation: 180, reflected: true, location: {5, 5})
      shape = Pentomino.to_shape(pento)

      assert_points_equal(shape.points, [{4, 6}, {6, 6}, {4, 5}, {5, 5}, {6, 5}])
    end
  end

  def create_pento(_content) do
    {:ok, pento: Pentomino.new()}
  end

  defp assert_points_equal(actual, expected) do
    assert MapSet.new(actual) == MapSet.new(expected),
           """
           Points mismatch.

           Actual:   #{inspect(actual)}
           Expected: #{inspect(expected)}
           """
  end
end
defmodule Pento.Game.BoardTest do
  use ExUnit.Case, async: true

  alias Pento.Game.{Board, Pentomino}

  @all_palettes [:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t]
  @small_palette [:u, :v, :p]

  describe "new board" do
    test ":tiny" do
      board = Board.new(:tiny)

      assert_pallette_equal(board.palette, @small_palette)
      assert_points_equal(board.points, for(x <- 1..5, y <- 1..3, do: {x, y}))
    end

    test ":default" do
      board = Board.new(:default)

      assert_pallette_equal(board.palette, @all_palettes)
      assert_points_equal(board.points, for(x <- 1..10, y <- 1..6, do: {x, y}))
    end

    test ":medium" do
      board = Board.new(:medium)

      assert_pallette_equal(board.palette, @all_palettes)
      assert_points_equal(board.points, for(x <- 1..12, y <- 1..5, do: {x, y}))
    end

    test ":wide" do
      board = Board.new(:wide)

      assert_pallette_equal(board.palette, @all_palettes)
      assert_points_equal(board.points, for(x <- 1..15, y <- 1..4, do: {x, y}))
    end

    test ":widest" do
      board = Board.new(:widest)

      assert_pallette_equal(board.palette, @all_palettes)
      assert_points_equal(board.points, for(x <- 1..20, y <- 1..3, do: {x, y}))
    end

    test ":skew" do
      board = Board.new(:skew)

      assert_pallette_equal(board.palette, @all_palettes)

      expected_points =
        for x <- 1..10, y <- 1..6, do: {x, rem(x + y, 2) + y}

      assert_points_equal(board.points, expected_points)
    end
  end

  describe "to_shape" do
    setup [:create_board]

    test "converts board to shape", %{board: board} do
      shape = Board.to_shape(board)

      assert shape.color == :purple
      assert shape.name == :board
      assert_points_equal(shape.points, board.points)
    end

    test "add pentomino to board and convert to shape", %{board: board} do
      pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
      board = %{board | active_pento: pento}
      shape = Board.to_shape(board)

      assert shape.color == :purple
      assert shape.name == :board
      assert_points_equal(shape.points, board.points)
    end

    test "add two pentominoes to board and convert to shape", %{board: board} do
      pento1 = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
      pento2 = Pentomino.new(name: :l, rotation: 180, reflected: true, location: {7, 7})
      board = %{board | active_pento: pento1, completed_pentos: [pento2]}
      shape = Board.to_shape(board)

      assert shape.color == :purple
      assert shape.name == :board
      assert_points_equal(shape.points, board.points)
    end
  end

  describe "active?" do
    setup [:create_board]

    test "active pentomino", %{board: board} do
      pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
      board = %{board | active_pento: pento}

      assert Board.active?(board, :p)
    end

    test "no active pentomino", %{board: board} do
      assert not Board.active?(board, :p)
    end

    test "active pentomino with different name", %{board: board} do
      pento = Pentomino.new(name: :p, rotation: 90, reflected: false, location: {5, 5})
      board = %{board | active_pento: pento}

      assert not Board.active?(board, :l)
    end
  end

  defp create_board(_content) do
    board = Board.new(:default)
    {:ok, %{board: board}}
  end

  defp assert_points_equal(actual, expected) do
    assert MapSet.new(actual) == MapSet.new(expected),
           """
           Points mismatch.

           Actual:   #{inspect(actual)}
           Expected: #{inspect(expected)}
           """
  end

  defp assert_pallette_equal(actual, expected) do
    assert MapSet.new(actual) == MapSet.new(expected),
           """
           Palette mismatch.

           Actual:   #{inspect(actual)}
           Expected: #{inspect(expected)}
           """
  end
end

Now for the skewed rectangle

  def new(:skew), do: new(:all, skewed_rect())
  def new(_), do: new(:default)

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

  defp skewed_rect() do
    for x <- 1..10, y <- 1..6, do: {x, rem(x + y, 2) + y}
  end

Don’t worry the test for this are in the tests above.


Now for the other direction for the rotation.

  def rotate(%{rotation: degrees} = p, clockwise \\ :clockwise) do
    degrees = if clockwise == :counterclockwise, do: 360 - 90 + degrees, else: degrees + 90
    %{p | rotation: rem(degrees, 360)}
  end

This will work for either way. Also tests for this are above.