$template_hits = 4 $blast_hits = 2 $large_blast_hits = 5 class Dice def self.d6 Proc.new { rand(6) + 1 } end def self.two_d6 Proc.new { rand(6) + rand(6) + 2 } end end class Charts def self.shoot_hit?(bs, roll_provider = Dice.d6, reroll = false) if(bs < 6 || reroll) roll_provider.call > (6 - bs) || (reroll && Charts.shoot_hit([bs, 5].max, roll_provider, false)) else roll_provider.call > 1 || roll_provider.call > (11 - bs) end end def self.wound?(s, t, roll_provider = Dice.d6, reroll = false) roll = roll_provider.call d = s - t if(d >= 2) roll >=2 elsif(d == 1) roll >=3 elsif(d == 0) roll >=4 elsif(d == -1) roll >=5 elsif(d == -2 || d == -3) roll >=6 else false end end end class Special def affect?(type) false end def ignore_cover? false end end class Melta < Special def affect?(type) type == :pen end def pen(s) rand(6) + rand(6) + 2 + s end end class Template < Special def affect?(type) type == :hits end def hits(weapon, target) $template_hits end def ignore_cover? true end end class TwinLinked < Special def affect?(type) type == :hits end def hits(weapon, target) counter = 0 tohit = 6 - weapon.bs 1.upto(weapon.count) do |i| counter += 1 if (rand(6) + 1) > (tohit) || (rand(6) + 1) > (tohit) #hit if either of 2 dice pass end counter end end class Weapon attr_reader :s, :bs, :count def initialize(count, bs, s, ap, *specials) @count = count @bs = bs @s = s @ap = ap @spec = specials end def shoot(target) #1 determine hits nhits = hits(target) #2 apply to target target.take_hits(nhits, self) end def hits(target) spec_hits = special_rule(:hits) if spec_hits spec_hits.hits(self, target) else default_hits(target) end end def default_hits(target) counter = 0 1.upto(@count) do |i| counter += 1 if (rand(6) + 1) > (6 - @bs) end counter end def ignore_cover? @spec.select{|s| s.ignore_cover?} end def max_armour spec_armour = special_rule(:armour) if spec_armour spec_armour.max_armour else 14 end end def ap(wnd_roll) spec_ap = special_rule(:ap) if spec_ap spec_ap.ap(wnd_roll) else @ap end end def ap_roll spec_penroll = special_rule(:pen) if spec_penroll spec_penroll.pen(@s) else rand(6) + 1 + @s end end def ap_mod case @ap when 7 -1 when 1 +1 else 0 end end def special_rule(type) specs = @spec.select{|s| s.affect?(type)} if(specs.size > 1) raise "Weapon has conflicting special rules" else specs.first end end end class Shooting def initialize(weapons, target) @w = weapons @t = target end def shoot inst = TargetStatus.new(@t) @w.each{|w| w.shoot(inst)} inst.interpret end end class Save def initialize(armour, invulnerable = 7, cover = 7) @a = armour @i = invulnerable @c = cover end def best(ap = 7, ignore_cover = false) [ @a < ap ? @a : 7, @i, ignore_cover ? 7 : @c ].min end end class Stats attr_reader :mean, :mode def initialize(data) d = data.sort @mean = d.inject(0){|acc, n| acc+n}.to_f / d.size vcounts = d.inject({}) do |acc, v| c = acc[v] || 0 acc[v] = c + 1 acc end mcount = vcounts.values.max mvals = vcounts.select{|k, v| v == mcount}.map{|k, v| k} @mode = mvals.inject(0){|acc, n| acc+n}.to_f / mvals.size @data = [] d.each_with_index do |v, n| p = ((n.to_f - 0.5) * 100 / d.size).ceil @data << [v, p] end end def median percentile(50) end def percentile(p) if(p < @data.first.last) @data.first.first elsif(p > @data.last.last) @data.last.first else k = (p.to_f * @data.size / 100 + 0.5).floor pk = @data[k].last vk = @data[k].first vk1 = @data[k + 1].first vk.to_f + (vk1 - vk).to_f * (p - pk) * @data.size / 100 end end end class Infantry def initialize(name, count, t, sv, ld) @name = name @count = count @t = t @sv = sv @ld = ld @results = [] @broken = 0 end def initial_state 0 end def take_hits(state, nhits, weapon) 1.upto(nhits) do |hit| wnd_roll = rand(6) + 1 if(wounds?(wnd_roll, weapon.s, @t)) ap = weapon.ap(wnd_roll) sv = @sv.best(ap, weapon.ignore_cover?) if((rand(6) + 1) < sv) state.damage += 1 end end end end def wounds?(roll, s, t) d = s - t if(d >= 2) roll >=2 elsif(d == 1) roll >=3 elsif(d == 0) roll >=4 elsif(d == -1) roll >=5 elsif(d == -2 || d == -3) roll >=6 else false end end def interpret(state) v = [@count, state.damage].min @results << v break_test if v * 4 > @count end def break_test @broken += 1 if (rand(6) + rand(6) + 2) > @ld end def print_interpretation(times, weapon) puts "#{weapon.name} vs. #{@name}" st = Stats.new(@results) puts "Mean: #{st.mean} Mode: #{st.mode} Median: #{st.median} 75th: #{st.percentile(25)} 90th: #{st.percentile(10)}" puts "Broken: #{@broken.to_f/times}" @results = [] @broken = 0 end end class Vehicle def initialize(name, count, armour) @name = name @count = count @armour = armour @results = {} end def initial_state [] end def take_hits(state, nhits, weapon) 1.upto(nhits) do |hit| ap_roll = weapon.ap_roll a = [weapon.max_armour, @armour].min if(ap_roll == a) #glancing hit state.damage << (rand(6) - 1 + weapon.ap_mod) elsif(ap_roll > a) #penetrating hit state.damage << (rand(6) + 1 + weapon.ap_mod) end end end def interpret(state) if state.damage.select{|d| d == 3 || d == 4}.size >= 3 state.damage << 4.5 end v = state.damage.sort.last count_result(v) if v end def count_result(n) c = @results[n] || 0 @results[n] = c + 1 end def print_interpretation(times, weapon) puts "#{weapon.name} vs. #{@name}" any = @results.values.inject(0){|acc, n| n+acc} a = "Any #{any.to_f / times}" dam = @results.values_at(3, 4, 4.5, 5, 6, 7).inject(0){|acc, n| n ? n+acc : acc} b = "Damaged #{dam.to_f / times}" wreck = @results.values_at(4.5, 5, 6, 7).inject(0){|acc, n| n ? n+acc : acc} c = "Wrecked #{wreck.to_f / times}" puts [a, b, c].join(', ') @results = {} end end class TargetStatus attr_accessor :damage def initialize(target) @t = target @damage = target.initial_state end def take_hits(nhits, weapon) @t.take_hits(self, nhits, weapon) end def interpret @t.interpret(self) end end class WeaponSet < Array attr_reader :name def initialize(name, *weapons) super(weapons) @name = name end end times = ARGV[0] ? ARGV[0].to_i : 10 shooters = [ WeaponSet.new( "10 CSM, plasma, autocannon - Range 12 - 24", Weapon.new(7, 4, 4, 5), Weapon.new(1, 4, 7, 2), Weapon.new(2, 4, 7, 4) ), WeaponSet.new( "10 CSM, 2 meltas - Range < 6", Weapon.new(16, 4, 4, 5), Weapon.new(2, 4, 8, 1, Melta.new) ), WeaponSet.new( "6 Havocs, 2 Lascannon, 1 Krak Missile", Weapon.new(3, 4, 4, 5), Weapon.new(2, 4, 9, 2), Weapon.new(1, 4, 8, 3) ), WeaponSet.new( "6 Havocs, 3 Lascannon", Weapon.new(3, 4, 4, 5), Weapon.new(3, 4, 9, 2) ), WeaponSet.new( "8 Havocs, 2 Autocannon, 2 Krak Missile", Weapon.new(4, 4, 4, 5), Weapon.new(4, 4, 7, 4), Weapon.new(2, 4, 8, 3) ), WeaponSet.new( "Stationary Land Raider", Weapon.new(2, 4, 9, 2, TwinLinked.new), Weapon.new(3, 4, 5, 4, TwinLinked.new) ) ] targets = [ Infantry.new("10 Space Marines", 10, 4, Save.new(3, 7, 4), 9), Infantry.new("10 Imperial Guard", 10, 3, Save.new(5, 7, 4), 8), Infantry.new("5 Terminators", 5, 4, Save.new(2, 5, 4), 9), Infantry.new("Daemon Prince", 4, 5, Save.new(3, 5, 4), 13), Vehicle.new("Trukk Front", 1, 10), Vehicle.new("Rhino Front", 1, 11), Vehicle.new("Chimera Front", 1, 12), Vehicle.new("Predator Front", 1, 13), Vehicle.new("Land Raider", 1, 14), ] targets.each do |target| shooters.each do |weapons| shooting = Shooting.new(weapons, target) 1.upto(times) do |i| shooting.shoot end target.print_interpretation(times, weapons) puts end end