Interlude: Tic Tac Toe
At this point if you have been reading sequentially through these chapters you know enough to put together a simple tic-tac-toe game.
The Game Board
The board is represented by an array of 9 cells. Cell 0 is the top left square, and cell 8 is the bottom right.
Each cell will contain nil, an :X or an :O.
Displaying the Board
The DisplayBoard component displays a board. DisplayBoard accepts a board param, and will fire back a clicked_at event when the user clicks one of the squares.
A small helper function draw_squares draws an individual square which is displayed as a BUTTON. A click handler is attached which will fire the clicked_at event with the appropriate cell id.
Notice that DisplayBoard has no internal state of its own. That is handled by the DisplayGame component.
class DisplayBoard < HyperComponent
  param :board
  fires :clicked_at
  def draw_square(id)
    BUTTON(class: :square, id: id) { board[id] }
    .on(:click) { clicked_at!(id) }
  end
  render(DIV) do
    (0..6).step(3) do |row|
      DIV(class: :board_row) do
        (row..row + 2).each { |id| draw_square(id) }
      end
    end
  end
endThe Game State
The DisplayGame component has two state variables:
@historywhich is an array of boards, each board being the array of cells.@stepwhich is the current step in the history (we begin at zero)
@step and @history allows the player to move backwards or forwards and replay parts of the game.
These are initialized in the before_mount callback. Because Ruby will adjust the array size as needed and return nil if an array value is not initialized, we can simply initialize the board to an empty array.
There are three reader methods that read the state:
playerreturns the current player's token. The first player is always:Xso even stepsare
:X, and odd steps are:O.currentreturns the board at the current step.historyuses state_reader to encapsulate the history state.
Encapsulated access to state in reader methods like this is not necessary but is good practice
class DisplayGame < HyperComponent
  before_mount do
    @history = [[]]
    @step = 0
  end
  def player
    @step.even? ? :X : :O
  end
  def current
    @history[@step]
  end
  state_reader :history
endCalculating the Winner Based on the Game State
We also have a current_winner? method that will return the winning player or nil based on the value of the current board:
class DisplayGame < HyperComponent
  WINNING_COMBOS = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ]
  def current_winner?
    WINNING_COMBOS.each do |a, b, c|
      return current[a] if current[a] && current[a] == current[b] && current[a] == current[c]
    end
    false
  end
endMutating the Game State
There are two mutator methods that change state:
handle_click!is called with the id of the square when a user clicks on a square.jump_to!moves the user back and forth through the history.
The handle_click! mutator first checks to make sure that no one has already won at the current step, and that no one has played in the cell that the user clicked on. If either of these conditions is true handle_click! returns, no mutation is signaled and nothing changes.
If we had wanted to return AND signal a state mutation we would use the Ruby
nextkeyword instead ofreturn.s
To update the board handle_click! duplicates the squares; adds the player's token to the cell; makes a new history with the new squares on the end, and finally updates the value of @step.
We like to use the convention where practical of ending mutator methods with a bang (!) so that readers of the code are aware that these will change state.
class DisplayGame < HyperComponent
  mutator :handle_click! do |id|
    board = history[@step]
    return if current_winner? || board[id]
    board = board.dup
    board[id] = player
    @history = history[0..@step] + [board]
    @step += 1
  end
  mutator(:jump_to!) { |step| @step = step }
endThe Game Display
Now we have a couple of helper methods to build parts of the game display.
movescreates the list items that allow the user to move back and forth through the history.statusprovides the play state
class DisplayGame < HyperComponent
  def moves
    return unless history.length > 1
    history.length.times do |move|
      LI(key: move) { move.zero? ? "Go to game start" : "Go to move ##{move}" }
        .on(:click) { jump_to!(move) }
    end
  end
  def status
    if (winner = current_winner?)
      "Winner: #{winner}"
    else
      "Next player: #{player}"
    end
  end
endAnd finally our render method which displays the Board and the game info:
class DisplayGame < HyperComponent
  render(DIV, class: :game) do
    DIV(class: :game_board) do
      DisplayBoard(board: current)
      .on(:clicked_at, &method(:handle_click!))
    end
    DIV(class: :game_info) do
      DIV { status }
      OL { moves }
    end
  end
end
&methodturns an instance method into a Proc rather than having to say{ |id| handle_click(id) }
Summary
This small game uses everything covered in the previous sections: HTML Tags, Component Classes, Params, Events and Callbacks, and State. The project was derived from this React tutorial: https://reactjs.org/tutorial/tutorial.html. You may want to compare our Ruby code with the React original.
The following sections cover reference materials, and some advanced information. You may want to skip to the HyperState section which will use this example to show how state can be encapsulated, extracted and shared resulting in easier to understand and maintain code.
Last updated
Was this helpful?