Don't miss

I created an open source development tool to quickly & dynamically mock API endpoints. Check it out at GitHub: DuckRails

Heads up

I will no longer maintain arubystory. For new posts, find me at my personal blog. CU there!

Sunday, February 22, 2015

Creating the Hangman game



In this tutorial we are going to create Hangman, a simple game in which the players try to find a word that our application chooses, by selecting letters.

We will use:

Rails (version 4): you know what this is otherwise you are in the wrong place
Foundation: for the looks & feels
Font Awesome: for icons

Overview:

The application will actually have two pages:
  1. The welcome page
  2. The game page
In the welcome page we will give user the option:
  • to start a new game
  • to continue a game he/she has already started
In the game page, user will be presented with the game "board" with the following sections:
  • the gallows container
  • the word letter boxes container
  • the latin characters container
  • buttons to return to home page or start a new game
We won't have any kind of authentication/authorisation and we won't need active record to persist any data. All user and game state will be kept in session.

The code for this part of the tutorial is available at GitHub.

Hands on


Let's begin. First we are going to create the application. So navigate to your projects directory and shoot:
rails new hangman -O

Note that we used the -O option that skips Active Record files.

Cleanup


We will now cleanup the application from things we will not use:

Turbolinks:

  • Open the Gemfile and remove the turbolinks related line

  • Open app/assets/javascripts/application.js and remove the turbolinks requirement









  • Open app/views/layouts/application.html.erb and remove the data-turbolinks-track options












CoffeeScript:
  • Open the Gemfile again and this time remove the coffee script related line










After these changes, open a console and run a bundle from our application's root directory just to make sure we didn't brake anything.
bundle

Now we will start the server so as to see our progress while we do stuff. From the application root directory execute:
bundle exec rails server

All good.

Controllers


We will create two controllers.

HomeController: this one is going to be as simple as having only one action, the one for the welcome page. Create it with the following:
rails generate controller home index

GamesController: this one will have the good stuff with actions for creating, showing, updating and cancelling a game.
rails generate controller games new show update destroy

These commands created a bunch of files besides the ones we will use but don't worry, we'll cleanup later.

Routes


We will now define the routes of the application. 

Open config/routes.rb and remove any automatically generated content you find there. After this, your routes.rb file should look like this:








We will define the root path, a.k.a. the welcome page to HomeController's index action, so add:
root :to => 'home#index'

We will define our game resource with the appropriate routes by adding:
resource :game, :only => [:new, :show, :update, :destroy]

Your routes.rb file should now contain the following:

Rails.application.routes.draw do
  root :to => 'home#index'
  resource :game, :only => [:new, :show, :update, :destroy]
end

Now execute the rake task that shows the application's routes and we'll shortly explain what we expect to be done by each one.
rake routes

You should see something like this:








Explanation:

  1. This is our welcome page, the first page users will view
  2. This is the route that will lead to a new game creation
  3. This route will present the user's current game
  4. This route will be used for serving the user letter selections
  5. Same as 4
  6. This route will be used for cancelling the current game
So far, this is all we need. We'll come back to this file in case we need another action/route later on.

Model

We need a class to keep a game's state and that will be the Game class. 

Create a file under app/models with name game.rb
In the rest of the tutorial, this class will also be referenced as "model". Let's see what we need this class to do.
  1. Define the user's max failed attempts
  2. Keep the word to be guessed
  3. Keep the user's letter selections
  4. Initialize a game
  5. Answer the number of failed letter selections
  6. Answer if user has guessed the word or not
  7. Answer if the game is finished or not
  8. Select a letter
Define the user's max failed attempts
User can't continue selecting letters forever cause that would not be a game but an exercise for mouse clicks. We have to define a limit of failed attempts. So add the following constant:
MAX_FAILED_ATTEMPTS = 5

Keep the word to be guessed
Add an attribute accessor for the word:
attr_accessor :word

Keep the user's letter selections
Add an attribute accessor for the selected letters:
attr_accessor :selected_letters

Initialize a game
We will define some stuff that we want to be executed upon a Game's creation:
def initialize
  @word = 'Hangman'.upcase
  @selected_letters = []
