Cards

/** Constructs a deck of playing cards. */
function DeckOfCards() {

  /* Fields */
  const thisDeck = this;
  const maxSize = Card.RANK_COUNT * Card.SUIT_COUNT + DeckOfCards.JOKER_COUNT;
  let deck = new Deque(maxSize);

  /* Initialization */
  for (let suit = 1; suit <= Card.SUIT_COUNT; suit++) {
    for (let i = 0, rank=2; i < Card.RANK_COUNT; i++, rank++) {
      const card = new Card(suit, rank);
      deck.push(card);
    }
  }
  for (let j = 0; j < DeckOfCards.JOKER_COUNT; j++) {
    deck.push(new Card(0, 0)); // Jokers
  }

  /* Methods */
  thisDeck.shuffle = function() {
    const currSize = deck.getSize();
    const shuffledDeck = new Deque(maxSize);
    for (let n = currSize; n > 0; n--) {
      const r = Math.random() * n;
      for (let i = 1; i < r; i++) {
        deck.unshift(deck.pop());
      }
      shuffledDeck.push(deck.pop());
    }
    deck = shuffledDeck;
    return thisDeck;
  };
  thisDeck.toString = function(lang) {
    let str = "";
    for (let i = 0; i < deck.getSize(); i++) {
      if (i > 0) str += ", ";
      let card = deck.shift();
      str += card.toString(lang);
      deck.push(card);
    }
    return str;
  };
}
DeckOfCards.JOKER_COUNT = 2;
Object.freeze(DeckOfCards);

/** Constructs a double-ended queue */
function Deque(maxSize) {

  /* Fields */
  const thisDeque = this;
  const deque = [];
  let size = 0;

  /* Properties */
  Object.defineProperty(thisDeque, "maxSize", {
    value: Number.isFinite(maxSize) && maxSize > 0 ? maxSize : Infinity,
    writable: false
  });

  /* Methods */
  thisDeque.getSize = function() {
    return size;
  };
  thisDeque.isEmpty = function() {
    return size === 0;
  };
  thisDeque.push = function(element) {
    if (size < thisDeque.maxSize) {
      deque.push(element);
      size++;
    }
    return thisDeque;
  };
  thisDeque.pop = function() {
    let element;
    if (size > 0) {
      element = deque.pop();
      size--;
    }
    return element;
  };
  thisDeque.shift = function() {
    let element;
    if (size > 0) {
      element = deque.shift();
      size--;
    }
    return element;
  };
  thisDeque.unshift = function(element) {
    if (size < thisDeque.maxSize) {
      deque.unshift(element);
      size++;
    }
    return thisDeque;
  };
}

Object.freeze(Deque);


/** Constructs a playing card. */
function Card(suit, rank) {

  const thisCard = this;

  /* Properties */
  Object.defineProperty(thisCard, "rank", {
    value: Number.isInteger(rank) && rank > 0 &&
        rank < Card.RANK_NAMES.length ? rank : 0,
    writable: false
  });
  Object.defineProperty(thisCard, "suit", {
    value: Number.isInteger(suit) && suit > 0 &&
        suit <= Card.SUIT_COUNT ? suit : 0,
    writable: false
  });

  /* Methods */
  thisCard.compare = function(otherCard) {
    return Card.compare(thisCard, otherCard);
  };
  thisCard.toString = function(lang) {
    return Card.toString(thisCard, lang);
  };
}

/* Static properties of Card */
Card.RANK_COUNT = 13;
Card.SUIT_COUNT = 4;
Card.DEFAULT_LANGUAGE = "French";
Card.RANK_NAMES = Object.freeze([
  "Joker","Ace","2","3","4","5","6","7",
  "8","9","10","Jack","King","Queen","King","Ace"
]);
Card.SUIT_NAMES = Object.freeze({
  French: [null,"Hearts","Diamonds","Clubs","Spades"],
  German: [null,"Hearts","Bells","Acorns","Leaves"]
});

/* Static methods of Card */
Card.compare = function(c1, c2) {
  return c1.rank - c2.rank;
}
Card.toString = function(card, lang) {
  if (!Card.SUIT_NAMES.hasOwnProperty(lang))
    lang = Card.DEFAULT_LANGUAGE;
  let str = Card.RANK_NAMES[card.rank];
  if (card.suit > 0)
    str += " of " + Card.SUIT_NAMES[lang][card.suit];
  return str;
};

Object.freeze(Card);

alert((new DeckOfCards()).shuffle().toString("German"));