Making Four-In-A-Row Using JavaScript - Part 9: Play Again (Finale)
Colin Kiama
Posted on July 9, 2023
Intro
In the previous blog post, you added the status area to the game. Players and spectators now clearly understand what's happening at any time during gameplay.
In this post, you'll add the ability to restart the game without having to refresh your browser.
You'll do this by adding a "Play Again" button to the game.
What Is The Play Again Button?
Let's take one more look at the mockup of the finished game:
The "Play Again" button is a button that:
- Is only visible when the game ends (a player wins or the game ends in a draw).
- Restarts the game and disappears when clicked on.
Creating The PlayAgainButton class
Just like the other components you've added so far, the "Play Again" button is also a game object.
In the src/components
directory, create a file create a file called PlayAgainButton.js
.
In that file, create a class called PlayAgainButton
that extends from GameObject
:
import GameObject from "./GameObject.js";
export default class PlayAgainButton extends GameObject {
}
You are also going to need some constants specific to this component too. Import PlayAgainButtonConfig
:
import GameObject from "./GameObject.js";
import { PlayAgainButtonConfig } from "../constants/index.js";
export default class PlayAgainButton extends GameObject {
}
Next, add the drawing logic for the button's background. Add renderBackground()
to the PlayAgainButton
class:
export default class PlayAgainButton extends GameObject {
renderBackground() {
const backgroundGradient = this.context.createLinearGradient(this.x, this.y, this.x, this.y + this.height);
backgroundGradient.addColorStop(0, PlayAgainButtonConfig.BACKGROUND_START_COLOR);
backgroundGradient.addColorStop(1, PlayAgainButtonConfig.BACKGROUND_END_COLOR);
this.context.fillStyle = backgroundGradient;
this.context.strokeStyle = `${PlayAgainButtonConfig.BORDER_WIDTH}px black`;
this.context.fillRect(this.x, this.y, this.width, this.height);
this.context.strokeRect(this.x, this.y, this.width, this.height);
}
}
Then, override the clear()
method from GameObject
to also take into account the button's borders:
export default class PlayAgainButton extends GameObject {
// ..
clear() {
const clearRectX = this.x - PlayAgainButtonConfig.BORDER_WIDTH;
const clearRectY = this.y - PlayAgainButtonConfig.BORDER_WIDTH;
const clearRectWidth = this.width + PlayAgainButtonConfig.BORDER_WIDTH * 2;
const clearRectHeight = this.height + PlayAgainButtonConfig.BORDER_WIDTH * 2;
this.context.clearRect(clearRectX, clearRectY, clearRectWidth, clearRectHeight);
}
}
Now to add text to the button, add renderText()
to the PlayAgainButton
class:
export default class PlayAgainButton extends GameObject {
// ..
renderText() {
this.context.fillStyle = "white";
this.context.font = "16px Arial";
this.context.textAlign = "center";
this.context.textBaseline = "top";
const textMetrics = this.context.measureText(PlayAgainButtonConfig.TEXT);
const textHeight = textMetrics.actualBoundingBoxDescent;
// Calculation ensures that text is displayed at the vertical center of the button
const finalTextY = this.y + (this.height / 2) - textHeight / 2;
this.context.fillText(PlayAgainButtonConfig.TEXT, this.x + PlayAgainButtonConfig.WIDTH / 2, finalTextY);
}
}
Add the render()
method to the PlayAgainButton
class:
export default class PlayAgainButton extends GameObject {
render() {
this.context.save();
this.renderBackground();
this.context.restore();
this.context.save();
this.renderText();
this.context.restore();
}
// ..
}
Rendering The Button
You'll now add the "Play Again" button to the game.
First, add the PlayAgainButton
class to src/components/index.js
so that it can be imported from there:
import Board from "./Board.js";
import StatusArea from './StatusArea.js';
import PlayAgainButton from "./PlayAgainButton.js";
export { Board, StatusArea, PlayAgainButton };
Now switch to src/FrontEnd.js
. Import PlayAgainButton
and PlayAgainButtonConfig
:
import { FrontEndConfig, BoardConfig, StatusAreaConfig, StatusMessages, PlayAgainButtonConfig } from "./constants/index.js";
import { Board, StatusArea, PlayAgainButton } from "./components/index.js";
import { Constants } from "./gameLogic/index.js";
export default class FrontEnd {
// ..
}
Add the playAgainButton
field to the FrontEnd
class:
export default class FrontEnd {
game;
canvas;
width;
height;
context;
board;
statusArea;
playAgainButton;
gameOver;
// ..
}
Then add createPlayAgainButton()
to the FrontEnd
class:
export default class FrontEnd {
// ..
createPlayAgainButton() {
let buttonX = this.width / 2 - PlayAgainButtonConfig.WIDTH / 2;
let buttonY = this.height - PlayAgainButtonConfig.MARGIN_BOTTOM;
let button = new PlayAgainButton(this.context, buttonX, buttonY, PlayAgainButtonConfig.WIDTH, PlayAgainButtonConfig.HEIGHT);
button.render();
return button;
}
}
In the start()
method, set the playAgainButton
field to the value returned from createPlayAgainButton()
:
export default class FrontEnd {
// ..
start() {
this.statusArea = this.createStatusArea();
this.board = this.createBoard();
this.playAgainButton = this.createPlayAgainButton();
document.body.addEventListener('click', (clickEvent) => {
this.board.handleClick(clickEvent);
});
}
}
If you check the game in your browser with a server running, you'll see the "Play Again" button on the canvas:
It's great that the button shows up but nothing happens when you click on it. It's not supposed to show up at this stage of the game either 😂️. You'll fix these problems next.
Handling Input
The "Play Again" button is only supposed to be visible when the game ends. Also, the PlayAgainButton
class handles clicks similarly to the Board
class.
To get started with this, go back to src/components/PlayAgainButton.js
. Add the buttonClicked
and isEnabled
fields to the PlayAgainButton
class:
export default class PlayAgainButton extends GameObject {
buttonClicked;
isEnabled;
// ..
}
Then add a constructor to the PlayAgainButton
class. It will call the parent constructor and then set the isEnabled
field to false
:
export default class PlayAgainButton extends GameObject {
constructor(context, x, y, width, height) {
super(context, x, y, width, height);
this.isEnabled = false;
}
// ..
}
Update render()
so that it sets the isEnabled
field to true
at the end:
export default class PlayAgainButton extends GameObject {
// ..
render() {
this.context.save();
this.renderBackground();
this.context.restore();
this.context.save();
this.renderText();
this.context.restore();
this.isEnabled = true;
}
}
To set the logic to run when the button is clicked, add setClickHandler()
to the PlayAgainButton
class:
export default class PlayAgainButton extends GameObject {
// ..
setClickHandler(handler) {
this.buttonClicked = handler;
}
}
Add handleClick()
to process the click events that will be passed in:
export default class PlayAgainButton extends GameObject {
// ..
handleClick(clickEvent) {
if (!this.isEnabled) {
return;
}
const wasButtonClicked = clickEvent.offsetX >= this.x
&& clickEvent.offsetX <= this.x + this.width
&& clickEvent.offsetY >= this.y
&& clickEvent.offsetY <= this.y + this.height;
if (!wasButtonClicked) {
return;
}
this.buttonClicked();
}
}
It checks if the location where a player has clicked was actually within the bounds of the button.
There's one more thing to add to the PlayAgainButton
class now. It's the hide()
method:
export default class PlayAgainButton extends GameObject {
// ..
hide() {
this.isEnabled = false;
this.clear();
}
}
There are quite a few things you've added to the PlayAgainButton
class. Similar to when you handled clicks on the board, it will all make sense after making use of these changes in the FrontEnd
class.
In the FrontEnd
class, update the start()
method so that the click event handler callback also calls handleClick()
on the playAgainButton
field with the click event passed in:
export default class FrontEnd {
// ..
start() {
this.statusArea = this.createStatusArea();
this.board = this.createBoard();
this.playAgainButton = this.createPlayAgainButton();
document.body.addEventListener('click', (clickEvent) => {
this.board.handleClick(clickEvent);
this.playAgainButton.handleClick(clickEvent);
});
}
}
Then in processMoveResult()
, call render()
on the playAgainButton
field if the game is over:
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;
}
if (this.gameOver) {
this.playAgainButton.render();
}
}
}
Also, import FourInARowGame
:
import { FrontEndConfig, BoardConfig, StatusAreaConfig, StatusMessages, PlayAgainButtonConfig } from "./constants/index.js";
import { Board, StatusArea, PlayAgainButton } from "./components/index.js";
import { Constants, FourInARowGame } from "./gameLogic/index.js";
export default class FrontEnd {
// ..
}
Then, add a reset()
method to the FrontEnd
class:
export default class FrontEnd {
// ..
reset() {
this.game = new FourInARowGame();
this.gameOver = false;
this.playAgainButton.hide();
this.statusArea.render(this.game.currentTurn, this.pickStatusMessage(this.game.status));
this.board.render(this.game.currentBoard);
}
}
This is the main method that restarts the game.
Lastly, in createPlayAgainButton()
, remove the call to render()
on button
and call setClickHandler()
on button
. Call reset()
in the callback:
export default class FrontEnd {
// ..
createPlayAgainButton() {
let buttonX = this.width / 2 - PlayAgainButtonConfig.WIDTH / 2;
let buttonY = this.height - PlayAgainButtonConfig.MARGIN_BOTTOM;
let button = new PlayAgainButton(this.context, buttonX, buttonY, PlayAgainButtonConfig.WIDTH, PlayAgainButtonConfig.HEIGHT);
button.setClickHandler(() => this.reset());
return button;
}
}
Now if you check the game in your browser with a server running, you'll have a fully playable four-in-a-row game that you can replay after completing a game.
Conclusion
Congratulations! You've finished the game. Feel free to modify it however you like. Maybe change how some of the components look? You could add animations too!
The source code of the game is available here: https://github.com/colinkiama/four-in-a-row-walkthrough
Thanks for reading!
Posted on July 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.