Skip to content

Commit

Permalink
add some comments
Browse files Browse the repository at this point in the history
  • Loading branch information
vuonghy2442 committed Sep 27, 2024
1 parent ea0637c commit 07f3a2b
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 22 deletions.
98 changes: 77 additions & 21 deletions js/Card.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,47 @@

// ♤♡♢♧♠♥♦♣

/** @type {string[]} */
const RANK_MAP = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];

/** @type {string[]} */
const SUIT_MAP = ["♡", "♢", "♧", "♤"];

/** @type {number} */
export const N_SUITS = SUIT_MAP.length;

/** @type {number} */
export const N_RANKS = RANK_MAP.length;

/** @type {number} */
export const KING_RANK = N_RANKS - 1;

/** @type {number} */
export const N_CARDS = N_SUITS * N_RANKS;

/**
* @param {number} rank
* @param {number} suit
* @returns {number}
*/
export function cardId(rank, suit) {
return rank * N_SUITS + suit;
}

/**
* @typedef {Object} CardObject
* @property {number} rank
* @property {number} suit
*/

/**
* @param {Card} c
* @returns {string}
*/
function createCardSVG(c) {
const rank_str = RANK_MAP[c.rank];
const suit_str = SUIT_MAP[c.suit];
const color = c.suit < 2 ? "#e22" : "#001";

return `<svg xmlns="http://www.w3.org/2000/svg" class="card_front" fill="${color}" viewBox="0 0 250 350">
<g font-size="45">
<text x="5%" y="50">${rank_str}</text>
Expand All @@ -33,20 +56,33 @@ function createCardSVG(c) {
</svg>`;
}

/** @type {HTMLElement | null} */
let front_element = null;

