#!/usr/bin/env ruby HORIZONTAL_STDEV = 24 VERTICAL_STDEV = 24 # board spec from WDF rules WIRE_WIDTH = 1.56 INNER_DIAMETER_BULL = 12.7 INNER_DIAMETER_25 = 31.8 DOUBLE_OUTER_EDGE = 170.0 TREBLE_OUTER_EDGE = 107.4 DOUBLE_INSIDE_WIDTH = 8.0 TREBLE_INSIDE_WIDTH = 8.0 SECTORS = %w[20 1 18 4 13 6 10 15 2 17 3 19 7 16 8 11 14 9 12 5].map(&:to_i) SECTOR_ANGLES = SECTORS.each_with_index.inject({}) do |m, (v, i)| m[v] = (90 - (i * 360 / SECTORS.length)) % 360 m end # distance from centre to apex of outer wire # must be ordered outwards from centre OUTER_DISTS = { bull: INNER_DIAMETER_BULL/2 + WIRE_WIDTH/2, '25': INNER_DIAMETER_25/2 + WIRE_WIDTH/2, small: TREBLE_OUTER_EDGE - WIRE_WIDTH - TREBLE_INSIDE_WIDTH - WIRE_WIDTH/2, treble: TREBLE_OUTER_EDGE - WIRE_WIDTH/2, big: DOUBLE_OUTER_EDGE - WIRE_WIDTH - DOUBLE_INSIDE_WIDTH - WIRE_WIDTH/2, double: DOUBLE_OUTER_EDGE - WIRE_WIDTH/2 } CENTRE_DISTS = OUTER_DISTS.inject([{}, nil]) { |(h, prev), (k, v)| h[k] = prev ? (v - ((v - prev) / 2)) : 0.0 [h, v] }[0] CHECKOUTS = { # FIXME better checkout shots 1 => { 50=>"BULL", 40=>"D20", 38=>"D19", 36=>"D18", 34=>"D17", 32=>"D16", 30=>"D15", 28=>"D14", 26=>"D13", 24=>"D12", 22=>"D11", 20=>"D10", 18=>"D9", 16=>"D8", 14=>"D7", 12=>"D6", 10=>"D5", 8=>"D4", 6=>"D3", 4=>"D2", 2=>"D1" }, 2 => { 110=>"T20", 107=>"T19", 104=>"T18", 101=>"T17", 100=>"T20", 98=>"T20", 97=>"T19", 96=>"T20", 95=>"T19", 94=>"T18", 93=>"T19", 92=>"T20", 91=>"T17", 90=>"T18", 89=>"T19", 88=>"T16", 87=>"T17", 86=>"T18", 85=>"T15", 84=>"T20", 83=>"T17", 82=>"T14", 81=>"T19", 80=>"T20", 79=>"T19", 78=>"T18", 77=>"T19", 76=>"T20", 75=>"T17", 74=>"T16", 73=>"T19", 72=>"T16", 71=>"T19", 70=>"T20", 69=>"T19", 68=>"T18", 67=>"T17", 66=>"T16", 65=>"T15", 64=>"T14", 63=>"T13", 62=>"T12", 61=>"T11", 60=>"20", 59=>"19", 58=>"18", 57=>"17", 56=>"16", 55=>"15", 54=>"14", 53=>"13", 52=>"20", 51=>"19", 50=>"18", 49=>"17", 48=>"16", 47=>"15", 46=>"14", 45=>"13", 44=>"12", 43=>"3", 42=>"10", 41=>"17", 40=>"D20", 39=>"7", 38=>"D19", 37=>"17", 36=>"D18", 35=>"3", 34=>"D17", 33=>"17", 32=>"D16", 31=>"7", 30=>"D15", 29=>"17", 28=>"D14", 27=>"3", 26=>"D13", 25=>"17", 24=>"D12", 23=>"7", 22=>"D11", 21=>"17", 20=>"D10", 19=>"3", 18=>"D9", 17=>"1", 16=>"D8", 15=>"7", 14=>"D7", 13=>"5", 12=>"D6", 11=>"3", 10=>"D5", 9=>"1", 8=>"D4", 7=>"3", 6=>"D3", 5=>"1", 4=>"D2", 3=>"1", 2=>"D1" }, 3 => { 170=>"T20", 167=>"T19", 164=>"T19", 161=>"T20", 160=>"T20", 158=>"T20", 157=>"T20", 156=>"T20", 155=>"T20", 154=>"T19", 153=>"T20", 152=>"T20", 151=>"T20", 150=>"T20", 149=>"T20", 148=>"T20", 147=>"T19", 146=>"T19", 145=>"T20", 144=>"T20", 143=>"T20", 142=>"T20", 141=>"T19", 140=>"T15", 139=>"T19", 138=>"T20", 137=>"T14", 136=>"T20", 135=>"BULL", 134=>"T14", 133=>"T14", 132=>"BULL", 131=>"T19", 130=>"T20", 129=>"T19", 128=>"T20", 127=>"T20", 126=>"T19", 125=>"BULL", 124=>"T20", 123=>"T19", 122=>"T18", 121=>"T20", 120=>"T20", 119=>"T19", 118=>"T20", 117=>"T20", 116=>"T19", 115=>"T19", 114=>"T20", 113=>"T19", 112=>"T20", 111=>"T19", 110=>"T19", 109=>"T20", 108=>"T20", 107=>"T19", 106=>"T20", 105=>"T20", 104=>"T19", 103=>"T19", 102=>"T20", 101=>"T19", 100=>"T20", 99=>"T19", 98=>"T20", 97=>"T19", 96=>"T20", 95=>"T19", 94=>"T18", 93=>"T19", 92=>"T20", 91=>"T17", 90=>"T20", 89=>"T19", 88=>"T20", 87=>"T17", 86=>"T18", 85=>"T15", 84=>"T20", 83=>"T17", 82=>"BULL", 81=>"T19", 80=>"T20", 79=>"T19", 78=>"T18", 77=>"T19", 76=>"T16", 75=>"T17", 74=>"T14", 73=>"T19", 72=>"T16", 71=>"T13", 70=>"T18", 69=>"T15", 68=>"T12", 67=>"T17", 66=>"T14", 65=>"BULL", 64=>"T16", 63=>"T13", 62=>"T10", 61=>"BULL", 60=>"20", 59=>"19", 58=>"18", 57=>"17", 56=>"16", 55=>"15", 54=>"14", 53=>"13", 52=>"20", 51=>"19", 50=>"18", 49=>"17", 48=>"16", 47=>"15", 46=>"14", 45=>"13", 44=>"12", 43=>"11", 42=>"10", 41=>"9", 40=>"D20", 39=>"7", 38=>"D19", 37=>"5", 36=>"D18", 35=>"3", 34=>"D17", 33=>"1", 32=>"D16", 31=>"15", 30=>"D15", 29=>"13", 28=>"D14", 27=>"11", 26=>"D13", 25=>"9", 24=>"D12", 23=>"7", 22=>"D11", 21=>"5", 20=>"D10", 19=>"3", 18=>"D9", 17=>"1", 16=>"D8", 15=>"7", 14=>"D7", 13=>"5", 12=>"D6", 11=>"3", 10=>"D5", 9=>"1", 8=>"D4", 7=>"3", 6=>"D3", 5=>"1", 4=>"D2", 3=>"1", 2=>"D1" } } def get_coordinates(angle, radius) t = angle * Math::PI / 180 x = radius * Math.cos(t) y = radius * Math.sin(t) [x, y] end def gauss theta = 2 * Math::PI * rand r = Math.sqrt(-2 * Math.log(1 - rand)) [r * Math.cos(theta), r * Math.sin(theta)] end def throw_dart(angle, radius) x, y = get_coordinates(angle, radius) x_offset, y_offset = gauss x += HORIZONTAL_STDEV * x_offset y += VERTICAL_STDEV * y_offset d_angle = Math.atan2(y, x) * 180 / Math::PI d_radius = Math.sqrt(x**2 + y**2) [d_angle, d_radius] end def get_sector(angle) sector = SECTOR_ANGLES.detect do |v, c| dist = (c - angle).abs % 360 dist = 360 - dist if dist > 180 dist < 360 / SECTORS.length / 2 #TODO hit wire end sector && sector.first end def get_ring(radius) ring = OUTER_DISTS.detect do |sym, dist| radius**2 < dist**2 end ring ? ring[0] : :out end def get_segment(angle, radius) sec = get_sector(angle) ring = get_ring(radius) case ring when :bull 'BULL' when :'25' '25' when :small, :big "#{sec}" when :double "D#{sec}" when :treble "T#{sec}" when :out 'OUT' end end def get_points(segment) case segment when 'BULL' 50 when /^T(\d+)$/ 3 * $1.to_i when /^D(\d+)$/ 2 * $1.to_i when /^(\d+)$/ $1.to_i else 0 end end def get_segment_coordinates(segment) sector, ring = case segment when 'BULL' [20, :bull] when '25' [20, :'25'] when /^T(\d+)$/ [$1.to_i, :treble] when /^D(\d+)$/ [$1.to_i, :double] when /^(\d+)$/ [$1.to_i, :big] end [SECTOR_ANGLES[sector], CENTRE_DISTS[ring]] end def next_dart(rem, in_hand) segment = CHECKOUTS[in_hand][rem] || (in_hand == 1 ? '20' : 'T20') get_segment_coordinates(segment) end def play_visit(rem) v_rem = rem darts = [] bust = false 3.times do t_angle, t_radius = next_dart(v_rem, 3 - darts.length) d_angle, d_radius = throw_dart(t_angle, t_radius) segment = get_segment(d_angle, d_radius) darts << segment d_points = get_points(segment) v_rem -= d_points if (v_rem < 0 || v_rem == 1 || (v_rem == 0 && segment != 'BULL' && segment !~ /^D/)) v_rem = rem bust = true break end break if v_rem.zero? end [v_rem, rem - v_rem, darts, bust] end def colour_points(points) str = "%3s" % points if points == 180 "\e[1;38;5;82m#{str}\e[0m" elsif points > 139 "\e[38;5;82m#{str}\e[0m" elsif points > 99 "\e[38;5;154m#{str}\e[0m" elsif points > 59 "\e[38;5;226m#{str}\e[0m" elsif points > 39 "\e[38;5;214m#{str}\e[0m" elsif points > 19 "\e[38;5;202m#{str}\e[0m" else "\e[38;5;196m#{str}\e[0m" end end def visit_darts(darts) str = darts.map { |x| '%3s' % x }.join(' ') str += ' ' * (3 - darts.length) str end def output_visit(n, rem, points = nil, darts = nil, bust = false) str = '(%2s) %3s' % [n, rem] str << ' %s' % colour_points(points) if points str << " \e[38;5;235m%s\e[0m" % visit_darts(darts) if darts str << " \e[38;5;88mBUST\e[0m" if bust str << " \e[38;5;76mWIN\e[0m" if rem.zero? puts str end def output_int_visit(n, p_rem, rem, p_points = nil, points = nil, darts = nil, bust = false) str = '(%2s) ' % n str << (p_points ? colour_points(p_points) : ' ') str << ' %3s' % p_rem str << ' %3s' % rem str << ' %s' % colour_points(points) if points str << " \e[38;5;235m%s\e[0m" % visit_darts(darts) if darts str << " \e[38;5;88mBUST\e[0m" if bust str << " \e[38;5;76mWIN\e[0m" if rem.zero? puts str end def play_match(start_points = 501, output = true) rem = start_points m_darts = [] output_visit(m_darts.length, rem) if output while rem != 0 rem, points, darts, bust = play_visit(rem) m_darts << darts output_visit(m_darts.length, rem, points, darts, bust) if output if rem.zero? && output puts req = (((m_darts.length - 1) * 3) + m_darts[-1].length) puts "darts: %s" % req puts "average: %.2f" % (start_points.to_f / req * 3) end end m_darts end def play_int_match(start_points = 501, output = true) p_rem = start_points rem = start_points m_darts = [] output_int_visit(m_darts.length, p_rem, rem) if output loop do rem, points, darts, bust = play_visit(rem) m_darts << darts output_int_visit(m_darts.length, p_rem, rem, nil, points, darts, bust) if output if rem == 0 puts puts 'Dartbot wins. :(' break end print "Enter points: " p_points = gets.to_i p_rem -= p_points output_int_visit(m_darts.length, p_rem, rem, p_points, points, darts, bust) if output puts if p_rem == 0 puts 'You win! :)' break end end m_darts end def test_average avgs = (0..10000).map do |x| m_darts = play_match(501, false) req = (((m_darts.length - 1) * 3) + m_darts[-1].length) (501.to_f / req * 3) end avg = avgs.sum / avgs.length stdev = Math.sqrt(avgs.sum { |x| (x - avg) ** 2 } / (avgs.length - 1)) puts puts avg puts stdev end play_int_match #test_average