HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

One-handed Solitaire algorithm

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
handedalgorithmonesolitaire

Problem

My attempt at a simulation of one-handed Solitaire. Any comments welcome!

// Basic implementation of one handed solitaire by Andrew Ewen.
//
// Not sure if working correctly as it wins roughly 1/90 games whereas wikipedia states it 
// is closer to 1/140.

function Card (suit, rank) {
    this.suit = suit;
    this.rank = rank;
}

Card.suits = ["Spades", "Hearts", "Clubs", "Diamonds"];
Card.prototype.toString = function () {
    var suit = Card.suits[this.suit];
    var rank = (this.rank + 1).toString();

    switch (this.rank) {
        case 0:
            rank = "Ace";
            break;
        case 10:
            rank = "Jack";
            break;
        case 11:
            rank = "Queen";
            break;
        case 12: 
            rank = "King";  
            break;
    }   

    return rank + " of " + suit;    
}

function debugLog(msg) {
    var enabled = false;
    if (enabled) console.log(msg);
}

var deck = function () {
    cards = [];
    // Create deck of cards
    for (var suit = 0; suit  0; i--) {
        var j = Math.floor(Math.random() * (i+1));
        var t = cards[j];
        cards[j] = cards[i];
        cards[i] = t;
    }
    return cards;
};

// Stats
var wins = 0;
var losses = 0;

for(var i = 0; i  0) {
            hand.push(pile.pop());
        }

        // Check if we're on our last 2 cards.
        var last2 = hand.length  " + cardB); }

        debugLog("hand: " + hand.length);
        debugLog("pile: " + pile.length);
        debugLog("discard: " + discard.length);
        debugLog("");

        // Detect win and loss conditions
        if (pile.length === 0 && !match) {
            debugLog("You lose...");
            losses += 1;
            gameOver = true;
        }
        if (pile.length === 0 && hand.length === 0) {
            debugLog("You win!");
            wins += 1;
            gameOver = true;
        }
    }
    console.log("Win rate: " + (wins/losses));
}

Solution

Overall it's pretty good, but there might be something iffy going on with the rules you use.

I had honestly never heard of one-handed solitaire, but if I understand it correctly, there are no special rules for the last cards. You either have exactly 4 cards left, and win if you can discard them all, or you lose.

But you have a last2 rule, where you change the rules to look at the 1st and 2nd card if there are less than four cards left. I imagine this skews the results.

Anyway, in terms of code:

-
Don't mix plain console.log and a custom logging function.

-
Put the code for playing 1 round into a function. Right now it's all inside a loop that runs 10000 times. It'd be a lot simpler to just call play() and get a won/lost boolean back. You can always loop that.

-
It'd be simpler to unshift cards onto the hand. Then you'd always be comparing hand[0] to hand[3] instead of having to subtract stuff from hand.length. Splicing also becomes just splice(0, 4) or splice(1, 2)

-
You can use concat instead of Array.prototype.push.apply (or the slightly shorter alternative [].push.apply). The difference, however, is that concat returns a new array instead of modifying the receiver like push does.

-
You can simplify Card and Deck a little by using a list of values for rank as well as suit:

function Card(suit, rank) {
  this.suit = suit;
  this.rank = rank;
}

Card.prototype = {
  toString: function () {
    return this.rank + this.suit;
  }
};

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

function Deck() {
  var cards, shuffled, i;

  cards = Card.suits.reduce(function (cards, suit) {
    return cards.concat(Card.ranks.map(function (rank) {
      return new Card(suit, rank);
    }));
  }, []);

  shuffled = [];
  while(cards.length) {
    i = Math.random() * cards.length | 0;
    shuffled.push(cards.splice(i, 1)[0]);
  }

  return shuffled;
}


I've named Deck with PascalCase as though it's a constructor, which it isn't quite. But you can call it as new Deck or just Deck() and either thing'll work, as it explicitly returns something.

You could also take a different tack altogether. You don't really need a hand array: You can just examine the shuffled deck directly. For instance:

function play() {
  var deck = new Deck,
      i = deck.length,
      a, b;

  while(true) {
    // decrement if we're too close to the end of the deck
    i = Math.min(i, deck.length-4);

    // if we had to decrement past zero, game's over
    if(i < 0) {
      return deck.length === 0;
    }

    // the cards to examine
    a = deck[i],
    b = deck[i+3];

    // match cards or decrement
    if(a.rank === b.rank) {
      deck.splice(i, 4);
    } else if(a.suit === b.suit) {
      deck.splice(i + 1, 2);
    } else {
      i--;
    }
  }
}


(Edit: I updated the code based on @Jonah's comment below to examine the deck from the end, rather than the beginning.)

Running 300,000 games, I get a 0.730% chance of winning, which is pretty close to 1 in 140.

Edit: It makes for a nice animation - see snippet below



var frameTime = 100;
var button = document.getElementsByTagName("button")[0];
var span = document.getElementsByTagName("span")[0];

function Card(suit, rank) {
this.suit = suit;
this.rank = rank;
}

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

function Deck() {
var cards, shuffled, i;

cards = Card.suits.reduce(function (cards, suit) {
return cards.concat(Card.ranks.map(function (rank) {
return new Card(suit, rank);
}));
}, []);

shuffled = [];
while(cards.length) {
i = Math.random() * cards.length | 0;
shuffled.push(cards.splice(i, 1)[0]);
}

return shuffled;
}

function play() {
var deck = new Deck,
i = deck.length - 4;

button.disabled = true;
span.innerHTML = "";

function step() {
var a, b;

i = Math.min(i, deck.length - 4);
drawState(deck, i);

if(i = 0) {
drawTriangle(offset * 10 + 5, 25);
drawTriangle((offset + 3) * 10 + 5, 25);
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
ctx.fillRect(offset * 10, 0, 40, 22);
}

ctx.fillStyle = "#000";
ctx.font = "10px serif";
ctx.textAlign = "center";

deck.forEach(function (card, x) {
x = x * 10 + 5;
ctx.fillText(card.rank, x, 10);
ctx.fillText(card.suit, x, 20);
});
}

button.addEventListener("click", play, false);
play();



Play again

Code Snippets

function Card(suit, rank) {
  this.suit = suit;
  this.rank = rank;
}

Card.prototype = {
  toString: function () {
    return this.rank + this.suit;
  }
};

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


function Deck() {
  var cards, shuffled, i;

  cards = Card.suits.reduce(function (cards, suit) {
    return cards.concat(Card.ranks.map(function (rank) {
      return new Card(suit, rank);
    }));
  }, []);

  shuffled = [];
  while(cards.length) {
    i = Math.random() * cards.length | 0;
    shuffled.push(cards.splice(i, 1)[0]);
  }

  return shuffled;
}
function play() {
  var deck = new Deck,
      i = deck.length,
      a, b;

  while(true) {
    // decrement if we're too close to the end of the deck
    i = Math.min(i, deck.length-4);

    // if we had to decrement past zero, game's over
    if(i < 0) {
      return deck.length === 0;
    }

    // the cards to examine
    a = deck[i],
    b = deck[i+3];

    // match cards or decrement
    if(a.rank === b.rank) {
      deck.splice(i, 4);
    } else if(a.suit === b.suit) {
      deck.splice(i + 1, 2);
    } else {
      i--;
    }
  }
}

Context

StackExchange Code Review Q#108645, answer score: 4

Revisions (0)

No revisions yet.