Making Four-In-A-Row Using JavaScript - Part 8: Status Updates

colinkiama

Colin Kiama

Posted on July 8, 2023

Making Four-In-A-Row Using JavaScript - Part 8: Status Updates

Intro

Welcome back! In the previous blog post, you drew the game board and made the game playable by clicking on board columns. In this post, you'll be adding the status area component to your four-in-a-row game.

Breaking Down The Status Area Component

Let's refer back to the mockup of the finished game:

Image of game mockup

The status area is at the top. It's broken down into 2 parts:

  • Player Turn Indicator: Indicates the current player's turn by showing the associated player's colour.
  • Status Message: Describes what is happening at each stage of the game (whose turn is it? Which player won? etc.)

Together they inform players and spectators about what is happening in the game.

Creating The StatusArea Class

To get started, create a file under the src/components directory called StatusArea.js.

In the file that you've just created, create an empty class called StatusArea. This class will inherit GameObject:

import GameObject from "./GameObject.js";

export default class StatusArea extends GameObject {

}
Enter fullscreen mode Exit fullscreen mode

Player Turn Indicator

The player turn indicator is a small circle that appears in the status area. It may have either of these states:

  • Yellow - Has a yellow colour when it's the yellow player's turn or the yellow player has won the game.
  • Red - Has a red colour when it's the red player's turn or the red player has won the game.
  • Invisible - The indicator is not visible when the game ends in a draw.

Now that you know how the player turn indicator behaves, the next step for you is to add it to your game.

Start Drawing The Player Turn Indicator

To start off, import the Constants object from the gameLogic directory as well as StatusAreaConfig and TokenColor from the constants directory:

import { Constants } from "../gameLogic/index.js";
import { StatusAreaConfig, TokenColor } from "../constants/index.js";
import GameObject from "./GameObject.js";

export default class StatusArea extends GameObject {

}
Enter fullscreen mode Exit fullscreen mode

Then, add renderPlayerTurnIndicator() to the StatusArea class:

