<commit_before>require 'cinch'
require 'cinch/plugins/game_bot'
require 'codenames/game'
require 'codenames/text'

module Cinch; module Plugins; class Codenames < GameBot
  include Cinch::Plugin

  match(/teams?(?:\s+(a|b|r|x))?/i, method: :team)
  match(/words(?:\s+(##?\w+))?/i, method: :words)
  match(/status/i, method: :status)

  match(/me$/i, method: :become_hinter)
  match(/rand(?:om)?$/i, method: :random_hinter)

  match(/h(?:int)?\s+(\S+)(?:\s+(\S+))?/i, method: :hint)
  match(/c(?:lue)?\s+(\S+)(?:\s+(\S+))?/i, method: :hint)

  match(/g(?:uess)?\s+(.+)/i, method: :guess)

  # We don't want !status to accidentally trigger !s, or the like.
  match(/s$|p$/i, method: :no_guess)
  match(/stop/i, method: :no_guess)
  match(/pass/i, method: :no_guess)
  match(/noguess/i, method: :no_guess)

  match(/help(?: (.+))?/i, method: :help)
  match(/rules/i, method: :rules)

  match(/peek(?:\s+(##?\w+))?/i, method: :peek)

  common_commands

  TEAMS = [
    [:bold, :blue, 'Team Blue'],
    [:bold, :red, 'Team Red'],
  ]
  NEUTRAL = 'Civilian'
  ASSASSIN = 'Assassin'

  def initialize(*args)
    super
    # Game strips out whitespace, but just to be sure!
    possible_words = config[:words_file] ? IO.readlines(config[:words_file]).map(&:chomp) : config[:words]
    ::Codenames::Game.possible_words = possible_words
  end

  #--------------------------------------------------------------------------------
  # Implementing classes should override these
  #--------------------------------------------------------------------------------

  def game_class
    ::Codenames::Game
  end

  def do_start_game(m, game, options)
    success, error = game.start_game
    unless success
      m.reply("Failed to start game because #{error}", true)
      return
    end

    chan = Channel(game.channel_name)
    chan.send("Welcome to game #{game.id}")
    teams_and_players = game.teams.map { |team|
      "#{format_team(team.id)}: #{team.users.map(&:nick).join(', ')}"
    }.join(' and ')
    chan.send("The teams for game #{game.id} are #{teams_and_players}")

    if game.current_phase == :choose_hinter
      chan.send("All teams: please have one player volunteer to be #{::Codenames::Text::ROLES[:hint]} (!me) or randomly decide (!random).")
    else
      self.send_initial_info(game)
    end
  end

  def do_reset_game(game)
    return unless game.started? && game.teams.all?(&:picked_roles?)
    Channel(game.channel_name).send(self.endgame_word_info(game))
  end

  def do_replace_user(game, replaced_user, replacing_user)
    # I don't think we need to do anything.
  end

  def bg3po_invite_command(channel_name)
    # Nah, I don't want to do bg3po
  end

  #--------------------------------------------------------------------------------
  # Game
  #--------------------------------------------------------------------------------

  def become_hinter(m)
    self.choose_hinter(m)
  end

  def random_hinter(m)
    self.choose_hinter(m, random: true)
  end

  def choose_hinter(m, random: false)
    game = self.game_of(m)
    return unless game && game.started?

    role_name = ::Codenames::Text::ROLES[:hint]

    # Actually if success == true then error is a boolean,
    # but I don't even need to rely on that.
    success, error = game.choose_hinter(m.user, random: random)
    if success
      # Is it OK to highlight the previous hinters when a new hinter is picked?
      # Well, a new hinter being picked always moves the game along, then yes.
      # We can revisit this if it turns out to be a problem later.
      m.reply(game.teams.select(&:picked_roles?).map { |team|
        hinters = team.hinters.map(&:nick)
        "#{format_team(team.id)}'s #{role_name}#{hinters.size == 1 ? ' is' : 's are'} #{hinters.join(', ')}"
      }.join(' and '))
    else
      m.reply("Failed to choose #{role_name} because #{error}", true)
    end

    # If all hinters have been chosen, move the game along.
    self.send_initial_info(game) if game.teams.all?(&:picked_roles?)
  end

  def hint(m, word, num)
    game = self.game_of(m)
    return unless game && game.started?

    success, error = game.hint(m.user, word, num)
    if success
      chan = Channel(game.channel_name)
      chan.send(self.public_word_info(game).join("\n"))
      chan.send(decision_info(game))
    else
      m.reply("Failed to give clue because #{error}", true)
    end
  end

  def guess(m, guessed)
    game = self.game_of(m)
    return unless game && game.started?

    # Save these because they could change after guess is done.
    team_name = format_team(game.current_team_id)
    other_team = game.teams[game.other_team_id]

    success, error = game.guess(m.user, guessed)

    if success
      chan = Channel(game.channel_name)
      guess_info = error

      prefix = "#{m.user} of #{team_name} contacted #{guessed.strip.capitalize}"
      case guess_info.role
      when :assassin
        chan.send("#{prefix} the #{ASSASSIN}! #{team_name} loses!")
        self.congratulate_winners(game)
      when :neutral
        chan.send("#{prefix} the #{NEUTRAL}. #{team_name}'s turn is over.")
        self.prompt_hinters(game, other_team.hinters)
      when Integer
        if guess_info.winner
          chan.send("#{prefix} the #{format_team(guess_info.role)} agent! #{format_team(guess_info.winner)} has found all their agents!")
          self.congratulate_winners(game)
        else
          what_next = guess_info.turn_ends ? "#{team_name}'s turn is over." : "#{team_name} can continue guessing."
          chan.send("#{prefix} the #{format_team(guess_info.role)} agent. #{what_next}")
          if guess_info.turn_ends
            self.prompt_hinters(game, other_team.hinters)
          else
            chan.send(decision_info(game))
          end
        end
      else
        chan.send("Unknown word type #{guess_info.role}, unclear how to proceed.")
        chan.send(decision_info(game))
      end
    else
      m.reply("Failed to guess because #{error}", true)
    end
  end

  def no_guess(m)
    game = self.game_of(m)
    return unless game

    success, error = game.no_guess(m.user)
    if success
      chan = Channel(game.channel_name)
      chan.send(decision_info(game))
    else
      m.reply("Failed to pass on guessing because #{error}", true)
    end
  end

  def team(m, arg = nil)
    game = self.game_of(m)
    return unless game

    if game.started?
      game.teams.each { |team|
        if team.picked_roles?
          msg = [:hint, :guess].map { |role|
            users = team.with_role(role)
            role_name = "#{::Codenames::Text::ROLES[role]}#{'s' if users.size != 1}"
            "#{format_team(team.id)} #{role_name}: #{users.map { |u| dehighlight_nick(u.nick) }.join(', ')}"
          }.join(', ')
        else
          msg = "#{format_team(team.id)}: #{team.users.map { |u| dehighlight_nick(u.nick) }.join(', ')}"
        end
        m.reply(msg)
      }
    elsif arg
      # Not started and arg given: change team preference.
      case arg.downcase
      when 'a'; team = 0
      when 'b'; team = 1<commit_msg>no_guess: Send hinter info<commit_after>require 'cinch'
require 'cinch/plugins/game_bot'
require 'codenames/game'
require 'codenames/text'

module Cinch; module Plugins; class Codenames < GameBot
  include Cinch::Plugin

  match(/teams?(?:\s+(a|b|r|x))?/i, method: :team)
  match(/words(?:\s+(##?\w+))?/i, method: :words)
  match(/status/i, method: :status)

  match(/me$/i, method: :become_hinter)
  match(/rand(?:om)?$/i, method: :random_hinter)

  match(/h(?:int)?\s+(\S+)(?:\s+(\S+))?/i, method: :hint)
  match(/c(?:lue)?\s+(\S+)(?:\s+(\S+))?/i, method: :hint)

  match(/g(?:uess)?\s+(.+)/i, method: :guess)

  # We don't want !status to accidentally trigger !s, or the like.
  match(/s$|p$/i, method: :no_guess)
  match(/stop/i, method: :no_guess)
  match(/pass/i, method: :no_guess)
  match(/noguess/i, method: :no_guess)

  match(/help(?: (.+))?/i, method: :help)
  match(/rules/i, method: :rules)

  match(/peek(?:\s+(##?\w+))?/i, method: :peek)

  common_commands

  TEAMS = [
    [:bold, :blue, 'Team Blue'],
    [:bold, :red, 'Team Red'],
  ]
  NEUTRAL = 'Civilian'
  ASSASSIN = 'Assassin'

  def initialize(*args)
    super
    # Game strips out whitespace, but just to be sure!
    possible_words = config[:words_file] ? IO.readlines(config[:words_file]).map(&:chomp) : config[:words]
    ::Codenames::Game.possible_words = possible_words
  end

  #--------------------------------------------------------------------------------
  # Implementing classes should override these
  #--------------------------------------------------------------------------------

  def game_class
    ::Codenames::Game
  end

  def do_start_game(m, game, options)
    success, error = game.start_game
    unless success
      m.reply("Failed to start game because #{error}", true)
      return
    end

    chan = Channel(game.channel_name)
    chan.send("Welcome to game #{game.id}")
    teams_and_players = game.teams.map { |team|
      "#{format_team(team.id)}: #{team.users.map(&:nick).join(', ')}"
    }.join(' and ')
    chan.send("The teams for game #{game.id} are #{teams_and_players}")

    if game.current_phase == :choose_hinter
      chan.send("All teams: please have one player volunteer to be #{::Codenames::Text::ROLES[:hint]} (!me) or randomly decide (!random).")
    else
      self.send_initial_info(game)
    end
  end

  def do_reset_game(game)
    return unless game.started? && game.teams.all?(&:picked_roles?)
    Channel(game.channel_name).send(self.endgame_word_info(game))
  end

  def do_replace_user(game, replaced_user, replacing_user)
    # I don't think we need to do anything.
  end

  def bg3po_invite_command(channel_name)
    # Nah, I don't want to do bg3po
  end

  #--------------------------------------------------------------------------------
  # Game
  #--------------------------------------------------------------------------------

  def become_hinter(m)
    self.choose_hinter(m)
  end

  def random_hinter(m)
    self.choose_hinter(m, random: true)
  end

  def choose_hinter(m, random: false)
    game = self.game_of(m)
    return unless game && game.started?

    role_name = ::Codenames::Text::ROLES[:hint]

    # Actually if success == true then error is a boolean,
    # but I don't even need to rely on that.
    success, error = game.choose_hinter(m.user, random: random)
    if success
      # Is it OK to highlight the previous hinters when a new hinter is picked?
      # Well, a new hinter being picked always moves the game along, then yes.
      # We can revisit this if it turns out to be a problem later.
      m.reply(game.teams.select(&:picked_roles?).map { |team|
        hinters = team.hinters.map(&:nick)
        "#{format_team(team.id)}'s #{role_name}#{hinters.size == 1 ? ' is' : 's are'} #{hinters.join(', ')}"
      }.join(' and '))
    else
      m.reply("Failed to choose #{role_name} because #{error}", true)
    end

    # If all hinters have been chosen, move the game along.
    self.send_initial_info(game) if game.teams.all?(&:picked_roles?)
  end

  def hint(m, word, num)
    game = self.game_of(m)
    return unless game && game.started?

    success, error = game.hint(m.user, word, num)
    if success
      chan = Channel(game.channel_name)
      chan.send(self.public_word_info(game).join("\n"))
      chan.send(decision_info(game))
    else
      m.reply("Failed to give clue because #{error}", true)
    end
  end

  def guess(m, guessed)
    game = self.game_of(m)
    return unless game && game.started?

    # Save these because they could change after guess is done.
    team_name = format_team(game.current_team_id)
    other_team = game.teams[game.other_team_id]

    success, error = game.guess(m.user, guessed)

    if success
      chan = Channel(game.channel_name)
      guess_info = error

      prefix = "#{m.user} of #{team_name} contacted #{guessed.strip.capitalize}"
      case guess_info.role
      when :assassin
        chan.send("#{prefix} the #{ASSASSIN}! #{team_name} loses!")
        self.congratulate_winners(game)
      when :neutral
        chan.send("#{prefix} the #{NEUTRAL}. #{team_name}'s turn is over.")
        self.prompt_hinters(game, other_team.hinters)
      when Integer
        if guess_info.winner
          chan.send("#{prefix} the #{format_team(guess_info.role)} agent! #{format_team(guess_info.winner)} has found all their agents!")
          self.congratulate_winners(game)
        else
          what_next = guess_info.turn_ends ? "#{team_name}'s turn is over." : "#{team_name} can continue guessing."
          chan.send("#{prefix} the #{format_team(guess_info.role)} agent. #{what_next}")
          if guess_info.turn_ends
            self.prompt_hinters(game, other_team.hinters)
          else
            chan.send(decision_info(game))
          end
        end
      else
        chan.send("Unknown word type #{guess_info.role}, unclear how to proceed.")
        chan.send(decision_info(game))
      end
    else
      m.reply("Failed to guess because #{error}", true)
    end
  end

  def no_guess(m)
    game = self.game_of(m)
    return unless game

    success, error = game.no_guess(m.user)
    if success
      # At this point current_team is no longer the passing team.
      # It's the team who needs to give a hint, so current_team is correct.
      self.prompt_hinters(game, game.current_team.hinters)
    else
      m.reply("Failed to pass on guessing because #{error}", true)
    end
  end

  def team(m, arg = nil)
    game = self.game_of(m)
    return unless game

    if game.started?
      game.teams.each { |team|
        if team.picked_roles?
          msg = [:hint, :guess].map { |role|
            users = team.with_role(role)
            role_name = "#{::Codenames::Text::ROLES[role]}#{'s' if users.size != 1}"
            "#{format_team(team.id)} #{role_name}: #{users.map { |u| dehighlight_nick(u.nick) }.join(', ')}"
          }.join(', ')
        else
          msg = "#{format_team(team.id)}: #{team.users.map { |u| dehighlight_nick(u.nick) }.join(', ')}"
        end
        m.reply(msg)
      }
    elsif arg
      # Not started and arg given: change team preference.
      case arg.downcase
      when 'a'; team = 0
      when 'b'; team = 1
{'fraction_non_alphanumeric': 0.18602369103525618, 'fraction_numerical': 0.0008411018434148734, 'mean_word_length': 2.5004906771344455, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7125028', 'n_tokens_mistral': 4883, 'n_tokens_neox': 4693, 'n_words': 1162}