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