# Ruby program to simulate a game of Narcotic Solitaire

In the game of Narcotic Solitaire, played with a standard 52-card deck, the player deals a row of four cards onto the table, from left to right. If all four cards are of the same value, they are removed from the game. If two or three cards are of the same value, they are moved on top of the leftmost card of that value. Once the whole deck has been dealt, the piles are gathered from right to left, and the game continues until all the cards have been removed.

As there are no elements of choice in this game, I wrote a Ruby program to simulate many games and determine how long it will take to complete. The game cannot always be won: sometimes it will enter an infinite loop, a fact I discovered only through writing the program!

SECONDS_IN_MINUTE = 60
MINUTES_IN_HOUR = 60
PERCENT = 100
CARDS_PER_DEAL = 4
CARDS_IN_DECK = 52
NUM_SUITS = 4

# Given an array of four numbers, returns an array of arrays with all identical
# numbers shifted to the leftmost matching number.
# e.g. [4, 4, 3, 1] returns [[4, 4], [], [3], [1]], and
# [1, 4, 4, 4] returns [[1], [4, 4, 4], [], []].
# If all the values are the same, returns an array of four empty arrays,
# equivalent to removing the cards from the game.
def proper_piles(cards)
piles = Array.new(CARDS_PER_DEAL){[]}
# Return an array of four empty arrays if the cards all match.
return piles if cards.all?{|card| cards[0] == card}
# Enclose each card in the array in its own array
piles = cards.map{|card| [card]}
# Check every card for matches with subsequent cards, and move it in the
# event of a match
piles.each_index do |i|
(i..piles.size - 1).each do |j|
if piles[i][0] == piles[j][0]
moved_card = piles[j].delete_at(0)
# If we compare two empty arrays, moved_card will be nil,
# which is why we have the if clause below.
piles[i] << moved_card if !moved_card.nil?
end
end
end
piles
end

def narcotic_iterations(deck)
seconds_to_place_card = 1
seconds_to_gather_deck = 5
time = 0
did_loop = false
past_decks = []
# Outer loop executes until all the cards have been taken away
until deck.empty?
piles = Array.new(CARDS_PER_DEAL){Array.new}
# Inner loop goes through one deck
until deck.empty?
# Get the proper arrangement of the cards dealt from the deck
deal = proper_piles(Array.new(CARDS_PER_DEAL){deck.pop})
deal.each_index do |i|
# += is used rather than indexing and << because array + []
# has no effect, exactly what we want.
piles[i] += deal[i]
end
time += CARDS_PER_DEAL * seconds_to_place_card
end

# Finally, merge all the piles into a single deck.
# The deck is not reversed because it is face up through
# the whole game, and piles are gathered from right to left.
deck = piles.flatten
# Detect an infinite loop
if past_decks.include?(deck)
did_loop = true
break
end
past_decks << deck.clone
time += seconds_to_gather_deck
end
return time, did_loop
end

def narcotic_benchmark(num_trials)
# Suits are not necessary for the game, only card values
deck = Array.new(CARDS_IN_DECK){|i| i%(CARDS_IN_DECK / NUM_SUITS) + 1}
total_time = 0
num_loops = 0
num_trials.times do |i|
iteration_time, did_loop = narcotic_iterations(deck.shuffle)
if did_loop
num_loops += 1
else
total_time += iteration_time
end
end
average_time = (total_time.to_f / num_trials)
loop_percent = (num_loops.to_f / num_trials) * PERCENT
completion_time_in_hours =
(average_time / (SECONDS_IN_MINUTE * MINUTES_IN_HOUR)).round
completion_time_in_minutes =
(average_time / MINUTES_IN_HOUR).round % MINUTES_IN_HOUR
puts "Average estimated completion time is %d hours %d minutes" % [
completion_time_in_hours, completion_time_in_minutes]
puts "%.2f percent of decks can be completed" % [PERCENT - loop_percent]
end

if ARGV[0].nil?
puts "Please provide the number of times to run the simulation."
else
narcotic_benchmark(ARGV[0].to_i)
end


Sample output:

$ruby narcotic.rb 50 Average estimated completion time is 2 hours 14 minutes 34.00 percent of decks can be completed$ ruby narcotic.rb 500
Average estimated completion time is 3 hours 37 minutes
48.60 percent of decks can be completed

• Are you asking about fixing said infinite loop? Commented Jan 16, 2015 at 3:03
• @Jamal No, not at all. I have that handled in the program, as can be seen. Commented Jan 16, 2015 at 3:08

Looks really good! I would change this:

    if did_loop
num_loops += 1
else
total_time += iteration_time
end


to this:

    did_loop ? num_loops += 1 : total_time += iteration_time


You very rarely see these "short"

if..
one line..
else
one line..
end


constructions in ruby.

piles[i] << moved_card if !moved_card.nil?


Don't use if !(condition) for simple conditions, use unless. But in this particular case you are just checking against nil, which is already 'falsey'. Just do this:

piles[i] << moved_card if moved_card


or if you prefer this syntax,

moved_card && piles[i] << moved_card


(but the first is more readable)

• I made the second fix, but for the first: if I don't include a space before the question mark, I get syntax error, unexpected ':', expecting end-of-input, and if I do, I also get syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '(' Commented Jan 21, 2015 at 4:49