module AecTally
Constants
- DEFAULT_PRIZE
- PAYOUT_MULTIPLIER
- PLUGIN
- STYLE_TITLE
- STYLE_WIN
Public Instance Methods
Source
# File lib/aec_tally.rb, line 75 def clear_tips_for_seat(network:, channel:, seat:, store: RabbotDb) slug = seat_slug(seat) GameStore.list_by_key_prefix( plugin: PLUGIN, network: network, channel: channel, key_prefix: "tip:", store: store ).each do |row| next unless row[:key].end_with?(":#{slug}") GameStore.clear( plugin: PLUGIN, network: network, channel: channel, key: row[:key], store: store ) end end
Source
# File lib/aec_tally.rb, line 63 def load_tips_for_seat(network:, channel:, seat:, store: RabbotDb) slug = seat_slug(seat) GameStore.list_by_key_prefix( plugin: PLUGIN, network: network, channel: channel, key_prefix: "tip:", store: store ).select { |row| row[:key].end_with?(":#{slug}") } .map { |row| row[:value] } end
Source
# File lib/aec_tally.rb, line 148 def parse_swing(value) return nil if value.nil? text = value.to_s.strip return nil if text.empty? text.delete_prefix("+").to_f end
Source
# File lib/aec_tally.rb, line 30 def parse_tally_args(parts) tokens = Array(parts).map(&:to_s) return { error: usage_message } if tokens.empty? if tokens[0].casecmp("settle").zero? seat = tokens[1..].join(" ").strip return { error: usage_message } if seat.empty? return { action: :settle, seat: seat } end return { error: usage_message } if tokens.length < 2 swing = tokens[-1].to_f seat = tokens[0..-2].join(" ").strip return { error: usage_message } if seat.empty? { action: :predict, seat: seat, swing: swing } end
Source
# File lib/aec_tally.rb, line 96 def score_tips(tips, actual) return [] if actual.nil? || tips.empty? scored = tips.map do |tip| error = (tip["swing"].to_f - actual).abs { tip: tip, error: error } end best_error = scored.map { |row| row[:error] }.min scored.select { |row| row[:error] == best_error } end
Source
# File lib/aec_tally.rb, line 22 def seat_slug(seat) seat.to_s.strip.downcase.gsub(/\s+/, "_") end
Source
# File lib/aec_tally.rb, line 107 def settle(network:, channel:, seat:, fetch: nil, store: RabbotDb, game_store: Game.default_store) result = AecResults.fetch_seat(seat, fetch: fetch) return { error: "Could not load results for #{seat}." } unless result actual = parse_swing(result[:swing]) tips = load_tips_for_seat(network: network, channel: channel, seat: seat, store: store) lines = [ IrcFormat.report_header(tag: "AEC", title: "Tally Tip ยท #{result[:name]}", style: STYLE_TITLE) ] if actual.nil? lines << "No live swing data โ archived results only outside election night." else lines << "Actual 2PP swing: #{format('%.1f', actual)}%" if tips.empty? lines << "No tips recorded for this seat." else winners = score_tips(tips, actual) prize_each = (DEFAULT_PRIZE * PAYOUT_MULTIPLIER) / winners.length tips.each do |tip| error = (tip["swing"].to_f - actual).abs lines << "#{tip['user']} tipped #{tip['swing']}% โ off by #{format('%.1f', error)}%" end winners.each do |row| user = row[:tip]["user"] Game.credit_winnings(user, prize_each, store: game_store) balance = Game.balance_for(user, store: game_store) lines << IrcFormat.decorate( "#{user} wins #{prize_each} credits (closest) โ balance #{balance}", STYLE_WIN ) end clear_tips_for_seat(network: network, channel: channel, seat: seat, store: store) end end lines << IrcFormat.report_footer("Australian Electoral Commission") { message: lines.join("\n") } end
Source
# File lib/aec_tally.rb, line 50 def store_prediction(network:, channel:, user:, seat:, swing:, store: RabbotDb) key = tip_key(user, seat) GameStore.save( plugin: PLUGIN, network: network, channel: channel, key: key, game: { "user" => user, "seat" => seat, "swing" => swing }, store: store ) "#{user} tipped #{swing}% swing in #{seat} โ settle with !aec tally settle #{seat}" end
Source
# File lib/aec_tally.rb, line 26 def tip_key(user, seat) "tip:#{GameStore.norm_nick(user)}:#{seat_slug(seat)}" end
Source
# File lib/aec_tally.rb, line 18 def usage_message "Usage: !aec tally <seat> <swing> โ Tally Tip prediction | !aec tally settle <seat>" end