Skip to content

Instantly share code, notes, and snippets.

@jamie
Created June 19, 2018 17:16
Show Gist options
  • Save jamie/fc0005d256c36e84bdac1a8a1edd80d6 to your computer and use it in GitHub Desktop.
Save jamie/fc0005d256c36e84bdac1a8a1edd80d6 to your computer and use it in GitHub Desktop.
class Deck
attr_accessor :needs_shuffle
def initialize(cards = [%w(x2 miss), %w(2 -2), %w(1 -1) * 5, %w(0) * 6].flatten)
@orig_cards = cards
shuffle!
end
def add(cards)
@orig_cards += cards
self
end
def remove(cards)
cards.each do |card|
index = @orig_cards.index(card)
@orig_cards.delete_at(index)
end
self
end
def replace(removed, added)
remove(removed)
add(added)
end
def cards
@orig_cards.sort
end
def shuffle!
@cards = @orig_cards.shuffle
@needs_shuffle = false
end
def draw_normal
draw = []
loop do
draw << @cards.shift
@needs_shuffle = true if ['x2', 'miss'].include? draw.last
break if draw.last !~ /r/
end
filter(draw)
end
def draw_advantage
draw = []
draw << @cards.shift
draw << @cards.shift
# If all rolling, keep drawing until no rolling
draw << @cards.shift while draw.all?{|c| c =~ /r/}
@needs_shuffle = true if draw.include?('x2') || draw.include?('miss')
# If any rolling, return whole stack
return filter(draw) if draw.any?{|c| c =~ /r/}
# If miss, use other
return filter([draw.first]) if draw.last == "miss"
return filter([draw.last]) if draw.first == "miss"
# If double, use that TODO: someknow pass a cutoff where +2 or +3 might be better on low attack
return ["x2"] if draw.include? "x2"
# If all numeric, return largest
if draw.all?{|c| c =~ /^-?\d$/}
return [draw.map(&:to_i).sort.last]
end
# TODO: +1 <thing> beats +1, <thing> vs <thing> is ambiguous
fail "Ambiguous draw: #{draw.inspect}"
end
def filter(draw)
return ['miss'] if draw.include? "miss"
draw.sort.map{|card| card == "x2" ? card : card.gsub(/^[r]/,'').to_i }
end
def end_turn
shuffle! if needs_shuffle
end
end
TRIALS = 1_000_000
def sim(caption, cards, operation)
deck = Deck.new(cards)
histogram = Hash.new{|h,k| 0}
TRIALS.times do
dmg = ATTACK
draw = operation.call(deck)
ddraw = draw.dup # Useful for debugging
if draw.include? "miss"
dmg = 0
else
while card = draw.shift do
case card
when Integer; dmg += card
when 'x2' ; dmg *= 2
else ; fail "Unknown card: #{card}"
end
end
end
histogram[dmg] += 1
deck.end_turn
end
puts "#{caption}: (#{cards.size}) #{cards.join(' ')}"
average = histogram.map{|k,v|k*v}.inject(&:+) / TRIALS.to_f
puts ">>> expected: %0.2f (+%0.2f)" % [average, average-ATTACK]
histogram.keys.sort.each do |k|
freq = (100.0 * histogram[k] / TRIALS)
puts "[%2d]: %5.2f %% %s" % [k, freq, '#' * freq]
end
puts
end
ATTACK = 3
draw_normal = ->(d){d.draw_normal}
draw_advantage = ->(d){d.draw_advantage}
draw_mixed = ->(d){rand(2).zero? ? d.draw_normal : d.draw_advantage}
default = Deck.new.cards
sim 'default', default, draw_normal
sim 'default w/ advantage', default, draw_advantage
# sim 'default 50% advantage', default, draw_mixed
sim 'two 1s', Deck.new.add(%w(1 1)).cards, draw_advantage
sim 'two 1s', Deck.new.add(%w(r1 r1)).cards, draw_advantage
# sunkeeper3a = Deck.new.add(%w(1 1)).remove(%w(-1 -1 -1 -1)).cards
# sunkeeper3b = Deck.new.add(%w(1 1)).remove(%w(-1 -1 0 0 0 0)).cards
# sunkeeper3c = Deck.new.add(%w()).remove(%w(-1 -1 -1 -1 0 0 0 0)).cards
# sunkeeper6 = Deck.new.add(%w(1 1 2)).remove(%w(-2 -1 -1 -1 -1 0 0 0 0)).cards
# sim 'SK3 A', sunkeeper3a, draw_mixed
# sim 'SK3 B', sunkeeper3b, draw_mixed
# sim 'SK3 C', sunkeeper3c, draw_mixed
# sim 'SK6', sunkeeper6, draw_mixed
ATTACK = 2
soothsinger = Deck.new.remove(%w(-1 -1 -1 -1 -2)).replace(%w(1 1 1 1), %w(4 4)).replace(%w(0 0 0 0 0 0), %w(1 1 2 2 2 3)).replace(%w(-1), %w(0)).add(%w(r1 r1 r1 r0 r0 r0 r0)).cards
soothsinger_slim = Deck.new.remove(%w(-1 -1 -1 -1 -2)).replace(%w(1 1 1 1), %w(4 4)).replace(%w(0 0 0 0 0 0), %w(1 1 2 2 2 3)).replace(%w(-1), %w(0)).cards
sim 'SSMax', soothsinger, draw_normal
sim 'SSSlim', soothsinger_slim, draw_normal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment