class Bingo
Constants
- BINGO_LETTERS
- CALL_INTERVAL_SEC
- COLUMN_RANGES
- PRIZE_MAX
- PRIZE_MIN
- PRIZE_STEP
- STYLE_COUNT
- STYLE_MUTED
- STYLE_SEP
- TITLE
- WIN_TEMPLATE
Public Class Methods
Source
# File plugins/bingo.rb, line 169 def self.active_bingo_game(channel, games: default_games) games[channel_game_key(channel)] end
Source
# File plugins/bingo.rb, line 477 def self.bingo( args_text, user:, channel: "#default", channel_users: [], bot_nick: "Rabbot", games: default_games, store: default_store, wins: default_wins, rand: ->(max) { Random.rand(0...max) }, sync: false ) parsed = parse_command(args_text.to_s) return parsed[:error] if parsed[:error] return usage_message if parsed[:action] == :help case parsed[:action] when :start if sync result = play_to_completion( user: user, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, store: store, wins: wins, rand: rand ) return result[:error] if result[:error] return result[:message] end result = start_game( user: user, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, store: store, wins: wins, rand: rand ) return result[:error] if result[:error] result[:start_message] || usage_message when :stop result = stop_game(user: user, channel: channel, games: games) return result[:error] if result[:error] result[:message] else usage_message end end
Source
# File plugins/bingo.rb, line 93 def self.build_card(rand: ->(max) { Random.rand(0...max) }) columns = COLUMN_RANGES.map { |range| sample_from_range(range, 5, rand: rand) } (0...5).map do |row| (0...5).map do |col| if row == 2 && col == 2 :free else columns[col][row] end end end end
Source
# File plugins/bingo.rb, line 400 def self.build_cards(players, rand: ->(max) { Random.rand(0...max) }) players.each_with_object({}) do |player, cards| cards[player] = build_card(rand: rand) end end
Source
# File plugins/bingo.rb, line 299 def self.build_play_schedule( channel:, games: default_games, interval: CALL_INTERVAL_SEC ) game = active_bingo_game(channel, games: games) return [] unless game && game[:stage] == :calling schedule = [] delay = interval called = game[:called].dup game[:pool].each do |number| called << number schedule << { delay: delay, message: format_call_message(number: number, called: called) } delay += interval winner = find_winner(game[:cards], called) next unless winner game[:called] = called game[:pending_finish] = { winner: winner, called: called.dup, channel: channel } schedule << { delay: delay, finish: true, channel: channel } break end schedule end
Source
# File plugins/bingo.rb, line 110 def self.card_has_bingo?(card, called) return true if card.any? { |row| row.all? { |cell| cell_marked?(cell, called) } } (0...5).each do |col| return true if card.all? { |row| cell_marked?(row[col], called) } end return true if (0...5).all? { |index| cell_marked?(card[index][index], called) } return true if (0...5).all? { |index| cell_marked?(card[index][4 - index], called) } false end
Source
# File plugins/bingo.rb, line 106 def self.cell_marked?(cell, called) cell == :free || called.include?(cell) end
Source
# File plugins/bingo.rb, line 165 def self.channel_game_key(channel) normalize_channel(channel) end
Source
# File plugins/bingo.rb, line 173 def self.clear_bingo_game(channel, games: default_games) games.delete(channel_game_key(channel)) end
Source
# File plugins/bingo.rb, line 16 def self.command_pattern /bingo(?: (.+))?$/i end
Source
# File plugins/bingo.rb, line 193 def self.decorate(text, style) IrcFormat.decorate(text, style) end
Source
# File plugins/bingo.rb, line 55 def self.default_wins @wins ||= PersistentStore.new(plugin: "bingo", key: "wins") end
Source
# File plugins/bingo.rb, line 443 def self.dispatch_command( args_text, user:, channel: "#default", channel_users: [], bot_nick: "Rabbot", games: default_games, store: default_store, wins: default_wins, rand: ->(max) { Random.rand(0...max) } ) parsed = parse_command(args_text.to_s) return { error: parsed[:error] } if parsed[:error] return { message: usage_message } if parsed[:action] == :help case parsed[:action] when :start start_game( user: user, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, store: store, wins: wins, rand: rand ) when :stop stop_game(user: user, channel: channel, games: games) else { message: usage_message } end end
Source
# File plugins/bingo.rb, line 254 def self.draw_until_winner(cards, rand: ->(max) { Random.rand(0...max) }) pool = shuffle_list((1..75).to_a, rand: rand) called = [] pool.each do |number| called << number winner = find_winner(cards, called) return { winner: winner, called: called, winning_number: number } if winner end { winner: nil, called: called, winning_number: nil } end
Source
# File plugins/bingo.rb, line 123 def self.enroll_players(channel_users:, bot_nick:) players = channel_users.map { |user| normalize_user(user.nick) }.uniq bot = normalize_user(bot_nick) players << bot unless players.include?(bot) players.sort end
Source
# File plugins/bingo.rb, line 247 def self.find_winner(cards, called) cards.each do |player, card| return player if card_has_bingo?(card, called) end nil end
Source
# File plugins/bingo.rb, line 339 def self.finish_pending_game(channel:, games: default_games, store: default_store, wins: default_wins) game = active_bingo_game(channel, games: games) pending = game && game[:pending_finish] return nil unless pending winner = pending[:winner] called = pending[:called] prize = game[:prize] players = game[:players] credit_winnings(winner, prize, store: store) record_win(winner, prize: prize, channel: channel, wins: wins) balance = balance_for(winner, store: store) clear_bingo_game(channel, games: games) { winner: winner, prize: prize, called: called, message: format_result( winner: winner, prize: prize, players: players, called: called, balance: balance ) } end
Source
# File plugins/bingo.rb, line 184 def self.format_ball_call(number) "#{letter_for_number(number)}-#{number}" end
Source
# File plugins/bingo.rb, line 222 def self.format_call_message(number:, called:) ball = format_ball_call(number) "#{STYLE_COUNT}>>> #{decorate(ball, STYLE_COUNT)} <<<#{IRC_RESET} #{format_called_bar(called)}" end
Source
# File plugins/bingo.rb, line 206 def self.format_called_bar(called) recent = called.last(15) bar = recent.map { |number| decorate(number.to_s.rjust(2), STYLE_COUNT) }.join(" ") "#{STYLE_LABEL}Called#{IRC_RESET} #{STYLE_SEP}(#{called.length})#{IRC_RESET} #{bar}" end
Source
# File plugins/bingo.rb, line 197 def self.format_label_line(label, value, value_style: STYLE_VALUE) "#{STYLE_LABEL}#{label}#{IRC_RESET} #{STYLE_SEP}—#{IRC_RESET} " \ "#{value_style}#{value}#{IRC_RESET}" end
Source
# File plugins/bingo.rb, line 202 def self.format_player_list(players) players.map { |player| "#{STYLE_NICK}#{player}#{IRC_RESET}" }.join("#{STYLE_LABEL}, #{IRC_RESET}") end
Source
# File plugins/bingo.rb, line 227 def self.format_result(winner:, prize:, players:, called:, balance:) lines = [ format_banner, format_label_line("Players", format_player_list(players)), format_label_line("Prize", "#{prize} credits", value_style: STYLE_COUNT), format_called_bar(called), "#{STYLE_WIN}>>> BINGO! #{STYLE_NICK}#{winner}#{IRC_RESET}#{STYLE_WIN} wins " \ "#{STYLE_COUNT}#{prize}#{IRC_RESET}#{STYLE_WIN} credits! <<<#{IRC_RESET}", format_label_line("Balance", "#{balance} credits", value_style: STYLE_COUNT) ] lines.join("\n") end
Source
# File plugins/bingo.rb, line 212 def self.format_start_message(players:, prize:) [ format_banner, format_label_line("Players", format_player_list(players)), format_label_line("Prize", "#{prize} credits", value_style: STYLE_COUNT), "#{STYLE_LABEL}Cards dealt#{IRC_RESET} #{STYLE_SEP}—#{IRC_RESET} " \ "#{STYLE_HINT}calling numbers...#{IRC_RESET}" ].join("\n") end
Source
# File plugins/bingo.rb, line 240 def self.format_stop_message [ format_banner, "#{STYLE_LABEL}Bingo game stopped — no prize awarded.#{IRC_RESET}" ].join("\n") end
Source
# File plugins/bingo.rb, line 177 def self.letter_for_number(number) COLUMN_RANGES.each_with_index do |range, index| return BINGO_LETTERS[index] if range.cover?(number) end nil end
Source
# File plugins/bingo.rb, line 138 def self.normalize_win_stats(raw) return WIN_TEMPLATE.dup if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?) { wins: (raw[:wins] || raw["wins"] || 0).to_i, total_prize: (raw[:total_prize] || raw["total_prize"] || 0).to_i, last_channel: raw[:last_channel] || raw["last_channel"], last_prize: raw[:last_prize] || raw["last_prize"] } end
Source
# File plugins/bingo.rb, line 64 def self.parse_command(text) parts = text.to_s.strip.split(/\s+/) return { action: :start } if parts.empty? case parts[0].downcase when "start", "new" { action: :start } when "stop" { action: :stop } when "help" { action: :help } else command_error end end
Source
# File plugins/bingo.rb, line 368 def self.play_to_completion( user:, channel:, channel_users:, bot_nick:, games: default_games, store: default_store, wins: default_wins, rand: ->(max) { Random.rand(0...max) } ) prep = prepare_game( user: user, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, rand: rand ) return prep if prep[:error] schedule = build_play_schedule(channel: channel, games: games) unless schedule.any? { |entry| entry[:finish] } clear_bingo_game(channel, games: games) return { error: "#{user}, bingo ended in a draw — try again." } end finish = finish_pending_game(channel: channel, games: games, store: store, wins: wins) return { error: "#{user}, bingo ended in a draw — try again." } unless finish finish.merge(start_message: prep[:start_message]) end
Source
# File plugins/bingo.rb, line 266 def self.prepare_game( user:, channel:, channel_users:, bot_nick:, games: default_games, rand: ->(max) { Random.rand(0...max) } ) key = channel_game_key(channel) if active_bingo_game(channel, games: games) return { error: "#{user}, a bingo game is already running here." } end players = enroll_players(channel_users: channel_users, bot_nick: bot_nick) prize = random_prize(rand: rand) cards = build_cards(players, rand: rand) pool = shuffle_list((1..75).to_a, rand: rand) games[key] = { stage: :calling, players: players, prize: prize, cards: cards, pool: pool, called: [] } { start_message: format_start_message(players: players, prize: prize), channel: channel } end
Source
# File plugins/bingo.rb, line 130 def self.prize_steps_count (PRIZE_MAX - PRIZE_MIN) / PRIZE_STEP + 1 end
Source
# File plugins/bingo.rb, line 134 def self.random_prize(rand: ->(max) { Random.rand(0...max) }) PRIZE_MIN + rand.call(prize_steps_count) * PRIZE_STEP end
Source
# File plugins/bingo.rb, line 154 def self.record_win(user, prize:, channel:, wins: default_wins) key = normalize_user(user) stats = normalize_win_stats(wins[key]) stats[:wins] += 1 stats[:total_prize] += prize stats[:last_channel] = normalize_channel(channel) stats[:last_prize] = prize wins[key] = stats stats end
Source
# File plugins/bingo.rb, line 89 def self.sample_from_range(range, count, rand: ->(max) { Random.rand(0...max) }) shuffle_list(range.to_a, rand: rand).take(count) end
Source
# File plugins/bingo.rb, line 80 def self.shuffle_list(list, rand: ->(max) { Random.rand(0...max) }) shuffled = list.dup (shuffled.length - 1).downto(1) do |index| swap_index = rand.call(index + 1) shuffled[index], shuffled[swap_index] = shuffled[swap_index], shuffled[index] end shuffled end
Source
# File plugins/bingo.rb, line 406 def self.start_game( user:, channel:, channel_users:, bot_nick:, games: default_games, store: default_store, wins: default_wins, rand: ->(max) { Random.rand(0...max) } ) prep = prepare_game( user: user, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, rand: rand ) return prep if prep[:error] schedule = build_play_schedule(channel: channel, games: games) unless schedule.any? { |entry| entry[:finish] } clear_bingo_game(channel, games: games) return { error: "#{user}, bingo ended in a draw — try again." } end prep.merge(schedule: schedule) end
Source
# File plugins/bingo.rb, line 435 def self.stop_game(user:, channel:, games: default_games) game = active_bingo_game(channel, games: games) return { error: "#{user}, no bingo game running in this channel." } unless game clear_bingo_game(channel, games: games) { message: format_stop_message } end
Source
# File plugins/bingo.rb, line 59 def self.usage_message "Usage: !bingo or !bingo new — deal cards to everyone in channel (incl. bot) and play for credits | " \ "!bingo start — same as new | !bingo stop — cancel an active game" end
Source
# File plugins/bingo.rb, line 149 def self.wins_for(user, wins: default_wins) key = normalize_user(user) normalize_win_stats(wins[key]) end
Public Instance Methods
Source
# File plugins/bingo.rb, line 534 def bingo(args_text = nil, **options) self.class.bingo(args_text, **options) end
Source
# File plugins/bingo.rb, line 538 def execute(m, args_text = nil) channel = m.channel ? m.channel.name : m.user.nick channel_users = m.channel ? m.channel.user_list : [] bot_nick = bot.nick games = self.class.default_games result = self.class.dispatch_command( args_text, user: m.user.nick, channel: channel, channel_users: channel_users, bot_nick: bot_nick, games: games, store: self.class.default_store, wins: self.class.default_wins ) if result[:error] themed_flood_safe_reply(m, result[:error]) return end if result[:start_message] && result[:schedule] themed_flood_safe_reply(m, result[:start_message]) schedule_timed_play(m, channel, result[:schedule], games: games) return end themed_flood_safe_reply(m, result[:message]) if result[:message] end
Source
# File plugins/bingo.rb, line 569 def schedule_timed_play(message, channel, schedule, games:) schedule.each do |entry| Timer(entry[:delay], shots: 1) do if entry[:finish] finish = self.class.finish_pending_game( channel: channel, games: games, store: self.class.default_store, wins: self.class.default_wins ) themed_flood_safe_reply(message, finish[:message]) if finish else next unless self.class.active_bingo_game(channel, games: games) themed_flood_safe_reply(message, entry[:message]) end end end end