export class Card {
/** @type {number} */
#rank;

/** @type {number} */
#suit;

/** @type {boolean} */
#flipped;

/** @type {boolean} */
#draggable;

/** @type {boolean} */
#animating;

/** @type {HTMLElement | null} */
#element;

/**
* @param {number} rank
* @param {number} suit
*/
constructor(rank, suit) {
if (typeof rank !== "number" || rank < 0 || rank > N_RANKS) throw new Error("Invalid rank");
if (typeof suit !== "number" || suit < 0 || suit >= N_SUITS) throw new Error("Invalid suit");

this.#rank = rank;
this.#suit = suit;
this.#flipped = false;
Expand All @@ -55,28 +91,34 @@ export class Card {
this.#element = null;
}

/** @returns {number} */
get id() {
return cardId(this.#rank, this.#suit);
}

/** @returns {number} */
get rank() {
return this.#rank;
}

/** @returns {number} */
get suit() {
return this.#suit;
}

/** @returns {boolean} */
get animating() {
return this.#animating;
}

/** @returns {HTMLElement | null} */
get element() {
return this.#element;
}

/** @returns {HTMLElement | null} */
get container() {
return this.#element.parentElement;
return this.#element ? this.#element.parentElement : null;
}

/**
Expand All @@ -94,16 +136,21 @@ export class Card {

moveToFront() {
if (this.#element === null) return;
this.container.appendChild(this.#element);
this.container?.appendChild(this.#element);
}

containerToFront() {
if (this.#element === null) return;
if (front_element !== null) front_element.style.zIndex = 0;
this.container.style.zIndex = 1;
if (front_element !== null) front_element.style.zIndex = "0";
if (this.container) this.container.style.zIndex = "1";
front_element = this.container;
}

/**
* @param {HTMLElement} container
* @param {number} posX
* @param {number} posY
*/
createDOM(container, posX, posY) {
if (this.#element !== null) return;
const cardElement = document.createElement("div");
Expand All @@ -113,67 +160,76 @@ export class Card {
${createCardSVG(this)}
</div>`;
const inner = cardElement.firstElementChild;
if (this.#flipped) inner.classList.add("flipped");

if (inner && this.#flipped) inner.classList.add("flipped");
cardElement.style.left = `${posX}%`;
cardElement.style.top = `${posY}%`;
cardElement.dataset.cardId = this.id;

cardElement.dataset.cardId = this.id.toString();
cardElement.addEventListener("transitionrun", (_) => {
this.#animating = true;
});

const doneAnimate = (_) => {
cardElement.style.removeProperty("transition");
this.#animating = false;
};

cardElement.addEventListener("transitioncancel", doneAnimate);
cardElement.addEventListener("transitionend", doneAnimate);

container.appendChild(cardElement);
this.#element = cardElement;
}

/**
* @param {number} duration
*/
turnUp(duration) {
if (this.#flipped) {
this.flipCard(duration);
}
}

/**
* @param {number} duration
*/
flipCard(duration) {
this.#flipped = !this.#flipped;
if (this.#element === null) return;
const inner = this.#element.firstElementChild;
inner.classList.toggle("flipped");
if (inner) inner.classList.toggle("flipped");
}

/**
* @param {HTMLElement | null} container
* @param {number | null} posX
* @param {number | null} posY
* @param {number} duration
*/
moveTo(container, posX, posY, duration) {
if (this.#element === null) return;
const sameContainer = container === null || container === this.container;
let currentLeft = parseFloat(this.#element.style.left) || 0;
let currentTop = parseFloat(this.#element.style.top) || 0;

if (sameContainer && Math.abs(currentLeft - posX) < 1e-2 && Math.abs(currentTop - posY) < 1e-2) {
return;
}

if (duration > 0) this.#element.style.transition = `top ${duration}ms ease-in, left ${duration}ms ease-in`;
if (posX !== null) this.#element.style.left = `${posX}%`;
if (posY !== null) this.#element.style.top = `${posY}%`;
if (!sameContainer) {
if (!sameContainer && container) {
const parent = this.container;
parent.removeChild(this.#element);
if (parent) parent.removeChild(this.#element);
container.appendChild(this.#element);
}
}

/** @returns {boolean} */
isDraggable() {
return this.#draggable && !this.#animating;
}

/**
* @param {Card} card
* @returns {boolean}
*/
goBefore(card) {
if (typeof card !== "object" || !(card instanceof Card)) throw new Error("Invalid card");
return this.#rank === card.#rank + 1 && (((this.#suit ^ card.#suit) & 2) === 2 || this.#rank === N_RANKS);
}
}
39 changes: 38 additions & 1 deletion js/Solitaire.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"use strict";
import { N_SUITS, cardId, Card, N_RANKS } from "./Card.js";
import { N_SUITS, Card, N_RANKS } from "./Card.js";

/** @type {number} */
export const N_PILES = 7;

/** @type {number} */
export const N_HIDDEN_CARDS = (N_PILES * (N_PILES + 1)) / 2;

export class Deck {
/**
* @param {Card[]} cards
* @param {number} drawStep
*/
constructor(cards, drawStep) {
if (!Array.isArray(cards)) throw new Error("Cards must be an array");
if (typeof drawStep !== "number" || drawStep <= 0) throw new Error("Draw step must be a positive number");
Expand All @@ -14,12 +21,19 @@ export class Deck {
this._drawStep = drawStep;
}

/**
* @param {number} n
* @returns {Card[]}
*/
peek(n) {
const len = this._waste.length;
const start = Math.max(len - n, 0);
return this._waste.slice(start, len);
}

/**
* @returns {Card}
*/
pop() {
return this._waste.pop();
}
Expand All @@ -35,6 +49,13 @@ export class Deck {
}
}

/**
* @typedef {Object} Pos
* @property {number} Deck - The position of a card on the deck.
* @property {number} Stack - The position of a card in a stack (e.g., tableau).
* @property {number} Pile - The position of a card in a pile which is offset by N_SUITS.
* @property {number} None - Indicates no specific position or an invalid/null position.
*/
export const Pos = {
Deck: 0,
Stack: 1,
Expand All @@ -43,6 +64,11 @@ export const Pos = {
};

export class Solitaire {
/**
* Creates an instance of Solitaire.
* @param {Array<Card>} cards - The array of all cards in the deck including hidden ones.
* @param {number} drawStep - The number of cards to deal from the top of the deck at each step.
*/
constructor(cards, drawStep) {
const hiddenCards = cards.slice(0, N_HIDDEN_CARDS);

Expand All @@ -64,6 +90,11 @@ export class Solitaire {
this.onRevealCallbacks = [];
}

/**
* Determines which positions cards can move to based on game rules.
* @param {Array<Card>} cards - The card(s) to check for movement possibilities.
* @returns {Array<string>} Positions where the card(s) can be moved: 'Deck', 'Pile', or 'Stack'.
*/
liftCard(cards) {
const card = cards[0];
const result = [];
Expand All @@ -80,6 +111,12 @@ export class Solitaire {
return result;
}

/**
* Executes a move in the game based on source and destination positions.
* @param {Card} card - The card to be moved.
* @param {string} src - Source position ('Deck', 'Pile', or 'Stack').
* @param {string} dst - Destination position ('Deck', 'Pile', or 'Stack').
*/
makeMove(card, src, dst) {
if (src === Pos.Deck && dst === Pos.Deck) {
this.deck.deal();
Expand Down

0 comments on commit 07f3a2b

Please sign in to comment.