#!/usr/local/bin/ruby -w # # pyramid.rb: plays the pyramid solitaire game # class Array def choose(k) if k == 1 map{|i| [i] } else (0...size).map{|i| slice(i+1...size).choose(k-1).map{|s| slice(i,1) + s } }.inject([]) {|a,i| a + i} end end end class Card attr_reader :n, :name, :abbrev def initialize(n = 0, suit = 0) @n = n @suit = suit @parents = [] @covered = 0 @name = %w(Zero Ace Two Three Four Five Six Seven Eight Nine Ten Jack Queen King)[n] + ' of ' + %w(Nothing Spades Clubs Diamonds Hearts)[suit] @abbrev = 'XA234567890JQK'[n,1] + 'NSCDH'[suit,1] end def free? @covered == 0 end def uncover @covered -= 1 end def cover @covered += 1 end def remove @parents.each {|p| p.uncover} end def add_parent(p) @parents << p p.cover end end ## deck object: fills with Cards on creation, pops them in random order class Deck < Array def initialize super for suit in 1..4 for n in 1..13 push Card.new(n, suit) end end end def pop slice!(rand(size)) end end ## sort a pair in the following preference order: ## 1) both cards from the pyramid ## 2) one from the pyramid, one from the waste pile (top) ## 3) one from the pyramid, one from the top of the deck (deck_top) ## 4) top and deck_top def rate(pair, top, deck_top) n = (pair | [top, deck_top]).size if n == 0 1 elsif n == 2 4 elsif pair.include? top 2 else 3 end end def play(verbose = true) ### build deck deck = Deck.new ### build pyramid pyramid = [] levels = [] 7.downto(1) {|n| levels[n] = [] n.times { c = deck.pop levels[n] << c pyramid << c } if n < 7 for i in 0..(levels[n+1].size-2) levels[n+1][i].add_parent levels[n][i] end for i in 1...(levels[n+1].size) levels[n+1][i].add_parent levels[n][i-1] end end } if verbose puts 'Pyramid is: ' for n in 1..7 puts(' ' * (7 - n) + levels[n].map {|c| c.abbrev }.join(' ')) end puts end ### play game zero = Card.new ## hack for kings waste = [] ## waste pile redeals = 0 ## how many redeals we have had deck_top = deck.pop puts 'Drew: ' + deck_top.name if verbose until pyramid.empty? or deck.empty? free = pyramid.find_all {|c| c.free? } + [zero] top = waste[0] ## find all pairs equalling 13 including the top of the waste pile ## and the next card from the deck while (pairs = (free + [deck_top] + waste[0,1]).choose(2).find_all{|p| p[0].n + p[1].n == 13 }).empty? and !deck.empty? waste.unshift top = deck_top deck_top = deck.pop puts "Drew: #{deck_top.name}; Discarded: #{top.name}" if verbose end ## ran out of cards to pop; flip or lose if deck.empty? break if waste.empty? or redeals == 2 puts 'Flipped over waste pile' if verbose redeals += 1 ## put the wastes back into the deck, not random this time deck = waste waste = [] next end ## sort the pairs by the rules in rate() pair = pairs.sort {|a, b| rate(a, top, deck_top) <=> rate(b, top, deck_top) }[0] ## remove placeholder card pair.delete zero puts 'Removed: ' + pair.map{|c| c.name + ' from ' + if c == top 'waste' elsif c == deck_top 'deck' else 'pyramid' end }.join(', ') if verbose for card in pair if card == top waste.shift elsif card == deck_top deck_top = deck.pop else pyramid.delete(card) card.remove end end end left = waste.size left += 1 if deck_top score = (pyramid.empty? ? [50, 35, 20][redeals] : 0) - left if verbose puts if pyramid.empty? print "You cleared the pyramid with #{redeals} redeal" + (redeals == 1 ? '' : 's') + ' and ' else print 'You did not clear the pyramid with ' end puts "#{left} cards not discarded, for a score of #{score}." end score end if ARGV[0] =~ /^\d+$/ n = ARGV[0].to_i total = 0 start = Time.now n.times { total += play(false) } finish = Time.now secs = '%.2f' % (finish - start) avg = '%.2f' % (total.to_f / n) puts "#{n} games played in #{secs} seconds. Average score: #{avg}." else play end