<div id="app"></div>
import { makeAutoObservable, observable } from "https://esm.sh/mobx";
import { observer } from "https://esm.sh/mobx-react-lite";
import ReactDom from "https://esm.sh/react-dom/client";
import React, { useEffect, useMemo, useState } from "https://esm.sh/react";
import { shuffle } from "https://esm.sh/lodash";
type Card = { id: number; value: number; isFound?: boolean };
class Board {
cardOne?: Card = undefined;
cardTwo?: Card = undefined;
pairCount = 0;
cards = observable.array<Card>();
size = 6;
constructor() {
makeAutoObservable(this);
this.reset();
}
get totalPairCount() {
return this.size ** 2 / 2;
}
reset() {
const cards: Card[] = [];
for (let cardValue = 1; cardValue <= this.totalPairCount; cardValue++) {
cards.push({ id: cards.length, value: cardValue });
cards.push({ id: cards.length, value: cardValue });
}
this.cards = shuffle(cards);
this.pairCount = 0;
}
updateSize(size: number) {
this.size = size;
this.reset();
}
flipCard(card: Card) {
if (this.cardOne === undefined) {
this.cardOne = card;
} else {
this.cardTwo = card;
}
const { cardOne, cardTwo } = this;
if (!(cardOne && cardTwo)) return;
setTimeout(() => {
// check if there is a match
if (cardOne.value === cardTwo.value) {
console.log("found match!");
cardOne.isFound = true;
cardTwo.isFound = true;
this.pairCount += 1;
}
this.resetSelectedCards();
}, 3_000);
}
isFlipped(card: Card) {
return board.cardOne === card || board.cardTwo === card;
}
resetSelectedCards() {
this.cardOne = undefined;
this.cardTwo = undefined;
}
get isGameFinished() {
return this.pairCount === this.totalPairCount;
}
get isFlipDisabled() {
return this.cardOne && this.cardTwo;
}
}
const board = new Board();
const CardBlock = observer(({ card }: { card: Card }) => {
const isFlipped = card.isFound || board.isFlipped(card);
const isFlipDisabled = isFlipped || board.isFlipDisabled;
return (
<label
className={
"swap swap-flip rounded-sm overflow-hidden transition-opacity duration-300 ease-out font-semibold text-lg border border-black/30" +
(isFlipped ? " swap-active" : "") +
(isFlipDisabled ? " pointer-events-none" : "") +
(card.isFound ? " opacity-0" : "")
}
onClick={() => board.flipCard(card)}
>
{/* Card with value */}
<div className="swap-on text-center size-12 flex justify-center place-items-center">
{card.value}
</div>
{/* Empty Card */}
<div className="swap-off bg-base-300 size-12" />
</label>
);
});
const levelLabelBySize = {
5: "Easy 5x5",
6: "Medium 6x6",
8: "Hard 8x8"
};
const App = observer(() => {
return (
<div className="rounded-lg p-6 pt-2 bg-base-200 w-fit m-4">
<h2 className="text-xl font-semibold text-center w-full">Memory Game</h2>
<div className="join w-full justify-center py-3">
{[5, 6, 8].map((level) => (
<input
className="join-item btn"
type="radio"
name="options"
aria-label={levelLabelBySize[level]}
checked={board.size === level}
onClick={() => board.updateSize(level)}
/>
))}
</div>
<div className="min-w-96 min-h-96 size-fit flex justify-center place-items-center">
{board.isGameFinished ? (
<button
className="btn btn-lg border-black! border-2!"
onClick={() => board.reset()}
>
Play again
</button>
) : (
<div className={"grid gap-4" + (" grid-cols-" + board.size)}>
{board.cards.map((card) => (
<CardBlock card={card} key={card.id} />
))}
</div>
)}
</div>
</div>
);
});
ReactDom.createRoot(document.getElementById("app")).render(<App />);
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.