=begin rdoc == RandomLab Random number generators and associated methods. =end =begin TODO number line -- mini-histogram, e.g. second tick drawn above first? else explain in lab manual / reference why two values per tickmark TODO another thing for documentation: diff between p.random(x,y) and random(x,y) [latter uses Ruby's PRNG] =end module RubyLabs module RandomLab require "permute.rb" =begin rdoc Pseudo-random number generator. Constraints on a, c, and m: * c and m must be relatively prime * a-1 divisible by prime factors of m * if m is multiple of 4, a-1 must also be a multiple of 4 Note: some good values (for small m) from Numerical Recipes: m = 53125, a = 171, c = 11213 =end class PRNG attr_accessor :a, :c, :m, :x def initialize(a, c, m) @a = a @c = c @m = m @x = 0 end def state return @x end def seed(val) @x = val end # :begin :advance def advance @x = (@x * @a + @c) % @m end # :end :advance # :begin :random def random(min, max) return nil if max <= min range = max - min + 1 return (advance() % range) + min end # :end :random def inspect sprintf "#<RandomLab::PRNG a: #{@a} c: #{@c} m: #{@m}>" end alias to_s inspect end # class PRNG # :begin :prng_sequence def prng_sequence(a, c, m) seq = [0] (m-1).times do seq << (a * seq.last + c) % m end return seq end # :end :prng_sequence =begin rdoc Make a new deck of cards =end def new_deck (0..51).map { |i| Card.new(i) } end =begin rdoc A "helper method" that can be called via a probe, to print the contents of an array during the execution of the permute! method =end # Note: permute! moved to own source file, permute.rb def brackets(a, i, r) res = "#{r}: " if i <= 0 res += ("[" + a.join(" ") + "]") elsif i >= a.length res += (" " + a.join(" ") + " [ ]") else pre = a.slice(0..(i-1)) post = a.slice(i..-1) res += (" " + pre.join(" ") + " [" + post.join(" ") + "]") end return res end =begin rdoc Classify a poker hand -- returns a symbol describing a hand. Return values: :pair :two_pair :three_of_a_kind :full_house :straight :flush :four_of_a_kind :straight_flush =end def poker_rank(a) rcount = Array.new(Card::Ranks.length, 0) scount = Array.new(Card::Suits.length, 0) a.each do |x| rcount[ Card::Ranks.index(x.rank) ] += 1 scount[ Card::Suits.index(x.suit) ] += 1 end if rcount.max == 1 straight = (rcount.rindex(1) - rcount.index(1) == 4) flush = scount.max == 5 return :straight_flush if (straight && flush) return :straight if straight return :flush if flush return :high_card else rcount.reject! { |x| x == 0 } rcount.sort! { |x,y| y <=> x } return :four_of_a_kind if rcount[0] == 4 return :full_house if (rcount[0] == 3 && rcount[1] == 2) return :three_of_a_kind if rcount[0] == 3 return :two_pair if (rcount[0] == 2 && rcount[1] == 2) return :pair end end def poker_rankings return [:high_card, :pair, :two_pair, :three_of_a_kind, :straight, :flush, :full_house, :four_of_a_kind, :straight_flush] end def poker_counts h = Hash.new poker_rankings.each { |x| h[x] = 0 } return h end =begin rdoc Class to represent cards from a standard 52-card deck. Includes comparators to sort by rank or suit. Call Card.new to get a random card, or Card.new(id) to get a specific card where id is a number between 0 and 51. =end # To test the expression that assigns suits and ranks: # 52.times { |x| puts (x/13).to_s + " " + (x%13).to_s } class Card attr_accessor :rank, :suit unless defined? Suits Suits = [:spades, :hearts, :diamonds, :clubs] end unless defined? Ranks Ranks = [:ace, :king, :queen, :jack, :ten, :nine, :eight, :seven, :six, :five, :four, :three, :two] end def initialize(id = nil) id = rand(52) if id.nil? raise "card must be between 0 and 51" if id < 0 || id > 51 @suit = Suits[id / 13] @rank = Ranks[id % 13] end def ==(x) return @suit == x.suit && @rank == x.rank end def <=>(x) r0 = Ranks.index(@rank); r1 = Ranks.index(x.rank) s0 = Suits.index(@suit); s1 = Suits.index(x.suit) if (res = (s0 <=> s1)) == 0 return (r0 <=> r1) else return res end end @@outputform = :utf8 def inspect s = "" s << case @rank when :ace : "A" when :king : "K" when :queen : "Q" when :jack : "J" when :ten : "10" when :nine : "9" when :eight : "8" when :seven : "7" when :six : "6" when :five : "5" when :four : "4" when :three : "3" when :two : "2" end if $KCODE[0] == ?U if @@outputform == :utf8 s << case @suit when :spades : "\xe2\x99\xa0" when :hearts : "\xe2\x99\xa5" when :clubs : "\xe2\x99\xa3" when :diamonds : "\xe2\x99\xa6" end else s << "!irb" + @suit.to_s.chop end else s << case @suit when :spades : "S" when :hearts : "H" when :clubs : "C" when :diamonds : "D" end end return s # "#{@rank} #{@suit}" end alias to_s inspect def Card.print_latex @@outputform = :latex end def Card.print_utf8 @@outputform = :utf8 end end # class Card =begin rdoc Visualization methods called by students to test a PRNG object: view_numberline(n) make a number line for integers between 0 and n-1 tick_mark(i) draw a tick mark at location i on the number line view_histogram(n, max) make a histogram with n bins for value from 0 to max view_histogram(a) make a histogram with one bin for each item in a update_bin(x) add 1 to the count of items in bin x get_counts get a copy of the bins (array of counts) view_dotplot(n) initialize an n x n dotplot plot_point(x,y) add a dot at (x,y) to the dotplot =end def view_numberline(npoints, userOptions = {}) Canvas.init(500, 100, "RandomLab::NumberLine") options = @@numberLineOptions.merge(userOptions) line = Canvas.line(0, 70, 500, 70, :width => options[:lineThickness], :fill => options[:lineColor]) @@drawing = NumberLine.new(line, npoints, options) return true end def tick_mark(i) if @@drawing.class != NumberLine puts "call view_numberline to initialize the number line" elsif i < 0 || i >= @@drawing.npoints puts "tick_mark: 0 <= i < #{@@drawing.npoints}" else x0, y0, x1, y1 = @@drawing.line.coords tx = (i.to_f / @@drawing.npoints) * (x1-x0) ty = y0 - @@drawing.options[:tickHeight] Canvas.line(tx, y0, tx, ty, :width => @@drawing.options[:tickWidth], :fill => @@drawing.options[:tickColor]) sleep(@@delay) end return true end def view_histogram(*args) begin if args[0].class == Array userOptions = args.length > 1 ? args[1] : { } raise "usage: view_histogram(keys, options)" unless userOptions.class == Hash keys = args[0] nbins = max = keys.length else userOptions = args.length > 2 ? args[2] : { } raise "usage: view_histogram(nbins, max, options)" unless userOptions.class == Hash nbins = args[0] max = args.length > 1 ? args[1] : nbins keys = nil end rescue Exception => e puts e return false end Canvas.init(500, 300, "RandomLab::Histogram") counts = Hash.new(0) options = @@histogramOptions.merge(userOptions) bins = [] binHeight = 3 binBorder = 2 binWidth = (500/(nbins+1)) binTop = 280 nbins.times do |i| x = i * binWidth + binWidth/2 bins << Canvas.rectangle( x + binBorder, binTop, x + binWidth - binBorder, binTop + binHeight, :outline => options[:binColor], :fill => options[:binColor] ) end @@drawing = Histogram.new(bins, max.to_f, keys, counts, binTop, options) return true end def update_bin(x) if @@drawing.class != Histogram puts "call view_histogram to initialize a histogram" return nil end if @@drawing.keys i = @@drawing.keys.index(x) if i.nil? puts "unknown bin: #{x}" return nil end else xmax = @@drawing.max nb = @@drawing.bins.length if x < 0 || x >= xmax puts "x must be between 0 and #{xmax-1}" return nil end i = ((x / xmax) * nb).to_i end @@drawing.counts[i] += 1 rect = @@drawing.bins[i] x1, y1, x2, y2 = rect.coords y1 = y1 - @@drawing.options[:boxIncrement] rect.coords = [x1, y1, x2, y2] if y1 < @@drawing.options[:rescaleTrigger] base = @@drawing.base @@drawing.bins.each do |rect| x1, y1, x2, y2 = rect.coords y1 = base - ((base - y1) / 2) rect.coords = [x1, y1, x2, y2] end @@drawing.options[:boxIncrement] /= 2 end return true end def get_counts if @@drawing.class == Histogram return @@drawing.counts else puts "current drawing is not a histogram" return nil end end def view_dotplot(npoints, userOptions = {}) Canvas.init(500, 500, "RandomLab::DotPlot") options = @@dotPlotOptions.merge(userOptions) @@drawing = DotPlot.new(npoints, options) return true end def plot_point(x,y) if @@drawing.class != DotPlot puts "call view_dotplot to initialize a dot plot" elsif x < 0 || x >= @@drawing.max || y < 0 || y >= @@drawing.max puts "plot_point: 0 <= x, y < #{@@drawing.max}" else px = (x.to_f / @@drawing.max) * Canvas.width py = (y.to_f / @@drawing.max) * Canvas.height r = @@drawing.options[:dotRadius] color = @@drawing.options[:dotColor] Canvas.circle( px, py, r, :outline => color, :fill => color ) end return nil end NumberLine = Struct.new(:line, :npoints, :options) Histogram = Struct.new(:bins, :max, :keys, :counts, :base, :options) DotPlot = Struct.new(:max, :options) @@numberLineOptions = { :lineThickness => 3, :lineColor => '#777777', :tickHeight => 20, :tickWidth => 1, :tickColor => '#0000FF', } @@histogramOptions = { :binColor => '#000080', :boxIncrement => 8.0, :rescaleTrigger => 50, } @@dotPlotOptions = { :dotColor => '#000080', :dotRadius => 1.0, } @@drawing = nil @@delay = 0.1 end # RandomLab end # RubyLabs