2
\$\begingroup\$

For an interview, I had to create a card game called match. The rules were the following:

Players can choose a number of packs of playing cards and combine them into a single deck. The deck can be shuffled.

Cards are played sequentially from the top of the deck into the pile,

If two cards played sequentially match, the first player to declare "Match" takes all the cards in the pile.

(For the purpose of the simulation the program should randomly choose a player to have declared "Match" first).

The play then continues with the next card in the deck, starting a new pile. The game ends no more cards can be drawn from the deck and no player can declare "Match!".

The player that has taken the most cards is the winner. The game may end in a draw.

The match condition can be the following

The suits of two cards must match
Example: "3 of hearts" and "5 of hearts" match because they are both hearts.
The values of two cards must match
Example: "7 of hearts" and "7 of clubs" match because they both have the value 7.
Both suit and value must match
Example: "Jack of spades" only matches another "Jack of spades"


The program
As input, the program should ask:

how many packs of cards to use for the deck
which match condition to use
It should then simulate the game.

The program should output the results by either declaring the winner, or a draw.

I had a timebox of 90 minutes to complete this task but I ran out of time. My approach was to basically eventually have a front end plugged into this so wrote the code with that approach. After I submitted I realized I didn't add the check for a draw.

Here is deck.js

const suits = ["♠", "♥", "♣", "♦"];
const values = [
  "2",
  "3",
  "4",
  "5",
  "6",
  "7",
  "8",
  "9",
  "10",
  "J,",
  "Q",
  "K",
  "A",
];

class Deck {
  constructor(packs = 1) {
    this.packs = packs;
    this.deck = [];
  }

  newDeck() {
    let counter = this.packs;
    while (counter) {
      for (let suit of suits) {
        for (let value of values) {
          this.deck.push(new Card(value, suit));
        }
      }
      counter--;
    }

    return this.deck;
  }

  get deckSize() {
    return this.deck.length;
  }

  shuffle() {
    let counter = this.deckSize,
      temp,
      i;
    while (counter) {
      i = Math.floor(Math.random() * counter--);
      temp = this.deck[counter];
      this.deck[counter] = this.deck[i];
      this.deck[i] = temp;
    }
    return this.deck;
  }

  deal() {
    let hand = [];
    while (hand.length < 2) {
      hand.push(this.deck.shift());
    }
    return hand;
  }

}

class Card {
  constructor(value, suit) {
    this.value = value;
    this.suit = suit;
  }
}

module.exports = {Deck, Card}

here is my app.js

const { Deck } = require("./deck.js");
const MatchCondition = Object.freeze({ suits: 1, values: 2, both: 3 });
const players = ["playerOne", "playerTwo"];
const playerScores = {
  playerOne: 3,
  playerTwo: 4,
};

let deck;
let cards = {};
let result = "";

const cardsAreEqual = (card1, card2) => {
  Object.keys(card1).length === Object.keys(card2).length &&
    Object.keys(card1).every((p) => card1[p] === card2[p]);
};

const matchCondition = (cards, condition) => {
  //Need to add some error handling for when the conditions
  //Does not match a MatchCondition
  let result = "";

  if (condition === MatchCondition.suits) {
    if (cards[0].suit === cards[1].suit) {
      result = `The Suits MATCH !!!`;
    }
  }
  if (condition === MatchCondition.value) {
    if (cards[0].value === cards[1].value) {
      result = `The Values MATCH !!!`;
    }
  }
  if (condition === MatchCondition.both) {
    if (cardsAreEqual(cards[0], cards[1])) {
      result = `The Cards MATCH !!!`;
    }
  }
  return result;
};

const winningScore = (playerObj) => {
  const [key, value] = Object.entries(playerObj).reduce((r, e) =>
    e[1] > r[1] ? e : r
  );
  return { [key]: value };
};

const startGame = (numOfPacks, condition) => {
  const randomPlayer = Math.floor(Math.random() * players.length);
  deck = new Deck(numOfPacks);
  deck.newDeck();
  deck.shuffle();

  if (deck.deckSize !== 0) {
    cards = deck.deal();
    result = matchCondition(cards, condition);

    if (result !== "") {
      playerScores[players[randomPlayer]]++;
      return `${players[randomPlayer]} shouted ${result}`;
    }
  } else {
    return winningScore(playerScores);
  }
};

startGame(2, MatchCondition.suits);
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Code building technique and structuring is the basis for continual progress and consistent quality. Algorithmic insights cannot overcome code that is perpetually 80% complete, so to speak. So I ask: What broke the time limit? Where/how was time spent? Sticking points? How was the code constructed; what was written in what order? How was functionality built up? How much bugginess was popping up as code evolved? For example did a "use case" bug seem to be threaded across multiple objects and/or functions? Was new code just using/calling existing code or forcing internal change? \$\endgroup\$
    – radarbob
    Commented Feb 10, 2021 at 1:39
  • \$\begingroup\$ @radarbob I think what broke the time limit was I underestimated what was required. My initial mistake was trying to create this project in a way that would also have a frontend. I think I should have focussed more on getting the basic functionality to work before thinking of the front end \$\endgroup\$ Commented Feb 10, 2021 at 8:11
  • 1
    \$\begingroup\$ trying to create this project in a way that would also have a frontend -- there IS a front end. It's the console. Note the last bit of code in the answers below. The game is instantiated, executed, and output displayed all from the console. A GUI would be doing exactly the same thing but with buttons and such. Also random pretty flashy thingies to convince folks to buy this game. \$\endgroup\$
    – radarbob
    Commented Feb 10, 2021 at 8:19

