In today's episode, we're just going to make a new piece come down every time the current piece hits the bottom of the board. Nothing fancy yet, but it's another piece of the game we can implement fairly easily off of the back of the work we did in the last episode.

Project

I've tagged the repo with before_episode_118 if you're following along.

Let's just get into it. We'd like to write a test to verify that when the current piece hits the bottom, we get a new piece and our x/y in the state are reset.

Now, in Tetris the next piece is known ahead of time, so we'll add a next_piece key to the Game State:

defmodule Extris.Game.State do
  defstruct shape: :ell, rotation: 0, x: 5, y: 0, next_shape: :ell
end

This is a test of the game logic, rather than user interaction, so open up test/extris/game_test.exs:

defmodule Extris.GameTest do
  use ExUnit.Case
  alias Extris.Game
  alias Extris.Game.State

  test "The next piece becomes the current piece when the current piece hits the bottom" do
    state = %State{shape: :ell, next_shape: :oh, y: 20}
    new_state = Game.tick_game(state)
    assert new_state.shape == :oh
  end
end

Go ahead and run the tests, and they'll fail. Open up the Game module and we'll make this happen:

  def tick_game(state=%State{y: y}) when y == 20 do
    %State{shape: state.next_shape, x: 5, y: 0}
  end
  def tick_game(state) do
    %State{state|y: state.y + 1}
  end

Alright, so run the tests again and they'll pass. Let's playtest it real quick.

(( do it ))

Alright, so obviously we don't really want it to be when the shape hits y 20, but rather when it hits 20 - the height of the shape. Let's add a test to our Shapes module to confirm we can get the height:

  test "an oh always has a height of 2" do
    for rotation <- 0..3 do
      assert Shapes.height(:oh, rotation) == 2
    end
  end

  test "an ell has a height of 2 or 3" do
    assert Shapes.height(:ell, 0) == 3
    assert Shapes.height(:ell, 1) == 2
    assert Shapes.height(:ell, 2) == 3
    assert Shapes.height(:ell, 3) == 2
  end

Run the tests and they'll fail. Open up the shapes module and add the height function:

  def height(shape, rotation) do
    shapes[shape]
    |> Enum.at(rotation)
    |> length
  end

OK, so open up the game test and let's make sure that it gives us a new piece when the current piece is height away from the bottom:

  test "The next piece becomes the current piece when the current piece hits the bottom" do
    state = %State{shape: :ell, next_shape: :oh, y: 17}
    new_state = Game.tick_game(state)
    assert new_state.shape == :oh
  end

Run the tests, and they'll fail. Now open up the Game module and modify the logic for providing the next piece. We started out with a guard, but we just can't use it with our height function, so we'll refactor it using cond.

  def tick_game(state) do
    cond do
      Shapes.height(state.shape, state.rotation) + state.y > 19 ->
        %State{state | shape: state.next_shape, x: 5, y: 0}
      true ->
        %State{state | y: state.y + 1}
    end
  end

Go ahead and run the tests, and they'll pass. Let's do a quick playtest.

Alright, the only other thing we need to do this session is to move the random-next-piece logic into the Shapes module and then use it appropriately.

(( move Window.random_shape to Shapes.random and make it public ))

(( open up lib/extris/window.ex ))

  alias Extris.Shapes
  #...
  Extris.Game.loop(%State{shape: Shapes.random, next_shape: Shapes.random}, frame)

Next, let's seed a new next_shape when we hit the bottom. Open up the Game module again:

  alias Extris.Shapes
  # ...
  
  def tick_game(state) do
    cond do
      Shapes.height(state.shape, state.rotation) + state.y > 19 ->
        %State{state | shape: state.next_shape, x: 5, y: 0, next_shape: Shapes.random}
      true ->
        %State{state | y: state.y + 1}
    end
  end

Alright, with that let's do a quick play test to make sure we're getting new random shapes.

(( do it ))

Summary

In today's episode we just provided the game logic for getting next pieces and showing them when the current piece drops. Again, it's just incrementally more like an actual game. See you soon!

Resources