In today's episode we're going to work on the Tetris clone a bit more. Let's get started.

Project

I've tagged the code before this episode as before_episode_115 if you want to follow along. It's what we did in the last episode, plus shape randomization.

Drawing the board

First off, it would be nice to draw the board boundaries. Tetris boards tend to be 10x20, so we'll define that in a module attribute. Open up the window module:

@board_size %{
  x: 10,
  y: 20
}

# We're also going to add the falling piece's location to the State:

defmodule State do
  defstruct shape: :ell, rotation: 0, x: 5, y: 0
end

# Next we'll make the window larger by default:
frame = :wxFrame.new(wx, -1, @title, size: {1000,1000})

# Next, draw the board when we do the game draw:
def do_draw(state, dc) do
  #...
  draw_board(canvas)
  draw_colored_shape(canvas, state.shape, Enum.at(shape, rotation), state.x, state.y)
end

# Now we have to refactor a little bit.  Presently, we set the brush, then we
# draw a square.  This is more stateful than we need to be.  Instead, we'd like
# to pass the brush into the draw_square function itself:
def draw_shape(canvas, shape, x, y, brush) do
  # Specify position in 'grid units'
  for {row, row_i} <- Enum.with_index(shape) do
    for {col, col_i} <- Enum.with_index(row) do
      if(col == 1) do
        draw_square(canvas, x + col_i , y + row_i, brush)
      end
    end
  end
end

def draw_square(canvas, x, y, brush) do
  :wxGraphicsContext.setBrush(canvas, brush)
  true_x = @side * x
  true_y = @side * y
  :wxGraphicsContext.drawRectangle(canvas, true_x, true_y, @side, @side)
end

def draw_colored_shape(canvas, brush_name, shape, x, y) do
  brush = brush_for(brush_name)
  draw_shape(canvas, shape, x, y, brush)
end

# Now we can specify colors for squares that aren't part of a shape, which of
# course is what our board will be composed of

# We'll define the brush for the board:
def brush_for(:board), do: :wxBrush.new({0, 0, 0, 255})

# Finally, implement draw_board:
def draw_board(canvas) do
  brush = brush_for(:board)
  # Draw the bottom of the board
  for x <- (0..@board_size.x) do
    draw_square(canvas, x, @board_size.y, brush)
  end
  # draw the sides of the board
  for y <- (0..@board_size.y) do
    draw_square(canvas, 0, y, brush)
    draw_square(canvas, @board_size.x, y, brush)
  end
end

Let's run what we have so far. I tend to map comma comma to it:

:map ,, :!iex -S mix<cr>

Alright, run it and use a and d to rotate the shape.

Piece movement with arrow keys

Next let's get piece movement working with the arrow keys.

# We'll add some module attributes for various keycodes
@a 65
@d 68
@right_arrow 316
@left_arrow 314
@up_arrow 315

Let's replace the existing keyCode maps for a and d with our new module attributes so our code reads more easily

wx(event: wxKey(keyCode: @a)) ->
  state = %State{state | rotation: rem(state.rotation + 1, 4)}
  loop(state, panel)
wx(event: wxKey(keyCode: @d)) ->
  state = %State{state | rotation: rem(state.rotation - 1, 4)}
  loop(state, panel)

Next, let's support the arrow keys for movement:

wx(event: wxKey(keyCode: @left_arrow)) ->
  state = %State{state | x: state.x - 1}
  loop(state, panel)
wx(event: wxKey(keyCode: @right_arrow)) ->
  state = %State{state | x: state.x + 1}
  loop(state, panel)

Go ahead and run it and play with the left and right arrows a bit to see it working...

Next, I'd really like the up arrow to rotate clockwise, so we'll add that:

wx(event: wxKey(keyCode: @up_arrow)) ->
  state = %State{state | rotation: rem(state.rotation + 1, 4)}
  loop(state, panel)

Try it out...

Ticking the game loop

Next we'd like our pieces to fall. We'll implement that pretty trivially. First, we'll define the rate at which our game logic ticks:

@interval 500

Next, before we start our input loop we'll start a game tick interval using Erlang's timers:

def do_init(_config) do
  #...
  :timer.send_interval(@interval, self, :tick)
  #...
end

Next, we'll support receiving that message in our loop:

      :tick ->
        state = tick_game(state)
        loop(state, panel)

Finally, our game logic goes in the tick_game function:

def tick_game(state) do
  %State{state | y: state.y + 1}
end

Summary

So that's it for today. We now have something a lot more like a tetris game. There's some collision detection to be worked out obviously, as well as line completion and scoring, but in general we're in pretty good shape for a 30 minute hack. See you soon!

Resources