Creating a Card Game with Object Oriented Programming in JavaScript

Building a digital card game can seem daunting at first, especially when considering hundreds of unique cards with varied effects. However, organizing your code effectively is key to managing complexity and allowing flexibility, such as enabling players to customize cards during gameplay. Drawing from best practices in object-oriented programming (OOP) and JavaScript, developers often adopt a modular approach, creating distinct classes for core game components like cards, decks, players, and the game board itself.

Core Components and Their Object-Oriented Design

Cards as Basic Building Blocks

At the foundation of any card game are the cards themselves. In an OOP context, each card can be represented as an object with properties such as suit, rank, and value. Using JavaScript ES6 classes, a Card class serves as a blueprint for creating individual card instances. This approach simplifies the process of generating many cards with different attributes and promotes code reusability.


class Card {
constructor(suit, rank, value) {
this.suit = suit;
this.rank = rank;
this.value = value;
}
}

With this class, creating a specific card is straightforward:


let jokerCard = new Card("special", "Joker", 100);
console.log(jokerCard);

Building a Deck

Next, the deck acts as a container for multiple cards. A Deck class can manage the creation, shuffling, and dealing of cards. It maintains an array of Card objects and provides methods to initialize the deck with all 52 standard cards, as well as shuffle them randomly.


class Deck {
constructor() {
this.cards = [];
}

createDeck() {
const suits = ['clubs', 'diamonds', 'hearts', 'spades'];
const ranks = ['ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'jack', 'queen', 'king'];
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
for (let suit of suits) {
for (let i = 0; i < ranks.length; i++) {
this.cards.push(new Card(suit, ranks[i], values[i]));
}
}
}

shuffleDeck() {
for (let i = this.cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];
}
}
}

This structure facilitates creating multiple decks, shuffling them, and dealing cards, which is essential for scalable card games with numerous unique cards.

Players and Their Hands

Players can be modeled with a class that stores their name and the cards they hold. By encapsulating player data, the game logic can easily manage multiple participants, deal cards, and track each player’s progress.


class Player {
constructor(name) {
this.playerName = name;
this.playerCards = [];
}
}

Managing the Game with a Board Class

The overall game state, including the table layout, active players, and ongoing rounds, can be managed with a Board class. This class orchestrates game initialization, such as creating players, generating and shuffling the deck, and distributing cards. It can also contain methods to progress the game, like starting a round, comparing cards, or determining winners.


class Board {
constructor() {
this.cardsInMiddle = [];
this.players = [];
}
start(playerOneName, playerTwoName) {
this.players.push(new Player(playerOneName));
this.players.push(new Player(playerTwoName));
const deck = new Deck();
deck.createDeck();
deck.shuffleDeck();
this.players[0].playerCards = deck.cards.slice(0, 26);
this.players[1].playerCards = deck.cards.slice(26, 52);
}
}

By calling start() with player names, the game initializes with a shuffled deck and two players holding half of the deck each. This modular setup not only simplifies game logic but also makes it easier to extend functionality, such as adding special effects or multiple rounds.

Handling Complex Card Effects and Customization

For a collectible card game (CCG) with hundreds of unique cards, each with distinct effects, a more dynamic approach is often necessary. Instead of creating a separate script for each card, developers might implement a flexible system where cards are defined by data objects or JSON configurations. These data-driven cards can then be instantiated into Card objects at runtime, allowing for dynamic modifications, such as changing effects or properties during gameplay.

Strategies for Managing Large Card Sets

  • Data-Driven Design: Store card data externally (e.g., in JSON files), and load them into your game engine. This makes it easy to add or modify cards without altering core code.
  • Inheritance and Composition: Use class inheritance to create specialized card types with unique behaviors, or compose cards with effect objects that define their actions.
  • Component-Based Effects: Assign effect functions or scripts to cards dynamically, enabling players or developers to customize card behaviors on the fly.

Example of a Dynamic Card Definition


const cardData = {
id: "fireball",
name: "Fireball",
effect: function(target) {
target.takeDamage(5);
},
description: "Deals 5 damage to target."
};

class EffectCard extends Card {
constructor(suit, rank, value, effect) {
super(suit, rank, value);
this.effect = effect;
}

activate(target) {
this.effect(target);
}
}

This approach allows players or developers to define new cards with custom effects dynamically, enhancing flexibility and scalability.

Conclusion

By leveraging object-oriented principles in JavaScript, developers can create modular, maintainable, and scalable systems for digital card games. Starting with fundamental classes like Card, Deck, and Player, and then expanding to more complex data-driven or effect-based models, enables handling large card collections with unique effects. This structured approach simplifies managing hundreds of different cards and supports runtime customization, making it suitable for developing sophisticated collectible card games or any card-based digital experience.

Alexa Monroe

Alexa Monroe

Alexa Monroe is a US-based gaming journalist and lifelong gamer. She writes about game codes, updates, and hidden secrets that help players get the most from every title. Link x.com Link insta

New Stories To Read