end

For now, all of our games are going to choose the word "Hangman" but later on we'll create a mechanism so as the words to be randomly selected from a source. We also initialize the selected letters with an empty array.

Answer the number of failed letter selections
Add a select! method which accepts a letter and returns true or false based on whether the word contains the letter or not:
def failed_attempts
  selected_letters.select { |letter|
    !word.include?(letter)
  }.size
end
Here we iterate through the selected letters and we count how many of them are included in the game's word.

Answer if user has guessed the word
Add a guessed? method:
def guessed?
  (word.split('') - selected_letters).empty?
end
Here we convert the word to an array of its characters and then we "subtract" the selected letters from this array. If the result is empty, it means that user has selected all the word's letters a.k.a. he/she guessed the word.

Answer if the game if finished
Add a finished? method:
def finished?
  failed_attempts >= MAX_FAILED_ATTEMPTS || !guessed?
end

Here we answer that a game has finished if either the failed attempts limit has been reached or user has successfully guessed the word.

Select a letter
Add the method for letter selection:
def select!(letter)
  raise GameOverError if finished?
  selected_letters << letter unless selected_letters.include? letter
  word.include? letter
end
Here we raise an error in case the game is finished. We will handle this error later in our controller. We add the selected letter in our game's state and we answer with true or false based on whether contains this letter or not.

Note: 
  • we have to define this GameOverError somewhere otherwise our code is invalid. So prepend to the class' code:

class GameOverError < StandardError; end
  • we have to add the following lines in order to be able to serialize our model properly in session. For more information you can visit this.

include ActiveModel::AttributeMethods, ActiveModel::Serializers::JSON

def attributes
  {'word' => nil,
   'selected_letters' => nil}
end

def attributes=(hash)
  hash.each do |key, value|
    send("#{key}=", value)
  end
end

Your model should now look like this:
class Game
  include ActiveModel::AttributeMethods, ActiveModel::Serializers::JSON

  class GameOverError < StandardError; end

  MAX_FAILED_ATTEMPTS = 5

  attr_accessor :word

  attr_accessor :selected_letters

  def initialize
    @word = 'Hangman'
    @selected_letters = []
  end

  def attributes
    {'word' => nil,
     'selected_letters' => nil}
  end

  def attributes=(hash)
    hash.each do |key, value|
      send("#{key}=", value)
    end
  end

  def failed_attempts
    selected_letters.select { |letter|
      !word.include?(letter)
    }.size
  end

  def guessed?
    (word.split('') - selected_letters).empty?
  end

  def finished?
    failed_attempts >= MAX_FAILED_ATTEMPTS || guessed?
  end

  def select!(letter)
    raise GameOverError if finished?
    selected_letters << letter unless selected_letters.include? letter
    word.include? letter
  end
end

It's about time to configure the application to use Foundation and FontAwesome in order to start creating our views.

Foundation


We will use Foundation mainly because of its excellent responsive grid. Besides that though, we will use some of its additional cool features (like button styles & utilities).

First we need to add the foundation gem to our Gemfile. Add this line:
gem 'foundation-rails'
and from the application's root directory execute bundle to have the gem installed
bundle
Next, we will setup it for application executing the following command:
rails generate foundation:install

You will be prompted with an overwrite message which actually says that it's going to change our application layout file.
Overwrite /blah/blah/hangman/app/views/layouts/application.html.erb? (enter "h" for help) [Ynaqdh].
We trust so press enter to continue and that's it. Foundation is successfully installed and configured for Hangman.

Font Awesome


We will use Font Awesome because of its great icons and the fact that we can change their size & color with the common color and font size css rules a.k.a. Font Awesome rules!

Again, we need to add the font awesome gem to our Gemfile. Add this line:
gem 'font-awesome-rails'
and execute the bundle command from the console to install the gem.

Next, open your app/stylesheets/application.css file and add the following line:
*= require font-awesome
Your file should look like this now:
/*
 *= require_tree .
 *= require_self
 *= require foundation_and_overrides
 *= require font-awesome
 */

That's it, we can now start building our views.

Welcome page


