Home Posts Post Search Tag Search

LiveView 32 - Chapter 12: Your Turn
Published on: 2026-02-19 Tags: elixir, Blog, Side Project, LiveView, Game Site, Phoenix, SVG, Graphics

Your Turn

You have done a lot here, now you can try somethings on your own.

Build a Game Instructions Component

Define a new function component, GameInstructions.show/1, that renders a paragraph with some game instructions. Then, render this component within the GameLive view, between the title and the board.


First you should create a new file and then a component and then we can fill it out with some render. pento/components/game_instructions.ex

defmodule PentoWeb.GameInstructions do
  use Phoenix.Component

  def render(assigns) do
    ~H"""
    <div class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">How to Play</h1>
      <p class="mb-4">
        The goal of Pento is to fill the board with the pentominoes in your palette.
        Each pentomino is made up of five squares, and there are twelve different
        shapes to choose from. You can rotate and flip the pieces to fit them into
        the board, but you cannot overlap them or leave any empty spaces.
      </p>
      <p class="mb-4">
        To move a piece, simply click on it and then click on the square where you
        want to place it. You can also use the arrow keys to move the active piece
        around the board. If you get stuck, you can click on a piece in your palette
        to see where it can be placed on the board.
      </p>
      <p class="mb-4">
        The game is won when all pieces are placed on the board without any overlaps
        or empty spaces. Good luck, and have fun playing Pento!
      </p>
    </div>
    """
  end
end

Now we need to add that to the render for the game_live liveview

defmodule PentoWeb.GameLive do
  use PentoWeb, :live_view

  import PentoWeb.GameLive.Component
  alias PentoWeb.GameLive.{Color, Board}
  alias PentoWeb.GameInstructions

  def mount(%{"puzzle" => puzzle}, _session, socket) do
    {:ok, assign(socket, puzzle: puzzle)}
  end

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
      <GameInstructions.show />
      <.live_component module={Board} puzzle={@puzzle} id="board-component" />
    </section>
    """
  end
end

Add a New Puzzle

Now we want to add a new puzzle size :small. So make sure that you are adding in all the needed pattern match and then trace the following:


• How does the PentoWeb.GameLive.Board component know the shape of the
puzzle board?
• How does the PentoWeb.GameLive.Board component render the list of points
that make up the puzzle board?
• How does the PentoWeb.GameLive.Board component know what shapes make
up the palette?
• How does the PentoWeb.GameLive.Board component render the palette?
• How does the .palette/1 function component render the correct list of
shapes?

When we page is first mounted we have the puzzle as a param within the URL. We then assign the puzzle param to an atom and then make reference to the component id. Once that is in the sockets we are able to assign the board to the socket. That will take care of the active and completed pentos, and then get to the board. We pass the atom to the Board.new/1 and that returns the pallet and the points.


We now have the board.points within a shape struct that is then sent to the to_shapes, that will then take that out and then put the active/completed into a list of shapes with the board on the top so it renders it below all others.


The Board.new/1 also adds in a palette key pair, that is populated by a call to palette with the board size atom.


There is an other component that uses the palette key pair, that is then passed to the .palette/1 component. There is then its passed to the .palette_shape/1 within a for loop that goes through all the palette shapes. There is a test for if the piece is active and if it is already part of the completed. Check the PentoWeb.GameLive.Colors for that logic.


This is answered in the previous answer.

Build a Pentomino Control Panel

We want to build a new component that will work with other SVG shapes. We want a series of 4 triangles that will be in different orientations to represent up down left and right. Let’s head to the component.ex to add in a new .control_panel/1.

  attr(:viewBox, :string)
  slot(:inner_block, required: true)

  def control_panel(assigns) do
    ~H"""
    <svg viewBox={@viewBox}>
      <defs>
        <polygon id="triangle" points="6.25 1.875, 12.5 12.5, 0 12.5" />
      </defs>
      <slot />
    </svg>
    """
  end

  attr(:x, :integer, required: true)
  attr(:y, :integer, required: true)
  attr(:rotate, :integer, required: true)
  attr(:fill, :string, required: true)

  def triangle(assigns) do
    ~H"""
    <use x={@x} y={@y} transform="rotate( @rotate, 100, 100)" href="#triangle" fill={@fill} />
    """
  end

So I wanted to go over a few things for this as it took a while for me to really understand what I was doing. When you send in the x and the y into the .triangle/1 you are giving the location of the triangle. Then when you are trying to rotate it you need to specify the location where you will rotate around. So while the book gave me some good ideas and a start this is what I ended up with.

  attr(:viewBox, :string)
  slot(:inner_block, required: true)

  def control_panel(assigns) do
    ~H"""
    <svg viewBox={@viewBox}>
      <defs>
        <polygon id="triangle" points="6.25 1.875, 12.5 12.5, 0 12.5" />
      </defs>
      {render_slot(@inner_block)}
    </svg>
    """
  end

  attr(:x, :integer, required: true)
  attr(:y, :integer, required: true)
  attr(:rotate, :integer, required: true)
  attr(:fill, :string, required: true)

  def triangle(assigns) do
    local_cx = 6.25
    local_cy = 8.958

    ~H"""
    <use
      x={@x}
      y={@y}
      transform={"rotate(#{@rotate} #{(@x + local_cx)} #{(@y + local_cy)})"}
      href="#triangle"
      fill={@fill}
    />
    """
  end

Okay so now that we have that we need to add that to the render of the game_live.ex

  def render(assigns) do
    ~H"""
    <div id={@id} phx-window-keydown="key" phx-target={@myself}>
      <.canvas view_box="0 0 200 70">
        <%= for shape <- @shapes do %>
          <.shape
            points={shape.points}
            fill={color(shape.color, Board.active?(@board, shape.name), false)}
            name={shape.name}
          />
        <% end %>
      </.canvas>
      <hr />
      <.palette
        shape_names={@board.palette}
        completed_shape_names={Enum.map(@board.completed_pentos, & &1.name)}
      />
      <.control_panel viewBox="0 0 200 40">
        <.triangle x={20} y={0} rotate={0} fill={color(:orange, true, true)} />
        <.triangle x={30} y={10} rotate={90} fill={color(:orange, true, false)} />
        <.triangle x={20} y={20} rotate={180} fill={color(:orange, true, false)} />
        <.triangle x={10} y={10} rotate={270} fill={color(:orange, true, false)} />
      </.control_panel>
      <hr />
    </div>
    """
  end