Bootcamp
Search…
5.2.2: AJAX Cards

Introduction

This module demonstrates how we might implement a card game using AJAX and a backend server. This is a pre-cursor to 5.POCE.3: AJAX Cards and Project 3: Full-Stack Game. In 5.POCE.3 we we will implement High Card with AJAX, and in Project 3 we will implement a game of your choice using AJAX and other technologies from modules so far.

Differences From Coding Basics Card Games

Data Persistence

In Coding Basics we created card games where the game state would disappear and reset when the user refreshed the page. Since we've learned backend technologies in Coding Bootcamp, we can use our backend server and database to persist game state across page refreshes.

Responses with Selective Data

In Coding Basics players could open the browser console and see all cards in the deck or other players' hands by logging the relevant variables. Since learning backend technologies in Coding Bootcamp, because we store game state server-side, we can prevent cheating by only sending what a player is supposed to see to that player's browser, and not the entire game state.

Starter Code Repo

Please find the starter repo cards-ajax-bootcamp here. Feel free to navigate to the relevant files in GitHub while reading the following sections.

Migrations

The starter code contains 1 migration to generate the Games table.
Note the gameState attribute's data type on line 10: JSON. We can use the JSON data type to break the rule of not storing data structures in a table cell. This is for the following reasons:

Card Data Structure Simplicity

We want to store data representing a standard deck of cards. Normally we would think about how to represent this with relational tables- a deck and hand table that are related through foreign keys. However, it will be easier to store the all the game's cards in a JSON column rather than creating a set of tables and joining across multiple tables to perform simple operations such as dealing or playing cards.

Relational-ness of Card Data

We do not need to query for game state data across games.
So far, we've set up a relational database schema that allows you to query relationships in multiple "directions". For example, when we store users and messages, we can get messages sent by user A and messages received by user A. We can get all recipes with the ingredient soy sauce, and we can get all the ingredients in the chicken noodle recipe.
In a card game we most likely only need to retrieve data from one "direction" or one perspective- that is, when we know about a single game, get that game's cards. Most likely we will never need have a card where we need to get all games with that card- for example that would be every game that has an Ace.
Depending on what specific application you are building, your data will necessitate having these relationships or not. It is up to the database designer to decide between having foreign key relationships and storing non-relational data is the appropriate choice, given a business situation or set of real world data.

20201221183525-create-games-table.js

The following migration code is available the repo here.
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("Games", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
gameState: {
// JSON allows us to store non-relational data easily.
// Non-relational data refers to data that we may not query across records.
// For the purposes of this project, where the focus is AJAX, let's store
// all game state (e.g. cardDeck, playerHand) in the gameState JSON column.
type: Sequelize.JSON,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
​
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Games");
},
};
For more on Postgres JSON column type check the official docs here. Note that we won't be any query features of the JSON column type. We'll simply be storing JS objects inside this column.

Game Creation Logic

Game creation logic spans the frontend and backend.

Frontend

The frontend components of game creation live in public/script.js. When the page loads we display a "Create Game" button. When the user clicks this button we'll create a single game in the database.

script.js

The following code is available in the repo here.
const createGame = function () {
// Make a request to create a new game
axios
.post("/games")
.then((response) => {
// set the global value to the new game.
currentGame = response.data;
​
console.log(currentGame);
​
// display it out to the user
runGame(currentGame);
​
// for this current game, create a button that will allow the user to
// manipulate the deck that is on the DB.
// Create a button for it.
const dealBtn = document.createElement("button");
dealBtn.addEventListener("click", dealCards);
​
// display the button
dealBtn.innerText = "Deal";
document.body.appendChild(dealBtn);
})
.catch((error) => {
// handle error
console.log(error);
});
};
​
// manipulate DOM, set up create game button
createGameBtn.addEventListener("click", createGame);
createGameBtn.innerText = "Create Game";
document.body.appendChild(createGameBtn);

Backend

The backend components of game creation logic primarily live in the Games Controller. The controller's create function creates a new game record using Sequelize. When creating a new game record, we also create and shuffle a new deck and save it in the game record. The game controller has card and deck logic helper functions from previous modules.

games.mjs

The following code is available in the repo here.
// create a new game. Insert a new row in the DB.
const create = async (request, response) => {
// deal out a new shuffled deck for this game.
const cardDeck = shuffleCards(makeDeck());
const playerHand = [cardDeck.pop(), cardDeck.pop()];
​
const newGame = {
gameState: {
cardDeck,
playerHand,
},
};
​
try {
// run the DB INSERT query
const game = await db.Game.create(newGame);
​
// send the new game back to the user.
// dont include the deck so the user can't cheat
response.send({
id: game.id,
playerHand: game.gameState.playerHand,
});
} catch (error) {
response.status(500).send(error);
}
};
Note that the DB schema and what we are doing here on like 8 is to put 2 keys into the gameState JSON type database column. We also had the option of creating a separate JSON column for cardDeck and playerHand. The decision to keep all game state data inside a single column is a trade-off between making more SQL/Sequelize queries to get out more data and keeping your game data more structured. There is no right answer, but we felt the application was simpler to write if the game application code handled all the data inside of gameState, instead of across different columns. The caveat is that care must be taken so that the keys and values in gameState are always in the correct format (e.g., playerHand is always an array), otherwise hard to spot errors may occur.

Game Running Logic

Game running logic also spans the frontend and backend.

Frontend

The response from the AJAX game creation request contains the player's hand. The app displays the hand the server dealt using the runGame function, and renders a Deal Cards button.
When a user clicks the Deal Cards button, the app makes a PUT request to the server. This request alters the cards in the game and the server responds with two new cards dealt from the deck.

script.js

The following code is available in the repo here.
// DOM manipulation function that displays the player's current hand.
const runGame = function ({ playerHand }) {
// manipulate DOM
const gameContainer = document.querySelector("#game-container");
​
gameContainer.innerText = `
Your Hand:
====
${playerHand[0].name}
of
${playerHand[0].suit}
====
${playerHand[1].name}
of
${playerHand[1].suit}
`;
};
​
// make a request to the server
// to change the deck. set 2 new cards into the player hand.
const dealCards = function () {
axios
.put(`/games/${currentGame.id}/deal`)
.then((response) => {
// get the updated hand value
currentGame = response.data;
​
// display it to the user
runGame(currentGame);
})
.catch((error) => {
// handle error
console.log(error);
});
};

Backend

The game controller uses the game ID in the request to find the relevant game and deal cards. It deals two new cards into the player's hand (and discards the old ones), updates both player hand and card deck in the DB, and sends back the updated player hand. Note the player client-side never sees the deck so they cannot cheat.

games.mjs

The following code is available in the repo here.
// deal two new cards from the deck.
const deal = async (request, response) => {
try {
// get the game by the ID passed in the request
const game = await db.Game.findByPk(request.params.id);
​
// make changes to the object
const playerHand = [
game.gameState.cardDeck.pop(),
game.gameState.cardDeck.pop(),
];
​
// update the game with the new info
await game.update({
gameState: {
cardDeck: game.gameState.cardDeck,
playerHand,
},
});
​
// send the updated game back to the user.
// dont include the deck so the user can't cheat
response.send({
id: game.id,
playerHand: game.gameState.playerHand,
});
} catch (error) {
response.status(500).send(error);
}
};

Exercise

Add game winning logic to this game. A user wins if the current pair of cards in their hand has a higher card than the highest card in the previous hand. Calculate winning logic server-side, and send the win result in the response to the client.