Object-Oriented Programming Through Solitaire

Object-Oriented Programming (OOP) is one of the most common approaches to writing software. Instead of just writing functions and variables, we model our code around objects—entities that combine both data (properties) and behaviors (methods).

This lesson will walk you through OOP using a Solitaire game as our example. By the end, you’ll see how the concepts of encapsulation, abstraction, inheritance, and polymorphism are not just theoretical ideas, but directly useful when building real software.

1. Core OOP Concepts

Before we dive into Solitaire, let’s review the key ideas behind OOP:

Pillar of OOP Use Case
Encapsulation Grouping related data and methods together, while controlling how they can be accessed
Abstraction Hiding complex implementation details behind simple, clear interfaces
Inheritance Defining new classes that reuse and extend behavior from existing classes
Polymorphism Allowing different classes to respond to the same method call in their own way

Benefits:

  • Clearer structure
  • Easier to maintain and extend
  • Matches how we think about real-world problems

Drawbacks:

  • Can be overcomplicated if misused
  • Sometimes slower to implement for very simple programs

2. Why OOP for Solitaire?

Solitaire maps almost perfectly onto object-oriented programming. The way we think about physical cards and piles mirrors how we design classes and objects in code. The table below connects real-world Solitaire to the four pillars of OOP.

Solitaire Concept How it Works in the Game OOP Concept
Card Each card has a suit, rank, color, and a face-up/face-down state Encapsulation – Each Card object holds its own data, instead of scattering those details across the program
Pile (stock, waste, tableau, foundation) Different piles follow different rules: tableau builds downward in alternating colors, foundations build upward in the same suit Inheritance & Polymorphism – All piles share a base Pile class but override canAccept() with their own rules
Player’s Strategy The player thinks ahead, deciding which cards to move where Abstraction – The Game class hides the complex rules of moving cards behind simple commands like tryMoveCardById()
Player’s Hands (making moves) The hands execute the chosen move: draw from stock, drag cards, place them Controller Object – The Controller connects user input (mouse/keyboard) to the game logic
Overall Game Keeps track of score, timer, win conditions, and manages all piles Composition – The Game class is built from smaller objects (Deck, Piles, Timer)

tl;dr: When we say Solitaire is a “natural fit for OOP,” we mean this:

  • Cards are objects
  • Piles inherit from a common base but specialize their rules
  • The Game provides abstraction, hiding complex steps behind simple actions
  • The Controller acts like the player’s hands, connecting ideas to actions

OOP models the real world of Solitaire directly, which makes the code easier to understand and extend.

Now, let’s learn about how we implemented that into the functional game linked at /solitaire/.

3. Our Game’s Modular Structure

We split the game into separate classes and files.

Logical Flow

Before looking at files and folders, it helps to see how the main parts of the game depend on each other conceptually. The diagram below shows the logical flow of the Solitaire program: how cards and piles build up into the Game, and how the Controller and UI interact with it.

graph TD
    Card --> Deck
    Deck --> Game
    Pile --> FoundationPile
    Pile --> TableauPile
    Pile --> StockPile
    Pile --> WastePile
    FoundationPile --> Game
    TableauPile --> Game
    StockPile --> Game
    WastePile --> Game
    Game --> Controller
    UI --> Controller
    Controller --> Game
    Controller --> UI

This view answers the question: “Which class uses which other class?”. It’s about roles and relationships rather than files.

File Structure

Once you understand the logical relationships, it’s helpful to see how the code is organized in the repository. Each box here is a file or folder, showing the physical structure of the project on disk.

flowchart TB
  ROOT["(repo root)"]
  ROOT --> A["/assets"]
  A --> B["/assets/js"]
  B --> C["/assets/js/solitaire"]
  C --> C1["models.js"]
  C --> C2["game.js"]
  C --> C3["ui.js"]
  C --> C4["controller.js"]
  C --> C5["main.js"]
  ROOT --> P["Solitaire Page: /solitaire"]

This view answers the question: “Where do I find the code for each part of the game?”. It’s about files and folders rather than abstract design.

Together, these two diagrams give both a conceptual map and a practical map: one shows how the classes connect, the other shows where the code lives. This modular approach makes the game easy to understand: each file and class has a clear role.

4. Examples of Each OOP Pillar

4.1. Encapsulation

Encapsulation means keeping an object’s data safe and only allowing controlled access.

// models.js
export class Pile {
  constructor(type) {
    this.type = type;
    this.cards = [];  // Internal data
  }

  top() { return this.cards[this.cards.length - 1]; }
  push(card) { this.cards.push(card); }
  pop() { return this.cards.pop(); }
  get isEmpty() { return this.cards.length === 0; }
}
  • The cards array is hidden inside each Pile
  • Other code can’t directly mess with it; instead, it must use push() or pop()
  • This keeps the game state consistent and prevents errors

4.2. Abstraction

Abstraction hides messy details and gives us simple ways to do complex things.

// game.js
tryMoveCardById(cardId, targetKind, targetIndex) {
  const loc = this.findCard(cardId);
  if (!loc || !loc.card.faceUp) return false;

  const targetPile = this.getPile(targetKind, targetIndex);
  if (!targetPile) return false;

  if (targetPile.canAccept(loc.card)) {
    this._moveCards(loc.pile, targetPile, 1);
    this.addScore(10);
    this._afterMove(loc.pile);
    return true;
  }
  return false;
}

From the outside, calling tryMoveCardById() looks simple. Internally, it:

  • Finds the card
  • Checks rules
  • Moves the card
  • Updates the score
  • Refreshes the UI

The complexity is hidden, so the rest of the program can just say: “Try to move this card here.”

4.3. Inheritance

Inheritance lets us create specialized versions of a base class. All piles share common behavior, but each has unique rules.

// models.js
class FoundationPile extends Pile {
  constructor() { super('foundation'); }
  canAccept(card) {
    if (this.isEmpty) return card.rank === 'A';
    const top = this.top();
    return (card.suit === top.suit && card.value === top.value + 1);
  }
}

class TableauPile extends Pile {
  constructor() { super('tableau'); }
  canAccept(card) {
    if (this.isEmpty) return card.rank === 'K';
    const top = this.top();
    return (card.color !== top.color && card.value === top.value - 1);
  }
}
  • Both inherit push(), pop(), and top() from Pile.
  • But each defines its own canAccept() method, enforcing the rules of Solitaire.

4.4. Polymorphism

Polymorphism means different objects can respond to the same method in different ways.

const allPiles = [
  new FoundationPile(),
  new TableauPile(),
  new StockPile(),
  new WastePile()
];

// All piles can be asked the same question:
allPiles.forEach(pile => {
  if (pile.canAccept && pile.canAccept(selectedCard)) {
    pile.push(selectedCard);
  }
});
  • Each pile type implements its own version of canAccept()
  • The Game doesn’t need to know which type it’s dealing with—it just calls the method and trusts the object to know what to do

5. Key Takeaways

By modeling Solitaire with OOP, we created a structured, modular, and maintainable game*:

âś… Encapsulation keeps card data safe inside piles. âś… Abstraction makes game actions simple, even if the code behind them is complex. âś… Inheritance allows different pile types to share common behavior while adding their own rules. âś… Polymorphism lets the Game treat all piles the same, while each pile enforces its own rules.

This is the power of OOP: turning a real-world system (like Solitaire) into clear, reusable code.

As you continue learning, try building small games or simulations of your own. Notice how often these four OOP principles appear: it’s no accident that they’re considered the foundation of modern programming.

Next Up: Complete hacks to solidify your understanding!