In this page we would like to show user:
  • a welcome message
  • a button to start a new game
  • a button to continue a game if he comes back later
Here's a draft:








Restart your server and from a browser navigate to http://localhost:3000
You should be viewing something like this:








Let's change it. As the page itself says, open app/views/home/index.html.erb and remove all of its contents.

We are going to use Foundation's default grid:
Imaging that you have a notebook who's lines are split to 12 same-length sections.
  • Each time we want to write something to a new line, we have to use a div with the "row" css class.
  • Each time we want to write something inside a line, we have to define how many of the 12 "sections" of the line we want to use, so we use a div with the "large-x", "medium-x" or "small-x" css class followed by the "columns" css class. As you might already assumed, "x" is the number of the sections to use. Eg: if we set x to 12 that means that our content will take over the whole line. If we set x to 6 then our content will take over the half line leaving place for other content to be added to the rest half of the line. The large/medium/small part of the classes is described in the next bullet.
  • Suppose you have four buttons with large text (eg: 'Press me, I'm a button') and you want users with wide screens (large breakpoint) to view all four buttons each one next to each other. This is the case that you must define the "large" mode of the grid. And since you have four buttons then you calculate that each button must occupy 12 / 4 = 3 sections. So each button has to be added in the same row and in a div with a css class "large-3":
<div class="row>
  <div class="large-3 columns">
    <button>xxx</button>
  </div>
  <div class="large-3 columns">
    <button>xxx</button>
  </div>
  <div class="large-3 columns">
    <button>xxx</button>
  </div>
</div>
And here's what you would have created:




Now, you want users with not so large screens (medium breakpoint), like tablet users to see these buttons in pairs of two since there is not enough space in their screens to see them in the same row. All you have to do is to add another class to your "column" divs defining how many sections of the row the div should occupy for these users (a.k.a. for the medium breakpoint). Since you want two buttons for each row, the x in this case should be 12 / 2 = 6:
<div class="row>
  <div class="medium-6 large-3 columns">
    <button>xxx</button>
  </div>
  <div class="medium-6 large-3 columns">
    <button>xxx</button>
  </div>
  <div class="medium-6 large-3 columns">
    <button>xxx</button>
  </div>
</div>
And here's your view for the medium breakpoint:






Finally, you want users with small screens like mobile users (small breakpoint) to view each button on each own row. Again, all you have to do is to add another class to the "columns" divs this time with x set to 12 (whole row):

<div class="row>
  <div class="small-12 medium-6 large-3 columns">
    <button>xxx</button>
  </div>
  <div class="small-12 medium-6 large-3 columns">
    <button>xxx</button>
  </div>
  <div class="small-12 medium-6 large-3 columns">
    <button>xxx</button>
  </div>
</div>









For our welcome page, we want the welcome message to occupy the whole line in any breakpoint. The buttons should occupy the half row for medium & large breakpoints but let's make them occupy the whole row for the small breakpoint. Translated to code, add the following to the index.html.erb file:
<div class="row">
    <div class="small-12 columns text-center">
        <h1>Welcome to Hangman!</h1>
    </div>
</div>

<div class="row">
    <div class="small-12 medium-6 columns">
        <button class="button expand">New game</button>
    </div>

    <div class="small-12 medium-6 columns">
        <button class="button expand">Continue game</button>
    </div>
</div>
Notes:

  • We used the foundation's "text-center" utility class in order to center the message in its column.
  • We used the foundation's "button" class for cool button style and the "expand" class to force the buttons to occupy the width of their containers.
  • We didn't define anything for the large breakpoint and that actually says "ok, use the bigger of the classes that were defined" thus the medium for our case.
Now navigate to the application's home page and voila:








As we already decided at the beginning of the tutorial, the games won't be persisted to any database but will be kept in the session. This of course is not a guideline, we do it for simplicity reasons. In real world applications it is strongly recommended to avoid this approach.

Anyways, we are going to keep each user's game as JSON to session under ":serialized_current_game" meaning that if session[:serialized_current_game] is blank, then the user hasn't started any game.

We are going to create some methods in our application's controller for reading and updating the current game from/to session. Open app/controllers/application.rb and change its contents to the following:
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  helper_method :current_game

  def current_game
    @current_game ||= load_current_game
  end

  def set_current_game(game)
    @current_game = game
    session[:serialized_current_game] = game.present? ? game.to_json : nil
  end

  def update_current_game
    set_current_game @current_game
  end

  protected

  def load_current_game
    Game.new.from_json(session[:serialized_current_game]) if session[:serialized_current_game].present?
  end
end

Note:
  • current_game: loads and keeps the current game to the @current_game variable
  • set_current_game: sets both the @current_page variable & the session's serialized current game with the given game
  • update_current_game: this one will serialize the current game to session, we will use it upon user letter selection
Time to replace the welcome page's buttons with functional ones.

<div class="row">
    <div class="small-12 columns text-center">
        <h1>Welcome to Hangman!</h1>
    </div>
</div>

<div class="row">
    <div class="small-12 medium-6 columns">
        <%= link_to new_game_path, :class => 'button expand' do %>
            <i class="fa fa-play"> New game</i>
        <% end %>
    </div>

    <% if current_game.present? && !current_game.finished? %>
        <div class="small-12 medium-6 columns">
            <%= link_to game_path, :class => 'button expand' do %>
                <i class="fa fa-refresh"> Continue game</i>
            <% end %>
        </div>
    <% end %>
</div>

We replaced the buttons with links navigating to actual pages of the application and we no longer show the "Continue game" button unless there is one.
We also used FontAwesome icons. Yes, it's so easy as to just add an "i" tag with a class of you desired icon. View all available icons here.

Now reload the page to your browser. Can you imagine the problem? No? Here:







The layout seems buggy when we don't render the "Continue game" button. No worries though. We can change the "New game"'s button class in this case so that it can occupy the whole line! How? Change its container's class so that if there is no current game to skip the "medium-6" one and default to "small-12". Here's the updated code:

<div class="row">
    <div class="small-12 columns text-center">
        <h1>Welcome to Hangman!</h1>
    </div>
</div>

<div class="row">
    <div class="small-12 <%= current_game.present? current_game.present? && !current_game.finished? ? 'medium-6' : '' %> columns">
        <%= link_to new_game_path, :class => 'button expand' do %>
            <i class="fa fa-play"> New game</i>
        <% end %>
    </div>

    <% if session[:current_game].present? %>
        <div class="small-12 medium-6 columns">
            <%= link_to game_path, :class => 'button expand' do %>
                <i class="fa fa-refresh"> Continue game</i>
            <% end %>
        </div>
    <% end %>
</div>
Reload please:







Moving on...

Press the "New game" button. Well, you're redirected to the new game page but wait a minute... Do we need such a page?
We would, if we gave user the opportunity to select game options such as word length or difficulty etc. But we don't and actually the new action is here just to create a new hangman game, set it to the user's session as the "current game" and then redirect to the game show page.

So, delete the automatically generated file app/views/games/new.html.erb and change the game's controller new action to the following (in app/views/controllers/games_controller.rb):
def new
  session[:current_game] = Game.new
  redirect_to game_path
end
Now, pressing the "New game" button takes you to the game show page.








And going back to home page we see that we...







we have current game!

Show page

Time to continue with the show page.  This page should:
  • Show the gallows updated to reflect the user's failed attempts
  • contain a box for each word letter either containing the successfully guessed letter or being empty otherwise
  • contain a series of boxes for all characters from which the user will make his/her guesses
  • have a button to return to home page
  • have a button to cancel the current game
Here's a draft:

















For the gallows, we are going to use a series of 6 images, each one for each possible game state.
Download them from here and add them to your app/assets/images folder.

Now open your app/views/games/show.html.erb file and replace its contents with the following (I will explain it thoroughly below):

<div class="row">
    <div class="small-12 medium-4 columns">
        <div id="gallows" class="gallows gallows-state-<%= current_game.failed_attempts %>">

        </div>
    </div>

    <div class="small-12 medium-8 columns">
        <div class="row">
            <div class="small-12 columns">
                <ul class="word small-block-grid-<%= current_game.word.length %>">
                    <% current_game.word.split('').each do |letter| %>
                        <li>
                            <div class="word-letter">
                                <%= current_game.finished? || current_game.selected_letters.include?(letter) ? letter : '_' %>
                            </div>
                        </li>
                    <% end %>
                </ul>
            </div>
        </div>

        <% if current_game.finished? %>
            <div class="row game-status">
                <div class="small-12 columns text-center">
                    <% if current_game.guessed? %>
                        <span class="label success radius">You successfully guessed the word! :)</span>
                    <% else %>
                        <span class="label alert radius">No, no... You didn't find the word :(</span>
                    <% end %>
                </div>
            </div>
        <% end %>

        <%= form_for :game, :url => game_path, :method => :patch do |form| %>
            <div class="row">
                <div class="letters">
                    <% ('A'..'Z').each do |letter| %>
                        <%
                            if current_game.selected_letters.include? letter
                                button_class = current_game.word.include?(letter) ? 'success' : 'alert'
                            end
                        %>
                        <div class="small-2 medium-2 columns text-center">
                            <div class="letter">
                                <%= form.submit letter, :name => 'letter', :class => "button expand #{button_class}" %>
                            </div>
                        </div>
                    <% end %>
                </div>
            </div>
        <% end %>

        <div class="row">
            <div class="game-actions">
                <div class="small-12 <%= current_game.finished? ? '' : 'medium-6' %> columns">
                    <%= link_to root_path(:method => :delete), :class => 'button expand' do %>
                        <i class="fa fa-home"></i> Home
                    <% end %>
                </div>

                <% unless current_game.finished? %>
                    <div class="small-12 medium-6 columns">
                        <%= link_to game_path, :class => 'button expand alert', :method => :delete do %>
                            <i class="fa fa-fire"></i> Cancel game
                        <% end %>
                    </div>
                <% end %>
            </div>
        </div>
    </div>