export default class StatusArea extends GameObject {
    renderPlayerTurnIndicator(indicatorColor) {
        let indicatorColorValue;

        switch (indicatorColor) {
            case Constants.PlayerColor.YELLOW:
                indicatorColorValue = TokenColor.YELLOW;
                break;
            case Constants.PlayerColor.RED:
                indicatorColorValue = TokenColor.RED;
                break;
            default:
                // Unknown color. Do not attempt to render player turn indicator.
                return;
        }

        this.context.fillStyle = indicatorColorValue;
        const indicatorY = this.y + StatusAreaConfig.INDICATOR_WIDTH / 2 + StatusAreaConfig.PADDING_TOP;
        this.context.beginPath();

        // Draws a circle using CanvasDrawingContext2D.arc(): https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc
        this.context.arc(this.width / 2, indicatorY, StatusAreaConfig.INDICATOR_WIDTH / 2, 0, Math.PI * 2);

        this.context.closePath();
        this.context.fill();
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, add render() to the StatusArea class:

export default class StatusArea extends GameObject {
    render(indicatorColor) {
        this.context.save();
        this.clear();

        if (indicatorColor !== Constants.PlayerColor.NONE) {
            this.renderPlayerTurnIndicator(indicatorColor);
        }

        this.context.restore();
    }

    // ..
}
Enter fullscreen mode Exit fullscreen mode

Before continuing any further, you need to expose the StatusArea class as a module through the src/components/index.js:

import Board from "./Board.js";
import StatusArea from './StatusArea.js';


export { Board, StatusArea };
Enter fullscreen mode Exit fullscreen mode

Now what you've implemented the rendering logic of the player turn indicator and exposed the StatusArea class through the components directory, you are now ready to start rendering the player turn indicator.

Render Player Turn Indicator

In src/FrontEnd.js import StatusAreaConfig and StatusArea:

import { FrontEndConfig, BoardConfig, StatusAreaConfig } from "./constants/index.js";
import { Board, StatusArea } from "./components/index.js";
import { Constants } from "./gameLogic/index.js";

export default class FrontEnd {
    // ..
}
Enter fullscreen mode Exit fullscreen mode

Next, add the statusArea field to the FrontEnd class:

export default class FrontEnd {
    game;
    canvas;
    width;
    height;
    context;
    board;
    statusArea;
    gameOver;

    // ..

}
Enter fullscreen mode Exit fullscreen mode

Add createStatusArea() to the FrontEnd class:

export default class FrontEnd {
    // ..

    createStatusArea() {
        let statusArea = new StatusArea(this.context, 0, 0, this.width, StatusAreaConfig.HEIGHT);
        statusArea.render(this.game.currentTurn);
        return statusArea;
    }
}
Enter fullscreen mode Exit fullscreen mode

In start(), set statusArea to a new status area returned from createStatusArea():

export default class FrontEnd {
    // ..

    start() {
        this.statusArea = this.createStatusArea();
        this.board = this.createBoard();

        document.body.addEventListener('click', (clickEvent) => {
            this.board.handleClick(clickEvent);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Add determineIndicatorColor() to the FrontEnd class:

export default class FrontEnd {
    // ..

    determineIndicatorColor(moveResult) {
        if (moveResult.status.value === Constants.MoveStatus.DRAW) {
            return Constants.PlayerColor.NONE
        } else if (moveResult.status.value === Constants.MoveStatus.WIN) {
            return moveResult.winner;
        } else {
            return this.game.currentTurn;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, update processMoveResult() so that it also determines the next player's turn and passes in the colour of the next player to a call to the render() method on statusArea:

export default class FrontEnd {
    // ..

    processMoveResult(moveResult) {
        if (this.gameOver || moveResult.status.value === Constants.MoveStatus.INVALID) {
            return;
        }

        const indicatorColor = this.determineIndicatorColor(moveResult);

        this.statusArea.render(indicatorColor);
        this.board.render(this.game.currentBoard);

        if (moveResult.status.value === Constants.MoveStatus.WIN || moveResult.status.value === Constants.MoveStatus.DRAW) {
            this.gameOver = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, in createBoard(), update the call to the Board constructor so that the board when the game board is rendered, it's shifted down below the status area:

export default class FrontEnd {
    // ..

    createBoard() {
        let board = new Board(this.context, BoardConfig.MARGIN_LEFT, this.statusArea.height + BoardConfig.MARGIN_TOP, BoardConfig.WIDTH, BoardConfig.HEIGHT);
        board.setColumnSelectionHandler((columnIndex) => this.playMove(columnIndex));
        board.render(this.game.currentBoard);
        return board;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you check the game in your browser while a server is running, you'll see the indicator colour being rendered.

It'll update based on the current state of the game.

Red player's turn on game board with player turn indicator displayed with a red colour

Status Messages

The status message is the text portion of the status area.

It is used to:

  • Display the current player's turn
  • Reveal which player has won a game
  • Shows when a game ends in a draw

Start Implementing Status Messages

Add the renderMessage() method to the StatusArea class:

export default class FrontEnd {
    // ..

    renderMessage(message) {
        this.context.fillStyle = "white";
        this.context.font = "bold 16px Arial";
        this.context.textBaseline = "top";
        this.context.textAlign = "center"; // Default value had vertical alignment issues. "center" fixes those in this case
        const messageY = this.y + StatusAreaConfig.PADDING_TOP + StatusAreaConfig.INNER_MARGIN;
        this.context.fillText(message, this.width / 2, messageY);
    }
}
Enter fullscreen mode Exit fullscreen mode

Proceed by adding the message parameter to the render() method then calling renderMessage() in render():

export default class StatusArea extends GameObject {
    render(indicatorColor, message) {
        this.context.save();
        this.clear();

        if (indicatorColor !== Constants.PlayerColor.NONE) {
            this.renderPlayerTurnIndicator(indicatorColor);
        }

        this.renderMessage(message);
        this.context.restore();
    }

    // ..
Enter fullscreen mode Exit fullscreen mode

Once you've done that, switch back to the src/FrontEnd.js file. Import StatusMessages from the constants directory:

import { FrontEndConfig, BoardConfig, StatusAreaConfig, StatusMessages } from "./constants/index.js";
import { Board } from "./components/index.js";
import { Constants } from "./gameLogic/index.js";
Enter fullscreen mode Exit fullscreen mode

Now, add logic that determines which status message to display depending on the current state of the game. Add pickStatusMessage() to the FrontEnd class:

export default class FrontEnd {
    // ..

    pickStatusMessage(status) {
        switch (status) {
            case Constants.GameStatus.WIN:
                // The game is on the the next turn but the somebody has won from the previous turn. The winning player is the opposite of the player who currently has a turn.
                return this.game.currentTurn === Constants.PlayerColor.YELLOW ? StatusMessages.RED_WIN : StatusMessages.YELLOW_WIN;
            case Constants.GameStatus.DRAW:
                return StatusMessages.DRAW;
        }

        // At this point, we can assume that the game is either has just started 
        // or is still in progress.
        return this.game.currentTurn === Constants.PlayerColor.YELLOW ? StatusMessages.YELLOW_TURN : StatusMessages.RED_TURN;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in the processMoveResult() method, update the render() call on statusArea so that it has an additional argument passed in. This argument is a method call to pickStatusMessage() with the status value of moveResult passed in:

export default class FrontEnd {
    // ..

    processMoveResult(moveResult) {
        if (this.gameOver || moveResult.status.value === Constants.MoveStatus.INVALID) {
            return;
        }

        const indicatorColor = this.determineIndicatorColor(moveResult);

        this.statusArea.render(indicatorColor, this.pickStatusMessage(moveResult.status.value))
        this.board.render(this.game.currentBoard);

        if (moveResult.status.value === Constants.MoveStatus.WIN || moveResult.status.value === Constants.MoveStatus.DRAW) {
            this.gameOver = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, repeat this same change in createStatusArea() except that the argument passed in to pickStatusMessage() will be the game's current status:

export default class FrontEnd {
    // ..

    createStatusArea() {
        let statusArea = new StatusArea(this.context, 0, 0, this.width, StatusAreaConfig.HEIGHT);
        statusArea.render(this.game.currentTurn, this.pickStatusMessage(this.game.status));
        return statusArea;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you check the game in your browser with a server running, you'll see that the status area will always reflect the current status of the game:

  • The player turn indicator will appear in states regarding a specific player
  • The status message will describe the current state of the game

Screenshot of yellow player's run with player turn indicator and the status message indicating this in the status area

Conclusion

Congratulations on getting this far! It's now clear to understand what is happening during gameplay.

Unfortunately, when the game ends, there's no way to start a new game without refreshing the page.

In the next (and final) part of this tutorial, you'll solve this problem by adding the final component to the game, the play again button.

Thanks for reading!

💖 💪 🙅 🚩
colinkiama
Colin Kiama

Posted on July 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related