Building a Turn-based Game Using JS and Rails

Overview

Players take turns placing tokens, until the entire board is filled. At the end of the game, the player with the highest amount of tokens on the board wins. Watch Triple S's video "How to Play Othello" to learn more.

Once a player wins a game, their score is recorded to the database and the players have the options of playing again.

GamePage is split into two repositories, frontend and backend:

Project Architecture

GamePage is served by a Rails API that responds to HTTP GET and POST requests and returns a JSON response. The front end Document-Object Model is manipulated by JS scripts that run with a successful fetch response, so the frontend user experiences a seamless single page application.

Rails Controllers

To access the main menu, a User must log in. They then are presented with a choice of options: Play Reversi, Leaderboard, and My Scores.

Choosing My Scores makes a fetch call which routes to the ScoresController's index action and returns an array of JSON objects which are then mapped into Score objects in JS and rendered on the page.

class ScoresController < ApplicationController
    def index
        scores = Score.where(user_id: params[:user_id])

        seralized_scores = scores.map do |score|
            {points: score.points, created_at: score.created_at.strftime('%b %d, %Y at %l:%M%P')}
        end

        render json: seralized_scores
    end
end

Similarly, choosing Leaderboard makes a fetch call to the rails server and returns an array of JSON objects which are mapped to JS User Objects.

To begin playing a game, another User must log in and to access the same Board. Once the front end receives a response from BoardController, a board is rendered on the front end. Each user then takes turns placing tokens by making POST calls to the BoardController's play action.

class BoardController < ApplicationController
    def play
        board_id = params[:board]["boardId"]
        cell = params[:id]

        board = Board.find(board_id)

        if board.set(current_user(board), cell)
            render json: board.cells_to_be_flipped
        else
            render json: {error: "You can't play here"}
        end
    end
end

If the POST call returns an invalid move, the turn indicator shakes and allows the User to try again. If the move is successful, a JSON object is returned with each cell that needs to be updated.

Frontend OO JavaScript

The front end of GamePage is made up of two main js directories: components and services. While components holds each object and object methods, services holds objects that are explicitly responsible for fetch requests.

class UserAPI {
    static getTopUsers() {
        fetch(root + "/users")
            .then(resp => resp.json())
            .then(json => {
                User.addAllTopUserDivs(json)
        })
    }
}

Reducing N+1 Queries

To increase the speed of fetch requests and reduce workload of ActiveRecord, I used the .includes method to specify relationships to be included in the result set. If I can tell Active Record about the associations I plan to use later, ActiveRecord can load the data eagerly which reduces queries in iterative methods.

class User < ApplicationRecord
    def self.top_users
        top_users = self.includes(:scores).sort_by { |user| -user.average_score}
        top_users.map {|user| {user: user, average_score: user.average_score, games_played: user.scores.length}}
    end
end

Resources

Feel free to check out GamePage on my Github or give me a follow on Twitter to continue following my coding journey.

GamePage is licensed with a BSD 2-Clause License.

Dependencies

GamePage does not have any npm dependencies, full npm data can be found in package.json.

31