Amy R. Johnson

Making a Simple Browser Game With Sinatra

This post details how I made a simple tic-tac-toe game using Sinatra.

The first step is creating the code that contains the logic of the game. There are a ton of different ways to do this, and Sinatra doesn’t care which way you choose. I decided to make a game class, a board class, and a player class. The game class contains the logic pertaining to turns and the running of the game, initialized with an empty board and two players. The board class stores the gameboard as a 3x3 matrix, updates itself with every turn, and determines when the game has ended in a win, loss, or tie. The player class stores the player attributes like name and marker.

The next step is to move the Ruby logic into a Sinatra application. To make things a little easier, I used Ratpack, a Sinatra boilerplate by Ashley Williams with support for activerecord, sqlite, and twitter bootstrap (even though we won’t be using the database functionality for this application). In the Ratpack schema the models live in the lib directory.

Now we can ignore our tic-tac-toe logic for a little bit and get our pages set up. First we need to define a class that inherits from Sinatra so that we can use all of Sinatra’s functionality in our application. Next create the routes. Each page needs a route in order to render, which we establish here with get statements.

1
2
3
4
5
6
7
8
9
10
11
12
13
# app.rb

module TicTacToeProject
  class App < Sinatra::Application

    #routes
    get '/' do
      erb :index
    end

    get '/play' do
      erb :play
    end

The erb :filename statements are telling Sinatra what to render. In this case that’s the erb files from my views folder. In order to make a board show up in the browser, I need to display each square from my board in my html, which is easy to do using erb. I chose to format the board as three rows with three columns each, but you could just as easily use a table or some other method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- views/play.erb -->

<div class="board">
  <div class="row center">
    <div class="col-md-4">
      <h2><%= game.square(0,0) %></h2> <!-- returns the value in square [0,0] -->
    </div>
    <div class="col-md-4 v">
      <h2><%= game.square(0,1) %></h2>
   </div>
    <div class="col-md-4">
      <h2><%= game.square(0,2) %></h2>
    </div>
  </div>

The only problem is, this erb file has no idea what game is. I need to pass the instance of my game class in my route in order for my views to render it. That looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app.rb

module TicTacToeProject
  class App < Sinatra::Application
    @@game = PlayGame.new

    #routes
    get '/' do
      @game = @@game
      erb :index
    end

    get '/play' do
      @game = @@game
      erb :play
    end

Once I’ve created views and routes for all my pages I’m most of the way there, but how do I get input from the user to allow them to play the game? One simple way to receive input from a user and store it for access later is to include a form in the view.

1
2
3
4
5
6
<!-- views/play.erb -->

      <form action="/move" method="get">
        <input type="text" name="move">
        <input type="Submit" value="Play">
      </form>

The text input of the form, which I named “move”, is stored in Sinatra’s params hash automatically. I can access whatever the user typed elsewhere in my program using the params[:move] variable. Now I can update the board with the player’s move before loading the erb in my route.

1
2
3
4
5
6
7
# app.rb

    get '/move' do
      @@game.play(params["move"]) #updates the gameboard with the player's chosen move
      @game = @@game
      erb :move
    end

We’re almost done! Now we can get a move from the player, update the board, and render a page displaying the updated board. However, if you were to run the code using the Shotgun gem you’d soon discover a problem. Every time the player moves the board will update, but when the player goes to make a second move they’ll find their previous move has been erased and the board is blank! The reason for this is that every time Shotgun rereads the Sinatra application to refresh the content it generates a new instance of the class App. So every time it runs, it creates a new instance of the PlayGame class, making a new game rather than updating an in-progress game. Try using Rackup instead, and the game should work as intended.

And now we have a working game! Fix up the views with twitter bootstrap, and we’re all done. You can check out my finished version at amystictactoegame.herokuapp.com.