</div>
As you can see, we first define two columns in order to separate the gallows container (a) from the container of the word & the letters (b).

Gallows section:
We define a div with id "gallows" (we will need it in another part of this tutorial) and we set its class to "gallows gallows-state-x" where x is the number of the failed attempts. As you will notice later on, we will define classes in the css for all these states that actually set the background image of this div to one of the images you downloaded.

Word & letters section:
We define a foundation block grid (the ul with the css class "word") in order to expand the word's letters to occupy the whole line regardless of the word's length. You can find more on the foundation's block grid here. We create a div with class "word-letter" for each word's letter and we show the actual letter only if the game is finished or user has already selected it.

If the game is finished, we also show an information note for the result using foundation's label class utilities.

Then we create a form in order to be able to submit user letter selections and we add 26 submit inputs, one for each latin character. To each of these submit buttons, we add a special success or alert button class in order to point out which letter selections were successful and which not.

Finally we add two links, one for returning to the application's home page and one for cancelling the current game. The second option will be available only in case the game is not finished yet. We have "bound" it to the game's controller destroy method.

So, the two things that are actually left to implement now are the game controller's update & destroy methods.

Edit your app/controllers/games_controller.rb file and update the two methods as below:

def update
  current_game.select! params[:letter]
  update_current_game
rescue Game::GameOverError
  flash[:alert] = 'This game is finished...'
ensure
  redirect_to game_path
end

def destroy
  set_current_game nil
  redirect_to root_path
end

As you can see, in the update method, we execute the select! method we had defined in our Game model passing the value of the params[:letter] a.k.a. the value submitted by the input submits we defined for each latin character. After updating the game, we enforce its serialization to the user's session by calling the update_current_game method we defined in the application controller. We also handle the GameOverError but this will be better explained and described in the next part of the tutorial.

The destroy method actually removes the game information both from the controller's variable and the user's session.

Reloading the game's page, you can actually play as many times as you want.





Reminder, you can find the code of the hangman tutorial at GitHub.

Hint: the word is hangman :P

These are too much for a part of a tutorial. See you soon at the second one.

What follows in the next part:

  1. Refactoring & improvement of the show page
  2. Use ajax for user selections
  3. Use another source for words
Post a Comment