2 Answers 2

1
\$\begingroup\$

Only what is needed

Programming is a creative process and things like time pressure have a negative effect on the whole project (even little 90min projects). That said software development is a business that must be profitable. Time is a limited resource so there will always be time pressure.

As a coder you must write efficiently, spending time on details not required puts pressure on you. The result is you don't think it through, you make mistakes and the quality of the code suffers.

There is not much to review as most of the code is superfluous, and what counts (the output) is incomplete and thus does not work.

What was the task?

Your question outlines the task, but your code suggests a much more complex brief.

If taken at face value (outline in question) you have overcooked the solutions.

Task Return highest score or "draw" of the card game "Snap" (variation there of) for 2 or more players playing with 1 or more decks.

Interpretation of rules of "Snap"

I don't think that there are 3 different rules, a match is any of 3 different conditions. Same suit, save value, or same suit And value. (the last is not needed)

Focus on the task.

An application is just a function where all you know about its behavior is down to what the function returns.

Don't code for unrelated content

There is no output with cards, or which player wins, only a number or a string.

Why bother with players and player names, only the player scores matter. That can be done with an array of values.

Why bother with the cards, the card only represent the number of hands dealt.

Then its the odds of a match per card dealt that determines if a random player wins the stack. There is a 1 in 4 odds that suits will match and 1 in 13 that the values will match. The odds of a match is the sum of (1 / 13 + 1 / 4)

To account for the end game (number of cards not collected when there is no match on last hand) I reduced the odds per card by 1/52

Thus you could have written

function Snap(decks, players) {
    const match = () => Math.random() <= (1 / 4 + 1 / 13 - 1 / 52);
    const scores = new Array(players).fill(0);
    var stack = 1, best = 0, cards = decks * 52 - 1;
    function play() {
        cards --;
        if (match()) {
            best = Math.max(scores[Math.random() * players | 0] += stack + 1, best);
            stack = 0;
            cards --;
        }
        stack ++;
    }    
    return Object.freeze({
        play,
        get gameOver() { return cards <= 0 },
        bestScore: () => { 
            1 === scores.reduce((c, pts)=> c + (pts === best ? 1 : 0), 0)? best: "Draw"; 
        },
    });
}
const game = Snap(2, 2); 
while (!game.gameOver) { game.play() }
const result = game.bestScore();

Total time to write ~15 min.

\$\endgroup\$
0
\$\begingroup\$

deck.js

The Card class can be removed entirely by writing newDeck() like so.

  newDeck() {
    this.cards = [];
    for (let i=0;i<this.packs;++i) {
      for (const suit of suits) {
        for (const value of values) {
          this.cards.push({value,suit});
        }
      }
    }

    return this.cards;
  }

Shuffling logic can be separated from class.

function shuffle(array) {
    for (let i=0;i<array.length;++i) {
        let j = array.length -1 - Math.floor(Math.random()*i);
        const temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

class Deck{
  // ...
  shuffle() {
    shuffle(this.cards);
  }
}

app.js

It seems weird to be checking which match condition to use while simulating a game. Grabbing a condition on init would be more suited ♠.

const MatchCondition = {
  'suits': ([a,b]) => 
    a.suit === b.suit,
  'value': ([a,b]) =>
    a.value === b.value,
  'both': ([a,b]) => 
    MatchCondition.suits(a,b) && MatchCondition.value(a,b)
}

Your start game function seems to actually simulate the entire game, so a rename would be appropriate.

const maxEntryFromDict = (dict) => {
  const [key, value] = Object.entries(dict).reduce(
    (r, e) => e[1] > r[1] ? e : r
  );
  return { [key]: value };
};

const randomChoice = (items) =>
  items[Math.floor(Math.random()*items.length)]

const playGame = (numOfPacks, condition) => {
  const scores = {
    playerOne: 3,
    playerTwo: 4,
  };
  const randomPlayer = randomChoice(Object.keys(scores));
  const deck = new Deck(numOfPacks);
  deck.newDeck();
  deck.shuffle();

  while (deck.cards.length > 0) {
    const cards = deck.deal();
    console.log(cards);

    if (condition(cards)) {
      scores[randomPlayer]++;
      console.log(`${randomPlayer} shouted Match!`);
    }
  }
  return maxEntryFromDict(scores);
};

const game_result = playGame(2, MatchCondition.suits);
console.log(game_result);

There's definitely some incorrect behaviours remaining including the fact that piles are not kept track of at all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.