/lib/cinch/plugins/coup_game.rb
Ruby | 1536 lines | 1220 code | 242 blank | 74 comment | 238 complexity | b76ddd0f919688663088b548b6680c7c MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- require 'cinch'
- require 'set'
- require 'yaml'
- require File.expand_path(File.dirname(__FILE__)) + '/core/action'
- require File.expand_path(File.dirname(__FILE__)) + '/core/game'
- require File.expand_path(File.dirname(__FILE__)) + '/core/turn'
- require File.expand_path(File.dirname(__FILE__)) + '/core/player'
- require File.expand_path(File.dirname(__FILE__)) + '/core/character'
- $pm_users = Set.new
- module Cinch
- class Message
- old_reply = instance_method(:reply)
- define_method(:reply) do |*args|
- if self.channel.nil? && !$pm_users.include?(self.user.nick)
- self.user.send(args[0], true)
- else
- old_reply.bind(self).(*args)
- end
- end
- end
- class User
- old_send = instance_method(:send)
- define_method(:send) do |*args|
- old_send.bind(self).(args[0], !$pm_users.include?(self.nick))
- end
- end
- end
- module Cinch
- module Plugins
- CHANGELOG_FILE = File.expand_path(File.dirname(__FILE__)) + "/changelog.yml"
- ACTION_ALIASES = {
- 'foreign aid' => 'foreign_aid',
- 'tax' => 'duke',
- 'assassinate' => 'assassin',
- 'kill' => 'assassin',
- 'steal' => 'captain',
- 'extort' => 'captain',
- 'exchange' => 'ambassador',
- 'recant' => 'apostatize',
- 'repent' => 'apostatize',
- 'betray' => 'defect',
- }
- # Length of the longest character's name (Ambassador / Inquisitor)
- LONGEST_NAME = 10
- class CoupGame
- include Cinch::Plugin
- def initialize(*args)
- super
- @changelog = self.load_changelog
- @mods = config[:mods]
- @channel_names = config[:channels]
- @settings_file = config[:settings]
- @games_dir = config[:games_dir]
- @idle_timer_length = config[:allowed_idle]
- @invite_timer_length = config[:invite_reset]
- @games = {}
- @idle_timers = {}
- @channel_names.each { |c|
- @games[c] = Game.new(c)
- @idle_timers[c] = self.start_idle_timer(c)
- }
- @user_games = {}
- @forced_id = 16
- settings = load_settings || {}
- $pm_users = settings['pm_users'] || Set.new
- end
- def self.xmatch(regex, args)
- match(regex, args.dup)
- args[:prefix] = lambda { |m| m.bot.nick + ': ' }
- match(regex, args.dup)
- args[:react_on] = :private
- args[:prefix] = /^/
- match(regex, args.dup)
- end
- # start
- xmatch /join(?:\s*(##?\w+))?/i, :method => :join
- xmatch /leave/i, :method => :leave
- xmatch /start(?:\s+(.+))?/i, :method => :start_game
-
- # game
- xmatch /(?:action )?(duke|tax|ambassador|exchange|income|foreign(?: |_)aid)/i, :method => :do_action
- xmatch /(?:action )?(recant|repent|apostatize|defect|betray|embezzle)/i, :method => :do_action
- xmatch /(?:action )?(assassin(?:ate)?|kill|captain|steal|extort|coup)(?: (.+))?/i, :method => :do_action
- xmatch /(?:action )?(inquisitor|convert|bribe)(?: (.+))?/i, :method => :do_action
- xmatch /block (duke|contessa|captain|ambassador|inquisitor)/i, :method => :do_block
- xmatch /pass/i, :method => :react_pass
- xmatch /challenge/i, :method => :react_challenge
- xmatch /bs/i, :method => :react_challenge
- xmatch /(?:flip|lose)\s*(1|2)/i, :method => :flip_card
- xmatch /(?:switch|keep|pick|swap)\s*([1-6])/i, :method => :pick_cards
- xmatch /show (1|2)/i, :method => :show_to_inquisitor
- xmatch /keep/i, :method => :inquisitor_keep
- xmatch /discard/i, :method => :inquisitor_discard
- xmatch /me$/i, :method => :whoami
- xmatch /table(?:\s*(##?\w+))?/i,:method => :show_table
- xmatch /who(?:\s*(##?\w+))?/i, :method => :list_players
- xmatch /status$/i, :method => :status
- # other
- xmatch /invite/i, :method => :invite
- xmatch /subscribe/i, :method => :subscribe
- xmatch /unsubscribe/i, :method => :unsubscribe
- xmatch /help ?(.+)?/i, :method => :help
- xmatch /intro/i, :method => :intro
- xmatch /rules ?(.+)?/i, :method => :rules
- xmatch /changelog$/i, :method => :changelog_dir
- xmatch /changelog (\d+)/i, :method => :changelog
- # xmatch /about/i, :method => :about
- xmatch /settings(?:\s+(##?\w+))?$/i, :method => :get_game_settings
- xmatch /settings(?:\s+(##?\w+))? (.+)/i, :method => :set_game_settings
-
- # mod only commands
- xmatch /reset(?:\s+(##?\w+))?/i, :method => :reset_game
- xmatch /replace (.+?) (.+)/i, :method => :replace_user
- xmatch /kick(?:\s+(##?\w+))?\s+(.+)/i, :method => :kick_user
- xmatch /room(?:\s+(##?\w+))?\s+(.+)/i, :method => :room_mode
- xmatch /chars(?:\s+(##?\w+))?/i, :method => :who_chars
- listen_to :join, :method => :voice_if_in_game
- listen_to :leaving, :method => :remove_if_not_started
- listen_to :op, :method => :devoice_everyone_on_start
- xmatch /notice(?:\s+(on|off))?/i, :method => :noticeme
- #--------------------------------------------------------------------------------
- # Listeners & Timers
- #--------------------------------------------------------------------------------
-
- def voice_if_in_game(m)
- game = @games[m.channel.name]
- m.channel.voice(m.user) if game && game.has_player?(m.user)
- end
- def remove_if_not_started(m, user)
- game = @user_games[user]
- self.remove_user_from_game(user, game) if game && game.not_started?
- end
- def devoice_everyone_on_start(m, user)
- if user == bot
- self.devoice_channel(m.channel)
- end
- end
- def start_idle_timer(channel_name)
- Timer(300) do
- game = @games[channel_name]
- game.players.map{|p| p.user }.each do |user|
- user.refresh
- if user.idle > @idle_timer_length
- self.remove_user_from_game(user, game) if game.not_started?
- user.send "You have been removed from the #{channel_name} game due to inactivity."
- end
- end
- end
- end
- #--------------------------------------------------------------------------------
- # Main IRC Interface Methods
- #--------------------------------------------------------------------------------
- def join(m, channel_name = nil)
- channel = channel_name ? Channel(channel_name) : m.channel
- unless channel
- m.reply('To join a game via PM you must specify the channel: ' +
- '!join #channel')
- return
- end
- # self.reset_timer(m)
- game = @games[channel.name]
- unless game
- m.reply(channel.name + ' is not a valid channel to join', true)
- return
- end
- if channel.has_user?(m.user)
- if (game2 = @user_games[m.user])
- m.reply("You are already in the #{game2.channel_name} game", true)
- return
- end
- if game.accepting_players?
- added = game.add_player(m.user)
- unless added.nil?
- channel.send "#{m.user.nick} has joined the game (#{game.players.count}/#{Game::MAX_PLAYERS})"
- channel.voice(m.user)
- @user_games[m.user] = game
- end
- else
- if game.started?
- m.reply('Game has already started.', true)
- elsif game.at_max_players?
- m.reply('Game is at max players.', true)
- else
- m.reply('You cannot join.', true)
- end
- end
- else
- User(m.user).send "You need to be in #{channel.name} to join the game."
- end
- end
- def leave(m)
- game = self.game_of(m)
- return unless game
- if game.accepting_players?
- self.remove_user_from_game(m.user, game)
- else
- if game.started?
- m.reply "Game is in progress.", true
- end
- end
- end
- def start_game(m, options = '')
- game = self.game_of(m)
- return unless game
- unless game.started?
- if game.at_min_players?
- if game.has_player?(m.user)
- @idle_timers[game.channel_name].stop
- if options && !options.empty?
- settings, unrecognized = self.class.parse_game_settings(options)
- unless unrecognized.empty?
- ur = unrecognized.collect { |x| '"' + x + '"' }.join(', ')
- m.reply('Unrecognized game types: ' + ur, true)
- return
- end
- if game.settings != settings
- game.settings = settings
- change_prefix = m.channel ? "The game has been changed" : "#{m.user.nick} has changed the game"
- Channel(game.channel_name).send("#{change_prefix} to #{game_settings(game)}.")
- end
- elsif game.players.size == 2
- m.reply('To start a two-player game you must choose to play with base rules ("!start base") or two-player variant rules ("!start twoplayer").')
- return
- end
- game.start_game!
- Channel(game.channel_name).send "The game has started."
- self.pass_out_characters(game)
- Channel(game.channel_name).send "Turn order is: #{game.players.map{ |p| p.user.nick }.join(' ')}"
- if game.players.size == 2 && game.settings.include?(:twoplayer)
- Channel(game.channel_name).send('This is a two-player variant game. The starting player receives only 1 coin. Both players are picking their first character.')
- game.current_turn.wait_for_initial_characters
- else
- Channel(game.channel_name).send('This is a two-player game. The starting player receives only 1 coin.') if game.players.size == 2
- Channel(game.channel_name).send "FIRST TURN. Player: #{game.current_player}. Please choose an action."
- end
- #User(@game.team_leader.user).send "You are team leader. Please choose a team of #{@game.current_team_size} to go on first mission. \"!team#{team_example(@game.current_team_size)}\""
- else
- m.reply "You are not in the game.", true
- end
- else
- m.reply "Need at least #{Game::MIN_PLAYERS} to start a game.", true
- end
- end
- end
- #--------------------------------------------------------------------------------
- # Game interaction methods
- #--------------------------------------------------------------------------------
- # For use in tests, since @game is not exposed to tests
- def coins(p)
- game = @user_games[User(p)]
- game.find_player(p).coins
- end
- # for use in tests
- def force_characters(p, c1, c2)
- game = @user_games[User(p)]
- if c1
- game.find_player(p).switch_character(Character.new(@forced_id, c1), 0)
- @forced_id += 1
- end
- if c2
- game.find_player(p).switch_character(Character.new(@forced_id, c2), 1)
- @forced_id += 1
- end
- end
- def pass_out_characters(game)
- game.players.each do |p|
- User(p.user).send "="*40
- if game.players.size == 2 && game.settings.include?(:twoplayer)
- chars = p.side_cards.each_with_index.map { |char, i|
- "#{i + 1} - (#{char.to_s})"
- }.join(' ')
- p.user.send(chars)
- p.user.send('Choose your first character card with "!pick #". The other four characters will not be used this game, and only you will know what they are.')
- else
- self.tell_characters_to(game, p, show_side: false)
- end
- end
- end
- def whoami(m)
- game = self.game_of(m)
- return unless game
- if game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- self.tell_characters_to(game, player)
- end
- end
- def character_info(c, opts = {})
- cname = c.face_down? && !opts[:show_secret] ? '########' : c.to_s
- c.face_down? ? "(#{cname})" : "[#{cname}]"
- end
- def player_info(game, player, opts)
- character_1, character_2 = player.characters
- char1_str = character_info(character_1, opts)
- char2_str = character_2 ? ' ' + character_info(character_2, opts) : ''
- coins_str = opts[:show_coins] ? " - Coins: #{player.coins}" : ""
- side_str = ''
- if opts[:show_side] && !player.side_cards.empty?
- chars = player.side_cards.collect { |c| "(#{c.to_s})" }.join(' ')
- side_str = ' - Set aside: ' + chars
- end
- faction_str = game.has_factions? ? " - #{game.factions[player.faction]}" : ''
- chars = player.characters.size == 2 ? char1_str + char2_str : 'Character not selected'
- "#{chars}#{coins_str}#{faction_str}#{side_str}"
- end
- def tell_characters_to(game, player, opts = {})
- opts = { :show_coins => true, :show_side => true, :show_secret => true }.merge(opts)
- player.user.send(player_info(game, player, opts))
- end
- def check_action(m, game, action)
- if (mf = Game::ACTIONS[action.to_sym].mode_forbidden) && game.settings.include?(mf)
- m.user.send("#{action.upcase} may not be used if the game type is #{mf.to_s.capitalize}.")
- return false
- end
- if (mrs = Game::ACTIONS[action.to_sym].mode_required) && mrs.all? { |mr| !game.settings.include?(mr) }
- modes = mrs.collect { |mr| mr.to_s.capitalize }.join(', ')
- m.user.send("#{action.upcase} may only be used if the game type is one of the following: #{modes}.")
- return false
- end
- true
- end
- def do_action(m, action, target = "")
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- if game.current_turn.waiting_for_action? && game.current_player.user == m.user
- action = ACTION_ALIASES[action.downcase] || action.downcase
- if game.current_player.coins >= 10 && action.upcase != "COUP"
- m.user.send "Since you have 10 coins, you must use COUP. !action coup <target>"
- return
- end
- game_action = Game::ACTIONS[action.to_sym]
- return unless check_action(m, game, action)
- if target.nil? || target.empty?
- target_msg = ""
- if game_action.needs_target
- m.user.send("You must specify a target for #{action.upcase}: !action #{action} <playername>")
- return
- end
- else
- target_player = game.find_player(target)
- # No self-targeting!
- if target_player == game.current_player && !game_action.self_targettable
- m.user.send("You may not target yourself with #{action.upcase}.")
- return
- end
- if target_player.nil?
- User(m.user).send "\"#{target}\" is an invalid target."
- return
- end
- target_msg = " on #{target}"
- unless game_action.can_target_friends || game.is_enemy?(game.current_player, target_player)
- us = game.factions[game.current_player.faction]
- them = game.factions[1 - game.current_player.faction]
- m.user.send("You cannot target a fellow #{us} with #{action.upcase} while the #{them} exist!")
- return
- end
- end
- cost = game_action.cost
- if game.current_player.coins < cost
- coins = game.current_player.coins
- m.user.send "You need #{cost} coins to use #{action.upcase}, but you only have #{coins} coins."
- return
- end
- Channel(game.channel_name).send "#{m.user.nick} uses #{action.upcase}#{target_msg}"
- game.current_turn.add_action(game_action, target_player)
- if game.current_turn.action.challengeable?
- game.current_turn.wait_for_action_challenge
- self.prompt_challengers(game)
- elsif game.current_turn.action.blockable?
- game.current_turn.wait_for_block
- self.prompt_blocker(game)
- else
- self.process_turn(game)
- end
- else
- User(m.user).send "You are not the current player."
- end
- end
- def prompt_challengers(game)
- turn = game.current_turn
- action = "#{dehighlight_nick(turn.active_player.user.nick)}'s #{turn.action.to_s.upcase}"
- action = "#{dehighlight_nick(turn.counteracting_player.user.nick)}'s #{turn.counteraction.to_s.upcase} blocking #{action}" if turn.counteraction
- list = game.reacting_players.collect(&:to_s).join(', ')
- Channel(game.channel_name).send("All other players (#{list}): Would you like to challenge #{action} (\"!challenge\") or not (\"!pass\")?")
- end
- def prompt_blocker(game)
- action = game.current_turn.action
- blockers = action.blockable_by.select { |c|
- game.action_usable?(Game::ACTIONS[c])
- }.collect { |c|
- "\"!block #{c.to_s.downcase}\""
- }.join(' or ')
- if action.needs_target
- prefix = game.current_turn.target_player.to_s
- else
- prefix = 'All other players'
- enemies = game.reacting_players
- if game.has_factions?
- active_faction = game.current_turn.active_player.faction
- faction_enemies = game.players.select { |p| p.faction != active_faction }
- unless faction_enemies.empty?
- enemies = faction_enemies
- prefix = "All #{game.factions[1 - active_faction]} players"
- end
- end
- prefix << " (#{enemies.collect(&:to_s).join(', ')})"
- end
- act_str = "#{dehighlight_nick(game.current_turn.active_player.user.nick)}'s #{action.action.to_s.upcase}"
- Channel(game.channel_name).send("#{prefix}: Would you like to block #{act_str} (#{blockers}) or not (\"!pass\")?")
- end
- def do_block(m, action)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- return unless turn.waiting_for_block? && game.reacting_players.include?(player)
- action.downcase!
- game_action = Game::ACTIONS[action.to_sym]
- return unless check_action(m, game, action)
- unless game.is_enemy?(player, turn.active_player)
- us = game.factions[game.current_player.faction]
- them = game.factions[1 - game.current_player.faction]
- m.user.send("You cannot block a fellow #{us}'s #{turn.action.action.upcase} while the #{them} exist!")
- return
- end
- if game_action.blocks == turn.action.action
- if turn.action.needs_target && m.user != turn.target_player.user
- m.user.send "You can only block with #{action.upcase} if you are the target."
- return
- end
- turn.add_counteraction(game_action, player)
- Channel(game.channel_name).send "#{player} uses #{action.upcase} to block #{turn.action.action.upcase}"
- self.prompt_challengers(game)
- turn.wait_for_block_challenge
- else
- User(m.user).send "#{action.upcase} does not block that #{turn.action.action.upcase}."
- end
- end
- def react_pass(m)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- return unless game.reacting_players.include?(player)
- if turn.waiting_for_challenges?
- success = turn.pass(player)
- Channel(game.channel_name).send "#{m.user.nick} passes." if success
- if game.all_reactions_in?
- if turn.waiting_for_action_challenge? && turn.action.blockable?
- # Nobody wanted to challenge the actor.
- # If action is blockable, ask for block now.
- turn.wait_for_block
- self.prompt_blocker(game)
- else
- # If action is unblockable or if nobody is challenging the blocker, proceed.
- self.process_turn(game)
- end
- end
- elsif turn.waiting_for_block?
- if turn.action.needs_target && turn.target_player == player
- # Blocker didn't want to block. Process turn.
- Channel(game.channel_name).send "#{m.user.nick} passes."
- self.process_turn(game)
- elsif !turn.action.needs_target && game.is_enemy?(player, turn.active_player)
- # This blocker didn't want to block, but maybe someone else will
- success = game.current_turn.pass(player)
- Channel(game.channel_name).send "#{m.user.nick} passes." if success
- # So we wait until all reactions are in.
- all_in = game.has_factions? ? game.all_enemy_reactions_in? : game.all_reactions_in?
- self.process_turn(game) if all_in
- end
- end
- end
- def react_challenge(m)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- return unless turn.waiting_for_challenges? && game.reacting_players.include?(player)
- defendant = turn.challengee_player
- chall_action = turn.challengee_action
- Channel(game.channel_name).send "#{m.user.nick} challenges #{defendant} on #{chall_action.to_s.upcase}!"
- # Prompt player if he has a choice
- self.prompt_challenge_defendant(defendant, chall_action) if defendant.influence == 2 && !chall_action.character_forbidden?
- if turn.waiting_for_action_challenge?
- turn.wait_for_action_challenge_reply
- turn.action_challenger = player
- elsif game.current_turn.waiting_for_block_challenge?
- turn.wait_for_block_challenge_reply
- turn.block_challenger = player
- end
- if chall_action.character_required?
- # If he doesn't have a choice, just sleep 3 seconds and make the choice for him.
- if defendant.influence == 1
- sleep(3)
- i = defendant.characters.index { |c| c.face_down? }
- self.respond_to_challenge(game, defendant, i + 1, chall_action, player)
- end
- elsif chall_action.character_forbidden?
- # sleep for suspense
- sleep(3)
- if !defendant.has_character?(chall_action.character_forbidden)
- # Do NOT have a forbidden character, so win the challenge.
- chars = defendant.characters.select { |c| c.face_down? }
- defendant_reveal_and_win(game, defendant, chars, player)
- else
- # Do have a forbidden character.
- index = defendant.character_position(chall_action.character_forbidden)
- card = "[#{chall_action.character_forbidden.to_s.upcase}]"
- Channel(game.channel_name).send("#{defendant} reveals a #{card}. #{defendant} loses the challenge!")
- defendant_reveal_and_lose(game, defendant, defendant.characters[index], chall_action)
- end
- else
- raise "how are we waiting_for_challenges if #{chall_action} not challengeable?"
- end
- end
- def defendant_reveal_and_win(game, defendant, chars, challenger)
- revealed = chars.collect { |c| "[#{c}]" }.join(' and ')
- raise "defendant reveals #{chars.size} cards?!" unless chars.size == 2 || chars.size == 1
- pronoun = chars.size == 2 ? 'both' : 'it'
- replacement = chars.size == 2 ? 'new cards' : 'a new card'
- revealed = 'a ' + revealed if chars.size == 1
- Channel(game.channel_name).send(
- "#{defendant} reveals #{revealed} and replaces #{pronoun} with #{replacement} from the Court Deck."
- )
- # Give defendant his new characters and tell him about them.
- chars.each { |c| game.replace_character_with_new(defendant, c.name) }
- self.tell_characters_to(game, defendant, show_coins: false)
- Channel(game.channel_name).send("#{challenger} loses influence for losing the challenge!")
- game.current_turn.wait_for_challenge_loser
- if challenger.influence == 2
- self.prompt_to_flip(challenger)
- else
- i = challenger.characters.index { |c| c.face_down? }
- self.lose_challenge(game, challenger, i + 1)
- end
- end
- def defendant_reveal_and_lose(game, defendant, revealed, action)
- Channel(game.channel_name).send(
- "#{defendant} loses influence over the [#{revealed}] and cannot use the #{action.action.to_s.upcase}."
- )
- revealed.flip_up
- self.check_player_status(game, defendant)
- turn = game.current_turn
- if turn.waiting_for_action_challenge_reply?
- # The action challenge succeeds, interrupting the action.
- # We don't need to ask for a block. Just finish the turn.
- turn.action_challenge_successful = true
- self.process_turn(game)
- elsif turn.waiting_for_block_challenge_reply?
- # The block challenge succeeds, interrupting the block.
- # That means the original action holds. Finish the turn.
- turn.block_challenge_successful = true
- self.process_turn(game)
- else
- raise "defendant_reveal_and_lose in #{turn.state}"
- end
- end
- def prompt_to_pick_card(target, what, cmd)
- user = User(target.user)
- raise "#{target} has no choice to #{what}" unless target.influence == 2
- character_1, character_2 = target.characters
- user.send("Choose a character to #{what}: 1 - (#{character_1}) or 2 - (#{character_2}); \"!#{cmd} 1\" or \"!#{cmd} 2\"")
- end
- def prompt_challenge_defendant(target, action)
- user = User(target.user)
- user.send("You are being challenged to show a #{action}!")
- prompt_to_pick_card(target, 'reveal', 'flip')
- end
- def prompt_to_flip(target)
- prompt_to_pick_card(target, 'turn face up', 'lose')
- end
- def flip_card(m, position)
- game = self.game_of(m)
- return unless game
- if game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- if turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :lose_influence
- self.couped(game, player, position)
- elsif turn.waiting_for_action_challenge_reply? && turn.active_player == player
- self.respond_to_challenge(game, player, position, turn.action, turn.action_challenger)
- elsif turn.waiting_for_block_challenge_reply? && turn.counteracting_player == player
- self.respond_to_challenge(game, player, position, turn.counteraction, turn.block_challenger)
- elsif turn.waiting_for_action_challenge_loser? && turn.action_challenger == player
- self.lose_challenge(game, player, position)
- elsif turn.waiting_for_block_challenge_loser? && turn.block_challenger == player
- self.lose_challenge(game, player, position)
- end
- end
- end
- # Couped, or assassinated
- def couped(game, player, position)
- character = player.flip_character_card(position.to_i)
- if character.nil?
- player.user.send "You have already flipped that card."
- return
- end
- Channel(game.channel_name).send "#{player.user} loses influence over a [#{character}]."
- self.check_player_status(game, player)
- # If I haven't started a new game, start a new turn
- self.start_new_turn(game) unless game.is_over?
- end
- def lose_challenge(game, player, position)
- pos = position.to_i
- unless pos == 1 || pos == 2
- player.user.send("#{pos} is not a valid option to reveal.")
- return
- end
- character = player.flip_character_card(pos)
- if character.nil?
- player.user.send "You have already flipped that card."
- return
- end
- Channel(game.channel_name).send "#{player.user} loses influence over a [#{character}]."
- self.check_player_status(game, player)
- turn = game.current_turn
- if turn.waiting_for_action_challenge_loser?
- # The action challenge fails. The original action holds.
- # We now need to ask for the blocker, if any.
- # In a double-kill, losing challenge may kill the blocker.
- # If he's dead, just skip to processing turn.
- if turn.action.blockable? && turn.target_player.has_influence?
- turn.wait_for_block
- self.prompt_blocker(game)
- else
- self.process_turn(game)
- end
- elsif turn.waiting_for_block_challenge_loser?
- # The block challenge fails. The block holds.
- # Finish the turn.
- self.process_turn(game)
- else
- raise "lose_challenge in #{turn.state}"
- end
- end
- def respond_to_challenge(game, player, position, action, challenger)
- pos = position.to_i
- unless pos == 1 || pos == 2
- player.user.send("#{pos} is not a valid option to reveal.")
- return
- end
- revealed = player.characters[pos - 1]
- unless revealed.face_down?
- player.user.send('You have already flipped that card.')
- return
- end
- turn = game.current_turn
- if revealed.name == action.character_required
- defendant_reveal_and_win(game, player, [revealed], challenger)
- else
- Channel(game.channel_name).send "#{player} reveals a [#{revealed}]. That's not a #{action.character_required.to_s.upcase}! #{player} loses the challenge!"
- defendant_reveal_and_lose(game, player, revealed, action)
- end
- end
- def prompt_to_switch(game, target, cards = 2)
- game.ambassador_cards = game.draw_cards(cards)
- card_names = game.ambassador_cards.collect { |c| c.to_s }.join(' and ')
- User(target.user).send "You drew #{card_names} from the Court Deck."
- fmt = "%#{LONGEST_NAME + 2}s"
- game.ambassador_options = get_switch_options(target, game.ambassador_cards)
- User(target.user).send "Choose an option for a new hand; \"!switch #\""
- game.ambassador_options.each_with_index do |option, i|
- User(target.user).send "#{i+1} - " + option.map{ |o|
- fmt % ["(#{o})"]
- }.join(" ")
- end
- end
- def switch_cards(m, game, player, choice)
- turn = game.current_turn
- return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :switch_cards
- facedown_indices = [0, 1].select { |i| player.characters[i].face_down? }
- facedowns = facedown_indices.collect { |i| player.characters[i] }
- cards_to_return = facedowns + game.ambassador_cards
- choice = choice.to_i
- if 1 <= choice && choice <= game.ambassador_options.size
- card_ids = Hash.new(0)
- new_hand = game.ambassador_options[choice - 1]
- # Remove the new hand from cards_to_return
- new_hand.each { |c|
- card_index = cards_to_return.index(c)
- cards_to_return.delete_at(card_index)
- card_ids[c.id] += 1
- }
- # Sanity check to make sure all cards are unique (no shared references)
- cards_to_return.each { |c| card_ids[c.id] += 1 }
- all_unique = card_ids.to_a.all? { |c| c[1] == 1 }
- unless all_unique
- Channel(game.channel_name).send("WARNING!!! Card IDs not unique. Game will probably be bugged. See console output.")
- puts card_ids
- end
- facedown_indices.each_with_index { |i, j|
- # If they have two facedowns, this will switch both.
- # If they have one facedown,
- # this will switch their one facedown with the card they picked
- player.switch_character(new_hand[j], i)
- }
- game.shuffle_into_deck(*cards_to_return)
- num_cards = cards_to_return.size == 1 ? 'a card' : 'two cards'
- Channel(game.channel_name).send "#{m.user.nick} shuffles #{num_cards} into the Court Deck."
- returned_names = cards_to_return.collect { |c| "(#{c})" }.join(' and ')
- m.user.send("You returned #{returned_names} to the Court Deck.")
- self.start_new_turn(game)
- else
- User(player.user).send "#{choice} is not a valid choice"
- end
- end
- def get_switch_options(target, new_cards)
- if target.influence == 2
- (target.characters + new_cards).combination(2).to_a.uniq{ |p| p || p.reverse }.shuffle
- elsif target.influence == 1
- facedown = target.characters.select { |c| c.face_down? }
- (facedown + new_cards).collect { |c| [c] }
- else
- raise "Invalid target influence #{target.influence}"
- end
- end
- def show_to_inquisitor(m, position)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- pos = position.to_i
- unless pos == 1 || pos == 2
- player.user.send("#{pos} is not a valid option to reveal.")
- return
- end
- player = game.find_player(m.user)
- revealed = player.characters[pos - 1]
- unless revealed.face_down?
- player.user.send('You have already flipped that card.')
- return
- end
- turn = game.current_turn
- return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :show_to_inquisitor
- _show_to_inquisitor(game, turn.decider, pos, turn.active_player)
- end
- def _show_to_inquisitor(game, target, position, inquisitor)
- Channel(game.channel_name).send("#{target} passes a card to #{inquisitor}.")
- Channel(game.channel_name).send("#{inquisitor}: Should #{target} be allowed to keep this card (\"!keep\") or not (\"!discard\")?")
- revealed = target.characters[position - 1]
- inquisitor.user.send("#{target} shows you a #{revealed}.")
- game.inquisitor_shown_card = revealed
- turn = game.current_turn
- turn.make_decider(inquisitor)
- turn.decision_type = :keep_or_discard
- end
- def inquisitor_keep(m)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :keep_or_discard
- Channel(game.channel_name).send("The card is returned to #{turn.target_player}.")
- self.start_new_turn(game)
- end
- def inquisitor_discard(m)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- turn = game.current_turn
- return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :keep_or_discard
- Channel(game.channel_name).send("#{turn.target_player} is forced to discard that card and replace it with another from the Court Deck.")
- game.replace_character_with_new(turn.target_player, game.inquisitor_shown_card.name)
- self.tell_characters_to(game, turn.target_player, show_coins: false)
- self.start_new_turn(game)
- end
- def pick_cards(m, choice)
- game = self.game_of(m)
- return unless game && game.started? && game.has_player?(m.user)
- player = game.find_player(m.user)
- if game.current_turn.waiting_for_initial_characters?
- self.pick_initial_card(m, game, player, choice)
- else
- self.switch_cards(m, game, player, choice)
- end
- end
- def pick_initial_card(m, game, player, choice)
- return if player.characters.size == 2
- choice = choice.to_i
- if 1 <= choice && choice <= player.side_cards.size
- player.select_side_character(choice)
- Channel(game.channel_name).send("#{player} has selected a character.")
- self.tell_characters_to(game, player, show_side: false)
- if game.all_characters_selected?
- Channel(game.channel_name).send "FIRST TURN. Player: #{game.current_player}. Please choose an action."
- game.current_turn.wait_for_action
- end
- else
- m.user.send("#{choice} is not a valid choice")
- end
- end
- def show_table(m, channel_name = nil)
- game = self.game_of(m, channel_name, ['see a game', '!table'])
- return unless game
- m.reply(table_info(game).join("\n"))
- end
- def table_info(game, opts = {})
- info = game.players.collect { |p|
- i = player_info(game, p, show_coins: true, show_side: opts[:cheating], show_secret: opts[:cheating])
- "#{dehighlight_nick(p.to_s)}: #{i}"
- }
- unless game.discard_pile.empty?
- discards = game.discard_pile.map{ |c| "[#{c}]" }.join(" ")
- info << "Discard Pile: #{discards}"
- end
- if game.has_factions?
- info << "#{game.bank_name}: #{game.bank} coin#{game.bank == 1 ? '' : 's'}"
- end
- info
- end
- def check_player_status(game, player)
- unless player.has_influence?
- Channel(game.channel_name).send "#{player} has no more influence, and is out of the game."
- game.discard_characters_for(player)
- remove_user_from_game(player.user, game, false)
- if game.is_over?
- Channel(game.channel_name).send "Game is over! #{game.winner} wins!"
- Channel(game.channel_name).send "#{game.winner} was #{player_info(game, game.winner, show_secret: true)}."
- self.start_new_game(game)
- end
- end
- end
- def process_turn(game)
- return if game.is_over?
- turn = game.current_turn
- if turn.counteracted? && !turn.block_challenge_successful
- game.pay_for_current_turn
- Channel(game.channel_name).send "#{turn.active_player}'s #{turn.action.action.upcase} was blocked by #{turn.counteracting_player} with #{turn.counteraction.action.upcase}."
- self.start_new_turn(game)
- elsif !turn.action_challenge_successful
- self_target = turn.active_player == turn.target_player
- target_msg = self_target || turn.target_player.nil? ? "" : ": #{turn.target_player}"
- effect = self_target ? turn.action.self_effect : turn.action.effect
- effect = turn.action.effect_f.call(game) if turn.action.effect_f
- Channel(game.channel_name).send "#{game.current_player} proceeds with #{turn.action.action.upcase}. #{effect}#{target_msg}."
- game.pay_for_current_turn
- game.process_current_turn
- if turn.action.needs_decision?
- turn.wait_for_decision
- if turn.action.action == :coup || turn.action.action == :assassin
- # In a double-kill situation, the target may already be out.
- # If target is already out, just move on to next turn.
- if turn.target_player.influence == 2
- self.prompt_to_flip(turn.target_player)
- elsif turn.target_player.influence == 1
- i = turn.target_player.characters.index { |c| c.face_down? }
- self.couped(game, turn.target_player, i + 1)
- else
- self.start_new_turn(game)
- end
- elsif turn.action.action == :ambassador
- self.prompt_to_switch(game, turn.active_player)
- elsif turn.action.action == :inquisitor
- if turn.target_player == turn.active_player
- self.prompt_to_switch(game, turn.active_player, 1)
- elsif turn.target_player.influence == 2
- self.prompt_to_pick_card(turn.target_player, "show to #{turn.active_player}", 'show')
- else
- i = turn.target_player.characters.index { |c| c.face_down? }
- self._show_to_inquisitor(game, turn.target_player, i + 1, turn.active_player)
- end
- end
- else
- self.start_new_turn(game)
- end
- else
- self.start_new_turn(game)
- end
- end
- def start_new_turn(game)
- game.next_turn
- Channel(game.channel_name).send "#{game.current_player}: It is your turn. Please choose an action."
- end
- def start_new_game(game)
- Channel(game.channel_name).moderated = false
- game.players.each do |p|
- Channel(game.channel_name).devoice(p.user)
- @user_games.delete(p.user)
- end
- @games[game.channel_name] = Game.new(game.channel_name)
- @idle_timers[game.channel_name].start
- end
- def list_players(m, channel_name = nil)
- game = self.game_of(m, channel_name, ['list players', '!who'])
- return unless game
- if game.players.empty?
- m.reply "No one has joined the game yet."
- else
- m.reply game.players.map{ |p| dehighlight_nick(p.to_s) }.join(' ')
- end
- end
- def devoice_channel(channel)
- channel.voiced.each do |user|
- channel.devoice(user)
- end
- end
- def remove_user_from_game(user, game, announce = true)
- left = game.remove_player(user)
- unless left.nil?
- Channel(game.channel_name).send "#{user.nick} has left the game (#{game.players.count}/#{Game::MAX_PLAYERS})" if announce
- Channel(game.channel_name).devoice(user)
- @user_games.delete(user)
- end
- end
- def dehighlight_nick(nickname)
- nickname.chars.to_a.join(8203.chr('UTF-8'))
- end
- #--------------------------------------------------------------------------------
- # Mod commands
- #--------------------------------------------------------------------------------
- def is_mod?(nick)
- # make sure that the nick is in the mod list and the user in authenticated
- user = User(nick)
- user.authed? && @mods.include?(user.authname)
- end
- def reset_game(m, channel_name)
- return unless self.is_mod? m.user.nick
- game = self.game_of(m, channel_name, ['reset a game', '!reset'])
- return unless game
- channel = Channel(game.channel_name)
- # Show everyone's cards.
- channel.send(table_info(game, cheating: true).join("\n")) if game.started?
- game.players.each do |p|
- @user_games.delete(p.user)
- end
- @games[channel.name] = Game.new(channel.name)
- self.devoice_channel(channel)
- channel.send("The game has been reset.")
- @idle_timers[channel.name].start
- end
- def kick_user(m, channel_name, nick)
- return unless self.is_mod? m.user.nick
- game = self.game_of(m, channel_name, ['kick a user', '!kick'])
- return unless game
- if game.not_started?
- user = User(nick)
- self.remove_user_from_game(user, game)
- else
- m.user.send "You can't kick someone while a game is in progress."
- end
- end
- def replace_user(m, nick1, nick2)
- if self.is_mod? m.user.nick
- # find irc users based on nick
- user1 = User(nick1)
- user2 = User(nick2)
- # Find game based on user 1
- game = @user_games[user1]
- # Can't do it if user2 is in a different game!
- if (game2 = @user_games[user2])
- m.user.send("#{nick2} is already in the #{game2.channel_name} game.")
- return
- end
- # replace the users for the players
- player = game.find_player(user1)
- player.user = user2
- # devoice/voice the players
- Channel(game.channel_name).devoice(user1)
- Channel(game.channel_name).voice(user2)
- @user_games.delete(user1)
- @user_games[user2] = game
- # inform channel
- Channel(game.channel_name).send "#{user1.nick} has been replaced with #{user2.nick}"
- # tell characters to new player
- User(player.user).send "="*40
- self.tell_characters_to(game, player)
- end
- end
- def room_mode(m, channel_name, mode)
- channel = channel_name ? Channel(channel_name) : m.channel
- if self.is_mod? m.user.nick
- case mode
- when "silent"
- Channel(channel.name).moderated = true
- when "vocal"
- Channel(channel.name).moderated = false
- end
- end
- end
- def who_chars(m, channel_name)
- return unless self.is_mod? m.user.nick
- game = self.game_of(m, channel_name, ['see a game', '!chars'])
- return unless game
- if game.started?
- if game.has_player?(m.user)
- m.user.send('Cheater!!!')
- else
- m.user.send(table_info(game, cheating: true).join("\n"))
- end
- else
- m.user.send('There is no game going on.')
- end
- end
- #--------------------------------------------------------------------------------
- # Game Settings
- #--------------------------------------------------------------------------------
- def self.parse_game_settings(options)
- unrecognized = []
- settings = []
- options.split.each { |opt|
- case opt.downcase
- when 'base'
- settings.clear
- when 'twoplayer'
- settings << :twoplayer
- when 'inquisitor', 'inquisition'
- settings << :inquisitor
- when 'reformation'
- settings << :reformation
- settings.delete(:incorporation)
- when 'incorporation'
- settings << :incorporation
- settings.delete(:reforation)
- else
- unrecognized << opt
- end
- }
- [settings.uniq, unrecognized]
- end
- def get_game_settings(m, channel_name = nil)
- game = self.game_of(m, channel_name, ['see settings', '!settings'])
- return unless game
- m.reply("Game settings: #{game_settings(game)}.")
- end
- def set_game_settings(m, channel_name = nil, options = "")
- game = self.game_of(m, channel_name, ['change settings', '!settings'])
- return unless game && !game.started?
- unless Channel(game.channel_name).has_user?(m.user)
- m.user.send("You need to be in #{game.channel_name} to change the settings.")
- return
- end
- settings, _ = self.class.parse_game_settings(options)
- game.settings = settings
- change_prefix = m.channel ? "The game has been changed" : "#{m.user.nick} has changed the game"
- Channel(game.channel_name).send("#{change_prefix} to #{game_settings(game)}.")
- end
- def game_settings(game)
- game.settings.empty? ? 'Base' : game.settings.collect { |s| s.to_s.capitalize }.join(', ')
- end
- #--------------------------------------------------------------------------------
- # Helpers
- #--------------------------------------------------------------------------------
- def game_of(m, channel_name = nil, warn_user = nil)
- # If in a channel, must be for that channel.
- return @games[m.channel.name] if m.channel
- # If in private and channel specified, show that channel.
- return game = @games[channel_name] if channel_name
- # If in private and channel not specified, show the game the player is in.
- game = @user_games[m.user]
- # and advise them if they aren't in any
- m.reply("To #{warn_user[0]} via PM you must specify the channel: #{warn_user[1]} #channel") if game.nil? && !warn_user.nil?
- game
- end
- def noticeme(m, toggle)
- if toggle && toggle.downcase == 'on'
- $pm_users.delete(m.user.nick)
- settings = load_settings || {}
- settings['pm_users'] = $pm_users
- save_settings(settings)
- elsif toggle && toggle.downcase == 'off'
- $pm_users.add(m.user.nick)
- settings = load_settings || {}
- settings['pm_users'] = $pm_users
- save_settings(settings)
- end
- m.reply("Private communications to you will occur in #{$pm_users.include?(m.user.nick) ? 'PRIVMSG' : 'NOTICE'}")
- end
- def help(m, page)
- if page.to_s.downcase == "mod" && self.is_mod?(m.user.nick)
- User(m.user).send "--- HELP PAGE MOD ---"
- User(m.user).send "!reset - completely resets the game to brand new"
- User(m.user).send "!replace nick1 nick1 - replaces a player in-game with a player out-of-game"
- User(m.user).send "!kick nick1 - removes a presumably unresponsive user from an unstarted game"
- User(m.user).send "!room silent|vocal - switches the channel from voice only users and back"
- m.user.send('!chars - the obligatory cheating command - NOT to be used while you are a participant of the game')
- else
- case page
- when "2"
- User(m.user).send "--- HELP PAGE 2/3 ---"
- m.user.send('!me - PMs you your current character cards')
- m.user.send('!table - examines the table, showing any face-up cards and how many coins each player has')
- m…
Large files files are truncated, but you can click here to view the full file