Object-Oriented Programming (OOP) Concepts Through Solitaire
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()
orpop()
- 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()
, andtop()
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!
🖥️ Try It Yourself
📝 Quick Check
What did you learn?