DEV Community

Cover image for Building a Turn-based Game Using JS and Rails
Karson Kalt
Karson Kalt

Posted on • Updated on

Building a Turn-based Game Using JS and Rails

Overview

GamePage is a web app built in Rails and JavaScript that lets two players play the classic board game "Reversi" aka "Othello". The basic premise of the game is to place tokens on the board. When placing a token, you attempt to also swap any opponent's tokens that lay between your placed token and your other tokens with your own.

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.

Main menu with three 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
Enter fullscreen mode Exit fullscreen mode

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.

GamePage menu showing leaderboard.

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
Enter fullscreen mode Exit fullscreen mode

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.

GIF of GamePage in action with multiple cells being clicked and the page responding.

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)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)