PageRenderTime 60ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/cinch/plugins/coup_game.rb

https://github.com/caitlin/cinch-coupgame
Ruby | 1536 lines | 1220 code | 242 blank | 74 comment | 238 complexity | b76ddd0f919688663088b548b6680c7c MD5 | raw file
  1. require 'cinch'
  2. require 'set'
  3. require 'yaml'
  4. require File.expand_path(File.dirname(__FILE__)) + '/core/action'
  5. require File.expand_path(File.dirname(__FILE__)) + '/core/game'
  6. require File.expand_path(File.dirname(__FILE__)) + '/core/turn'
  7. require File.expand_path(File.dirname(__FILE__)) + '/core/player'
  8. require File.expand_path(File.dirname(__FILE__)) + '/core/character'
  9. $pm_users = Set.new
  10. module Cinch
  11. class Message
  12. old_reply = instance_method(:reply)
  13. define_method(:reply) do |*args|
  14. if self.channel.nil? && !$pm_users.include?(self.user.nick)
  15. self.user.send(args[0], true)
  16. else
  17. old_reply.bind(self).(*args)
  18. end
  19. end
  20. end
  21. class User
  22. old_send = instance_method(:send)
  23. define_method(:send) do |*args|
  24. old_send.bind(self).(args[0], !$pm_users.include?(self.nick))
  25. end
  26. end
  27. end
  28. module Cinch
  29. module Plugins
  30. CHANGELOG_FILE = File.expand_path(File.dirname(__FILE__)) + "/changelog.yml"
  31. ACTION_ALIASES = {
  32. 'foreign aid' => 'foreign_aid',
  33. 'tax' => 'duke',
  34. 'assassinate' => 'assassin',
  35. 'kill' => 'assassin',
  36. 'steal' => 'captain',
  37. 'extort' => 'captain',
  38. 'exchange' => 'ambassador',
  39. 'recant' => 'apostatize',
  40. 'repent' => 'apostatize',
  41. 'betray' => 'defect',
  42. }
  43. # Length of the longest character's name (Ambassador / Inquisitor)
  44. LONGEST_NAME = 10
  45. class CoupGame
  46. include Cinch::Plugin
  47. def initialize(*args)
  48. super
  49. @changelog = self.load_changelog
  50. @mods = config[:mods]
  51. @channel_names = config[:channels]
  52. @settings_file = config[:settings]
  53. @games_dir = config[:games_dir]
  54. @idle_timer_length = config[:allowed_idle]
  55. @invite_timer_length = config[:invite_reset]
  56. @games = {}
  57. @idle_timers = {}
  58. @channel_names.each { |c|
  59. @games[c] = Game.new(c)
  60. @idle_timers[c] = self.start_idle_timer(c)
  61. }
  62. @user_games = {}
  63. @forced_id = 16
  64. settings = load_settings || {}
  65. $pm_users = settings['pm_users'] || Set.new
  66. end
  67. def self.xmatch(regex, args)
  68. match(regex, args.dup)
  69. args[:prefix] = lambda { |m| m.bot.nick + ': ' }
  70. match(regex, args.dup)
  71. args[:react_on] = :private
  72. args[:prefix] = /^/
  73. match(regex, args.dup)
  74. end
  75. # start
  76. xmatch /join(?:\s*(##?\w+))?/i, :method => :join
  77. xmatch /leave/i, :method => :leave
  78. xmatch /start(?:\s+(.+))?/i, :method => :start_game
  79. # game
  80. xmatch /(?:action )?(duke|tax|ambassador|exchange|income|foreign(?: |_)aid)/i, :method => :do_action
  81. xmatch /(?:action )?(recant|repent|apostatize|defect|betray|embezzle)/i, :method => :do_action
  82. xmatch /(?:action )?(assassin(?:ate)?|kill|captain|steal|extort|coup)(?: (.+))?/i, :method => :do_action
  83. xmatch /(?:action )?(inquisitor|convert|bribe)(?: (.+))?/i, :method => :do_action
  84. xmatch /block (duke|contessa|captain|ambassador|inquisitor)/i, :method => :do_block
  85. xmatch /pass/i, :method => :react_pass
  86. xmatch /challenge/i, :method => :react_challenge
  87. xmatch /bs/i, :method => :react_challenge
  88. xmatch /(?:flip|lose)\s*(1|2)/i, :method => :flip_card
  89. xmatch /(?:switch|keep|pick|swap)\s*([1-6])/i, :method => :pick_cards
  90. xmatch /show (1|2)/i, :method => :show_to_inquisitor
  91. xmatch /keep/i, :method => :inquisitor_keep
  92. xmatch /discard/i, :method => :inquisitor_discard
  93. xmatch /me$/i, :method => :whoami
  94. xmatch /table(?:\s*(##?\w+))?/i,:method => :show_table
  95. xmatch /who(?:\s*(##?\w+))?/i, :method => :list_players
  96. xmatch /status$/i, :method => :status
  97. # other
  98. xmatch /invite/i, :method => :invite
  99. xmatch /subscribe/i, :method => :subscribe
  100. xmatch /unsubscribe/i, :method => :unsubscribe
  101. xmatch /help ?(.+)?/i, :method => :help
  102. xmatch /intro/i, :method => :intro
  103. xmatch /rules ?(.+)?/i, :method => :rules
  104. xmatch /changelog$/i, :method => :changelog_dir
  105. xmatch /changelog (\d+)/i, :method => :changelog
  106. # xmatch /about/i, :method => :about
  107. xmatch /settings(?:\s+(##?\w+))?$/i, :method => :get_game_settings
  108. xmatch /settings(?:\s+(##?\w+))? (.+)/i, :method => :set_game_settings
  109. # mod only commands
  110. xmatch /reset(?:\s+(##?\w+))?/i, :method => :reset_game
  111. xmatch /replace (.+?) (.+)/i, :method => :replace_user
  112. xmatch /kick(?:\s+(##?\w+))?\s+(.+)/i, :method => :kick_user
  113. xmatch /room(?:\s+(##?\w+))?\s+(.+)/i, :method => :room_mode
  114. xmatch /chars(?:\s+(##?\w+))?/i, :method => :who_chars
  115. listen_to :join, :method => :voice_if_in_game
  116. listen_to :leaving, :method => :remove_if_not_started
  117. listen_to :op, :method => :devoice_everyone_on_start
  118. xmatch /notice(?:\s+(on|off))?/i, :method => :noticeme
  119. #--------------------------------------------------------------------------------
  120. # Listeners & Timers
  121. #--------------------------------------------------------------------------------
  122. def voice_if_in_game(m)
  123. game = @games[m.channel.name]
  124. m.channel.voice(m.user) if game && game.has_player?(m.user)
  125. end
  126. def remove_if_not_started(m, user)
  127. game = @user_games[user]
  128. self.remove_user_from_game(user, game) if game && game.not_started?
  129. end
  130. def devoice_everyone_on_start(m, user)
  131. if user == bot
  132. self.devoice_channel(m.channel)
  133. end
  134. end
  135. def start_idle_timer(channel_name)
  136. Timer(300) do
  137. game = @games[channel_name]
  138. game.players.map{|p| p.user }.each do |user|
  139. user.refresh
  140. if user.idle > @idle_timer_length
  141. self.remove_user_from_game(user, game) if game.not_started?
  142. user.send "You have been removed from the #{channel_name} game due to inactivity."
  143. end
  144. end
  145. end
  146. end
  147. #--------------------------------------------------------------------------------
  148. # Main IRC Interface Methods
  149. #--------------------------------------------------------------------------------
  150. def join(m, channel_name = nil)
  151. channel = channel_name ? Channel(channel_name) : m.channel
  152. unless channel
  153. m.reply('To join a game via PM you must specify the channel: ' +
  154. '!join #channel')
  155. return
  156. end
  157. # self.reset_timer(m)
  158. game = @games[channel.name]
  159. unless game
  160. m.reply(channel.name + ' is not a valid channel to join', true)
  161. return
  162. end
  163. if channel.has_user?(m.user)
  164. if (game2 = @user_games[m.user])
  165. m.reply("You are already in the #{game2.channel_name} game", true)
  166. return
  167. end
  168. if game.accepting_players?
  169. added = game.add_player(m.user)
  170. unless added.nil?
  171. channel.send "#{m.user.nick} has joined the game (#{game.players.count}/#{Game::MAX_PLAYERS})"
  172. channel.voice(m.user)
  173. @user_games[m.user] = game
  174. end
  175. else
  176. if game.started?
  177. m.reply('Game has already started.', true)
  178. elsif game.at_max_players?
  179. m.reply('Game is at max players.', true)
  180. else
  181. m.reply('You cannot join.', true)
  182. end
  183. end
  184. else
  185. User(m.user).send "You need to be in #{channel.name} to join the game."
  186. end
  187. end
  188. def leave(m)
  189. game = self.game_of(m)
  190. return unless game
  191. if game.accepting_players?
  192. self.remove_user_from_game(m.user, game)
  193. else
  194. if game.started?
  195. m.reply "Game is in progress.", true
  196. end
  197. end
  198. end
  199. def start_game(m, options = '')
  200. game = self.game_of(m)
  201. return unless game
  202. unless game.started?
  203. if game.at_min_players?
  204. if game.has_player?(m.user)
  205. @idle_timers[game.channel_name].stop
  206. if options && !options.empty?
  207. settings, unrecognized = self.class.parse_game_settings(options)
  208. unless unrecognized.empty?
  209. ur = unrecognized.collect { |x| '"' + x + '"' }.join(', ')
  210. m.reply('Unrecognized game types: ' + ur, true)
  211. return
  212. end
  213. if game.settings != settings
  214. game.settings = settings
  215. change_prefix = m.channel ? "The game has been changed" : "#{m.user.nick} has changed the game"
  216. Channel(game.channel_name).send("#{change_prefix} to #{game_settings(game)}.")
  217. end
  218. elsif game.players.size == 2
  219. 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").')
  220. return
  221. end
  222. game.start_game!
  223. Channel(game.channel_name).send "The game has started."
  224. self.pass_out_characters(game)
  225. Channel(game.channel_name).send "Turn order is: #{game.players.map{ |p| p.user.nick }.join(' ')}"
  226. if game.players.size == 2 && game.settings.include?(:twoplayer)
  227. 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.')
  228. game.current_turn.wait_for_initial_characters
  229. else
  230. Channel(game.channel_name).send('This is a two-player game. The starting player receives only 1 coin.') if game.players.size == 2
  231. Channel(game.channel_name).send "FIRST TURN. Player: #{game.current_player}. Please choose an action."
  232. end
  233. #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)}\""
  234. else
  235. m.reply "You are not in the game.", true
  236. end
  237. else
  238. m.reply "Need at least #{Game::MIN_PLAYERS} to start a game.", true
  239. end
  240. end
  241. end
  242. #--------------------------------------------------------------------------------
  243. # Game interaction methods
  244. #--------------------------------------------------------------------------------
  245. # For use in tests, since @game is not exposed to tests
  246. def coins(p)
  247. game = @user_games[User(p)]
  248. game.find_player(p).coins
  249. end
  250. # for use in tests
  251. def force_characters(p, c1, c2)
  252. game = @user_games[User(p)]
  253. if c1
  254. game.find_player(p).switch_character(Character.new(@forced_id, c1), 0)
  255. @forced_id += 1
  256. end
  257. if c2
  258. game.find_player(p).switch_character(Character.new(@forced_id, c2), 1)
  259. @forced_id += 1
  260. end
  261. end
  262. def pass_out_characters(game)
  263. game.players.each do |p|
  264. User(p.user).send "="*40
  265. if game.players.size == 2 && game.settings.include?(:twoplayer)
  266. chars = p.side_cards.each_with_index.map { |char, i|
  267. "#{i + 1} - (#{char.to_s})"
  268. }.join(' ')
  269. p.user.send(chars)
  270. 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.')
  271. else
  272. self.tell_characters_to(game, p, show_side: false)
  273. end
  274. end
  275. end
  276. def whoami(m)
  277. game = self.game_of(m)
  278. return unless game
  279. if game.started? && game.has_player?(m.user)
  280. player = game.find_player(m.user)
  281. self.tell_characters_to(game, player)
  282. end
  283. end
  284. def character_info(c, opts = {})
  285. cname = c.face_down? && !opts[:show_secret] ? '########' : c.to_s
  286. c.face_down? ? "(#{cname})" : "[#{cname}]"
  287. end
  288. def player_info(game, player, opts)
  289. character_1, character_2 = player.characters
  290. char1_str = character_info(character_1, opts)
  291. char2_str = character_2 ? ' ' + character_info(character_2, opts) : ''
  292. coins_str = opts[:show_coins] ? " - Coins: #{player.coins}" : ""
  293. side_str = ''
  294. if opts[:show_side] && !player.side_cards.empty?
  295. chars = player.side_cards.collect { |c| "(#{c.to_s})" }.join(' ')
  296. side_str = ' - Set aside: ' + chars
  297. end
  298. faction_str = game.has_factions? ? " - #{game.factions[player.faction]}" : ''
  299. chars = player.characters.size == 2 ? char1_str + char2_str : 'Character not selected'
  300. "#{chars}#{coins_str}#{faction_str}#{side_str}"
  301. end
  302. def tell_characters_to(game, player, opts = {})
  303. opts = { :show_coins => true, :show_side => true, :show_secret => true }.merge(opts)
  304. player.user.send(player_info(game, player, opts))
  305. end
  306. def check_action(m, game, action)
  307. if (mf = Game::ACTIONS[action.to_sym].mode_forbidden) && game.settings.include?(mf)
  308. m.user.send("#{action.upcase} may not be used if the game type is #{mf.to_s.capitalize}.")
  309. return false
  310. end
  311. if (mrs = Game::ACTIONS[action.to_sym].mode_required) && mrs.all? { |mr| !game.settings.include?(mr) }
  312. modes = mrs.collect { |mr| mr.to_s.capitalize }.join(', ')
  313. m.user.send("#{action.upcase} may only be used if the game type is one of the following: #{modes}.")
  314. return false
  315. end
  316. true
  317. end
  318. def do_action(m, action, target = "")
  319. game = self.game_of(m)
  320. return unless game && game.started? && game.has_player?(m.user)
  321. if game.current_turn.waiting_for_action? && game.current_player.user == m.user
  322. action = ACTION_ALIASES[action.downcase] || action.downcase
  323. if game.current_player.coins >= 10 && action.upcase != "COUP"
  324. m.user.send "Since you have 10 coins, you must use COUP. !action coup <target>"
  325. return
  326. end
  327. game_action = Game::ACTIONS[action.to_sym]
  328. return unless check_action(m, game, action)
  329. if target.nil? || target.empty?
  330. target_msg = ""
  331. if game_action.needs_target
  332. m.user.send("You must specify a target for #{action.upcase}: !action #{action} <playername>")
  333. return
  334. end
  335. else
  336. target_player = game.find_player(target)
  337. # No self-targeting!
  338. if target_player == game.current_player && !game_action.self_targettable
  339. m.user.send("You may not target yourself with #{action.upcase}.")
  340. return
  341. end
  342. if target_player.nil?
  343. User(m.user).send "\"#{target}\" is an invalid target."
  344. return
  345. end
  346. target_msg = " on #{target}"
  347. unless game_action.can_target_friends || game.is_enemy?(game.current_player, target_player)
  348. us = game.factions[game.current_player.faction]
  349. them = game.factions[1 - game.current_player.faction]
  350. m.user.send("You cannot target a fellow #{us} with #{action.upcase} while the #{them} exist!")
  351. return
  352. end
  353. end
  354. cost = game_action.cost
  355. if game.current_player.coins < cost
  356. coins = game.current_player.coins
  357. m.user.send "You need #{cost} coins to use #{action.upcase}, but you only have #{coins} coins."
  358. return
  359. end
  360. Channel(game.channel_name).send "#{m.user.nick} uses #{action.upcase}#{target_msg}"
  361. game.current_turn.add_action(game_action, target_player)
  362. if game.current_turn.action.challengeable?
  363. game.current_turn.wait_for_action_challenge
  364. self.prompt_challengers(game)
  365. elsif game.current_turn.action.blockable?
  366. game.current_turn.wait_for_block
  367. self.prompt_blocker(game)
  368. else
  369. self.process_turn(game)
  370. end
  371. else
  372. User(m.user).send "You are not the current player."
  373. end
  374. end
  375. def prompt_challengers(game)
  376. turn = game.current_turn
  377. action = "#{dehighlight_nick(turn.active_player.user.nick)}'s #{turn.action.to_s.upcase}"
  378. action = "#{dehighlight_nick(turn.counteracting_player.user.nick)}'s #{turn.counteraction.to_s.upcase} blocking #{action}" if turn.counteraction
  379. list = game.reacting_players.collect(&:to_s).join(', ')
  380. Channel(game.channel_name).send("All other players (#{list}): Would you like to challenge #{action} (\"!challenge\") or not (\"!pass\")?")
  381. end
  382. def prompt_blocker(game)
  383. action = game.current_turn.action
  384. blockers = action.blockable_by.select { |c|
  385. game.action_usable?(Game::ACTIONS[c])
  386. }.collect { |c|
  387. "\"!block #{c.to_s.downcase}\""
  388. }.join(' or ')
  389. if action.needs_target
  390. prefix = game.current_turn.target_player.to_s
  391. else
  392. prefix = 'All other players'
  393. enemies = game.reacting_players
  394. if game.has_factions?
  395. active_faction = game.current_turn.active_player.faction
  396. faction_enemies = game.players.select { |p| p.faction != active_faction }
  397. unless faction_enemies.empty?
  398. enemies = faction_enemies
  399. prefix = "All #{game.factions[1 - active_faction]} players"
  400. end
  401. end
  402. prefix << " (#{enemies.collect(&:to_s).join(', ')})"
  403. end
  404. act_str = "#{dehighlight_nick(game.current_turn.active_player.user.nick)}'s #{action.action.to_s.upcase}"
  405. Channel(game.channel_name).send("#{prefix}: Would you like to block #{act_str} (#{blockers}) or not (\"!pass\")?")
  406. end
  407. def do_block(m, action)
  408. game = self.game_of(m)
  409. return unless game && game.started? && game.has_player?(m.user)
  410. player = game.find_player(m.user)
  411. turn = game.current_turn
  412. return unless turn.waiting_for_block? && game.reacting_players.include?(player)
  413. action.downcase!
  414. game_action = Game::ACTIONS[action.to_sym]
  415. return unless check_action(m, game, action)
  416. unless game.is_enemy?(player, turn.active_player)
  417. us = game.factions[game.current_player.faction]
  418. them = game.factions[1 - game.current_player.faction]
  419. m.user.send("You cannot block a fellow #{us}'s #{turn.action.action.upcase} while the #{them} exist!")
  420. return
  421. end
  422. if game_action.blocks == turn.action.action
  423. if turn.action.needs_target && m.user != turn.target_player.user
  424. m.user.send "You can only block with #{action.upcase} if you are the target."
  425. return
  426. end
  427. turn.add_counteraction(game_action, player)
  428. Channel(game.channel_name).send "#{player} uses #{action.upcase} to block #{turn.action.action.upcase}"
  429. self.prompt_challengers(game)
  430. turn.wait_for_block_challenge
  431. else
  432. User(m.user).send "#{action.upcase} does not block that #{turn.action.action.upcase}."
  433. end
  434. end
  435. def react_pass(m)
  436. game = self.game_of(m)
  437. return unless game && game.started? && game.has_player?(m.user)
  438. player = game.find_player(m.user)
  439. turn = game.current_turn
  440. return unless game.reacting_players.include?(player)
  441. if turn.waiting_for_challenges?
  442. success = turn.pass(player)
  443. Channel(game.channel_name).send "#{m.user.nick} passes." if success
  444. if game.all_reactions_in?
  445. if turn.waiting_for_action_challenge? && turn.action.blockable?
  446. # Nobody wanted to challenge the actor.
  447. # If action is blockable, ask for block now.
  448. turn.wait_for_block
  449. self.prompt_blocker(game)
  450. else
  451. # If action is unblockable or if nobody is challenging the blocker, proceed.
  452. self.process_turn(game)
  453. end
  454. end
  455. elsif turn.waiting_for_block?
  456. if turn.action.needs_target && turn.target_player == player
  457. # Blocker didn't want to block. Process turn.
  458. Channel(game.channel_name).send "#{m.user.nick} passes."
  459. self.process_turn(game)
  460. elsif !turn.action.needs_target && game.is_enemy?(player, turn.active_player)
  461. # This blocker didn't want to block, but maybe someone else will
  462. success = game.current_turn.pass(player)
  463. Channel(game.channel_name).send "#{m.user.nick} passes." if success
  464. # So we wait until all reactions are in.
  465. all_in = game.has_factions? ? game.all_enemy_reactions_in? : game.all_reactions_in?
  466. self.process_turn(game) if all_in
  467. end
  468. end
  469. end
  470. def react_challenge(m)
  471. game = self.game_of(m)
  472. return unless game && game.started? && game.has_player?(m.user)
  473. player = game.find_player(m.user)
  474. turn = game.current_turn
  475. return unless turn.waiting_for_challenges? && game.reacting_players.include?(player)
  476. defendant = turn.challengee_player
  477. chall_action = turn.challengee_action
  478. Channel(game.channel_name).send "#{m.user.nick} challenges #{defendant} on #{chall_action.to_s.upcase}!"
  479. # Prompt player if he has a choice
  480. self.prompt_challenge_defendant(defendant, chall_action) if defendant.influence == 2 && !chall_action.character_forbidden?
  481. if turn.waiting_for_action_challenge?
  482. turn.wait_for_action_challenge_reply
  483. turn.action_challenger = player
  484. elsif game.current_turn.waiting_for_block_challenge?
  485. turn.wait_for_block_challenge_reply
  486. turn.block_challenger = player
  487. end
  488. if chall_action.character_required?
  489. # If he doesn't have a choice, just sleep 3 seconds and make the choice for him.
  490. if defendant.influence == 1
  491. sleep(3)
  492. i = defendant.characters.index { |c| c.face_down? }
  493. self.respond_to_challenge(game, defendant, i + 1, chall_action, player)
  494. end
  495. elsif chall_action.character_forbidden?
  496. # sleep for suspense
  497. sleep(3)
  498. if !defendant.has_character?(chall_action.character_forbidden)
  499. # Do NOT have a forbidden character, so win the challenge.
  500. chars = defendant.characters.select { |c| c.face_down? }
  501. defendant_reveal_and_win(game, defendant, chars, player)
  502. else
  503. # Do have a forbidden character.
  504. index = defendant.character_position(chall_action.character_forbidden)
  505. card = "[#{chall_action.character_forbidden.to_s.upcase}]"
  506. Channel(game.channel_name).send("#{defendant} reveals a #{card}. #{defendant} loses the challenge!")
  507. defendant_reveal_and_lose(game, defendant, defendant.characters[index], chall_action)
  508. end
  509. else
  510. raise "how are we waiting_for_challenges if #{chall_action} not challengeable?"
  511. end
  512. end
  513. def defendant_reveal_and_win(game, defendant, chars, challenger)
  514. revealed = chars.collect { |c| "[#{c}]" }.join(' and ')
  515. raise "defendant reveals #{chars.size} cards?!" unless chars.size == 2 || chars.size == 1
  516. pronoun = chars.size == 2 ? 'both' : 'it'
  517. replacement = chars.size == 2 ? 'new cards' : 'a new card'
  518. revealed = 'a ' + revealed if chars.size == 1
  519. Channel(game.channel_name).send(
  520. "#{defendant} reveals #{revealed} and replaces #{pronoun} with #{replacement} from the Court Deck."
  521. )
  522. # Give defendant his new characters and tell him about them.
  523. chars.each { |c| game.replace_character_with_new(defendant, c.name) }
  524. self.tell_characters_to(game, defendant, show_coins: false)
  525. Channel(game.channel_name).send("#{challenger} loses influence for losing the challenge!")
  526. game.current_turn.wait_for_challenge_loser
  527. if challenger.influence == 2
  528. self.prompt_to_flip(challenger)
  529. else
  530. i = challenger.characters.index { |c| c.face_down? }
  531. self.lose_challenge(game, challenger, i + 1)
  532. end
  533. end
  534. def defendant_reveal_and_lose(game, defendant, revealed, action)
  535. Channel(game.channel_name).send(
  536. "#{defendant} loses influence over the [#{revealed}] and cannot use the #{action.action.to_s.upcase}."
  537. )
  538. revealed.flip_up
  539. self.check_player_status(game, defendant)
  540. turn = game.current_turn
  541. if turn.waiting_for_action_challenge_reply?
  542. # The action challenge succeeds, interrupting the action.
  543. # We don't need to ask for a block. Just finish the turn.
  544. turn.action_challenge_successful = true
  545. self.process_turn(game)
  546. elsif turn.waiting_for_block_challenge_reply?
  547. # The block challenge succeeds, interrupting the block.
  548. # That means the original action holds. Finish the turn.
  549. turn.block_challenge_successful = true
  550. self.process_turn(game)
  551. else
  552. raise "defendant_reveal_and_lose in #{turn.state}"
  553. end
  554. end
  555. def prompt_to_pick_card(target, what, cmd)
  556. user = User(target.user)
  557. raise "#{target} has no choice to #{what}" unless target.influence == 2
  558. character_1, character_2 = target.characters
  559. user.send("Choose a character to #{what}: 1 - (#{character_1}) or 2 - (#{character_2}); \"!#{cmd} 1\" or \"!#{cmd} 2\"")
  560. end
  561. def prompt_challenge_defendant(target, action)
  562. user = User(target.user)
  563. user.send("You are being challenged to show a #{action}!")
  564. prompt_to_pick_card(target, 'reveal', 'flip')
  565. end
  566. def prompt_to_flip(target)
  567. prompt_to_pick_card(target, 'turn face up', 'lose')
  568. end
  569. def flip_card(m, position)
  570. game = self.game_of(m)
  571. return unless game
  572. if game.started? && game.has_player?(m.user)
  573. player = game.find_player(m.user)
  574. turn = game.current_turn
  575. if turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :lose_influence
  576. self.couped(game, player, position)
  577. elsif turn.waiting_for_action_challenge_reply? && turn.active_player == player
  578. self.respond_to_challenge(game, player, position, turn.action, turn.action_challenger)
  579. elsif turn.waiting_for_block_challenge_reply? && turn.counteracting_player == player
  580. self.respond_to_challenge(game, player, position, turn.counteraction, turn.block_challenger)
  581. elsif turn.waiting_for_action_challenge_loser? && turn.action_challenger == player
  582. self.lose_challenge(game, player, position)
  583. elsif turn.waiting_for_block_challenge_loser? && turn.block_challenger == player
  584. self.lose_challenge(game, player, position)
  585. end
  586. end
  587. end
  588. # Couped, or assassinated
  589. def couped(game, player, position)
  590. character = player.flip_character_card(position.to_i)
  591. if character.nil?
  592. player.user.send "You have already flipped that card."
  593. return
  594. end
  595. Channel(game.channel_name).send "#{player.user} loses influence over a [#{character}]."
  596. self.check_player_status(game, player)
  597. # If I haven't started a new game, start a new turn
  598. self.start_new_turn(game) unless game.is_over?
  599. end
  600. def lose_challenge(game, player, position)
  601. pos = position.to_i
  602. unless pos == 1 || pos == 2
  603. player.user.send("#{pos} is not a valid option to reveal.")
  604. return
  605. end
  606. character = player.flip_character_card(pos)
  607. if character.nil?
  608. player.user.send "You have already flipped that card."
  609. return
  610. end
  611. Channel(game.channel_name).send "#{player.user} loses influence over a [#{character}]."
  612. self.check_player_status(game, player)
  613. turn = game.current_turn
  614. if turn.waiting_for_action_challenge_loser?
  615. # The action challenge fails. The original action holds.
  616. # We now need to ask for the blocker, if any.
  617. # In a double-kill, losing challenge may kill the blocker.
  618. # If he's dead, just skip to processing turn.
  619. if turn.action.blockable? && turn.target_player.has_influence?
  620. turn.wait_for_block
  621. self.prompt_blocker(game)
  622. else
  623. self.process_turn(game)
  624. end
  625. elsif turn.waiting_for_block_challenge_loser?
  626. # The block challenge fails. The block holds.
  627. # Finish the turn.
  628. self.process_turn(game)
  629. else
  630. raise "lose_challenge in #{turn.state}"
  631. end
  632. end
  633. def respond_to_challenge(game, player, position, action, challenger)
  634. pos = position.to_i
  635. unless pos == 1 || pos == 2
  636. player.user.send("#{pos} is not a valid option to reveal.")
  637. return
  638. end
  639. revealed = player.characters[pos - 1]
  640. unless revealed.face_down?
  641. player.user.send('You have already flipped that card.')
  642. return
  643. end
  644. turn = game.current_turn
  645. if revealed.name == action.character_required
  646. defendant_reveal_and_win(game, player, [revealed], challenger)
  647. else
  648. Channel(game.channel_name).send "#{player} reveals a [#{revealed}]. That's not a #{action.character_required.to_s.upcase}! #{player} loses the challenge!"
  649. defendant_reveal_and_lose(game, player, revealed, action)
  650. end
  651. end
  652. def prompt_to_switch(game, target, cards = 2)
  653. game.ambassador_cards = game.draw_cards(cards)
  654. card_names = game.ambassador_cards.collect { |c| c.to_s }.join(' and ')
  655. User(target.user).send "You drew #{card_names} from the Court Deck."
  656. fmt = "%#{LONGEST_NAME + 2}s"
  657. game.ambassador_options = get_switch_options(target, game.ambassador_cards)
  658. User(target.user).send "Choose an option for a new hand; \"!switch #\""
  659. game.ambassador_options.each_with_index do |option, i|
  660. User(target.user).send "#{i+1} - " + option.map{ |o|
  661. fmt % ["(#{o})"]
  662. }.join(" ")
  663. end
  664. end
  665. def switch_cards(m, game, player, choice)
  666. turn = game.current_turn
  667. return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :switch_cards
  668. facedown_indices = [0, 1].select { |i| player.characters[i].face_down? }
  669. facedowns = facedown_indices.collect { |i| player.characters[i] }
  670. cards_to_return = facedowns + game.ambassador_cards
  671. choice = choice.to_i
  672. if 1 <= choice && choice <= game.ambassador_options.size
  673. card_ids = Hash.new(0)
  674. new_hand = game.ambassador_options[choice - 1]
  675. # Remove the new hand from cards_to_return
  676. new_hand.each { |c|
  677. card_index = cards_to_return.index(c)
  678. cards_to_return.delete_at(card_index)
  679. card_ids[c.id] += 1
  680. }
  681. # Sanity check to make sure all cards are unique (no shared references)
  682. cards_to_return.each { |c| card_ids[c.id] += 1 }
  683. all_unique = card_ids.to_a.all? { |c| c[1] == 1 }
  684. unless all_unique
  685. Channel(game.channel_name).send("WARNING!!! Card IDs not unique. Game will probably be bugged. See console output.")
  686. puts card_ids
  687. end
  688. facedown_indices.each_with_index { |i, j|
  689. # If they have two facedowns, this will switch both.
  690. # If they have one facedown,
  691. # this will switch their one facedown with the card they picked
  692. player.switch_character(new_hand[j], i)
  693. }
  694. game.shuffle_into_deck(*cards_to_return)
  695. num_cards = cards_to_return.size == 1 ? 'a card' : 'two cards'
  696. Channel(game.channel_name).send "#{m.user.nick} shuffles #{num_cards} into the Court Deck."
  697. returned_names = cards_to_return.collect { |c| "(#{c})" }.join(' and ')
  698. m.user.send("You returned #{returned_names} to the Court Deck.")
  699. self.start_new_turn(game)
  700. else
  701. User(player.user).send "#{choice} is not a valid choice"
  702. end
  703. end
  704. def get_switch_options(target, new_cards)
  705. if target.influence == 2
  706. (target.characters + new_cards).combination(2).to_a.uniq{ |p| p || p.reverse }.shuffle
  707. elsif target.influence == 1
  708. facedown = target.characters.select { |c| c.face_down? }
  709. (facedown + new_cards).collect { |c| [c] }
  710. else
  711. raise "Invalid target influence #{target.influence}"
  712. end
  713. end
  714. def show_to_inquisitor(m, position)
  715. game = self.game_of(m)
  716. return unless game && game.started? && game.has_player?(m.user)
  717. pos = position.to_i
  718. unless pos == 1 || pos == 2
  719. player.user.send("#{pos} is not a valid option to reveal.")
  720. return
  721. end
  722. player = game.find_player(m.user)
  723. revealed = player.characters[pos - 1]
  724. unless revealed.face_down?
  725. player.user.send('You have already flipped that card.')
  726. return
  727. end
  728. turn = game.current_turn
  729. return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :show_to_inquisitor
  730. _show_to_inquisitor(game, turn.decider, pos, turn.active_player)
  731. end
  732. def _show_to_inquisitor(game, target, position, inquisitor)
  733. Channel(game.channel_name).send("#{target} passes a card to #{inquisitor}.")
  734. Channel(game.channel_name).send("#{inquisitor}: Should #{target} be allowed to keep this card (\"!keep\") or not (\"!discard\")?")
  735. revealed = target.characters[position - 1]
  736. inquisitor.user.send("#{target} shows you a #{revealed}.")
  737. game.inquisitor_shown_card = revealed
  738. turn = game.current_turn
  739. turn.make_decider(inquisitor)
  740. turn.decision_type = :keep_or_discard
  741. end
  742. def inquisitor_keep(m)
  743. game = self.game_of(m)
  744. return unless game && game.started? && game.has_player?(m.user)
  745. player = game.find_player(m.user)
  746. turn = game.current_turn
  747. return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :keep_or_discard
  748. Channel(game.channel_name).send("The card is returned to #{turn.target_player}.")
  749. self.start_new_turn(game)
  750. end
  751. def inquisitor_discard(m)
  752. game = self.game_of(m)
  753. return unless game && game.started? && game.has_player?(m.user)
  754. player = game.find_player(m.user)
  755. turn = game.current_turn
  756. return unless turn.waiting_for_decision? && turn.decider == player && turn.decision_type == :keep_or_discard
  757. Channel(game.channel_name).send("#{turn.target_player} is forced to discard that card and replace it with another from the Court Deck.")
  758. game.replace_character_with_new(turn.target_player, game.inquisitor_shown_card.name)
  759. self.tell_characters_to(game, turn.target_player, show_coins: false)
  760. self.start_new_turn(game)
  761. end
  762. def pick_cards(m, choice)
  763. game = self.game_of(m)
  764. return unless game && game.started? && game.has_player?(m.user)
  765. player = game.find_player(m.user)
  766. if game.current_turn.waiting_for_initial_characters?
  767. self.pick_initial_card(m, game, player, choice)
  768. else
  769. self.switch_cards(m, game, player, choice)
  770. end
  771. end
  772. def pick_initial_card(m, game, player, choice)
  773. return if player.characters.size == 2
  774. choice = choice.to_i
  775. if 1 <= choice && choice <= player.side_cards.size
  776. player.select_side_character(choice)
  777. Channel(game.channel_name).send("#{player} has selected a character.")
  778. self.tell_characters_to(game, player, show_side: false)
  779. if game.all_characters_selected?
  780. Channel(game.channel_name).send "FIRST TURN. Player: #{game.current_player}. Please choose an action."
  781. game.current_turn.wait_for_action
  782. end
  783. else
  784. m.user.send("#{choice} is not a valid choice")
  785. end
  786. end
  787. def show_table(m, channel_name = nil)
  788. game = self.game_of(m, channel_name, ['see a game', '!table'])
  789. return unless game
  790. m.reply(table_info(game).join("\n"))
  791. end
  792. def table_info(game, opts = {})
  793. info = game.players.collect { |p|
  794. i = player_info(game, p, show_coins: true, show_side: opts[:cheating], show_secret: opts[:cheating])
  795. "#{dehighlight_nick(p.to_s)}: #{i}"
  796. }
  797. unless game.discard_pile.empty?
  798. discards = game.discard_pile.map{ |c| "[#{c}]" }.join(" ")
  799. info << "Discard Pile: #{discards}"
  800. end
  801. if game.has_factions?
  802. info << "#{game.bank_name}: #{game.bank} coin#{game.bank == 1 ? '' : 's'}"
  803. end
  804. info
  805. end
  806. def check_player_status(game, player)
  807. unless player.has_influence?
  808. Channel(game.channel_name).send "#{player} has no more influence, and is out of the game."
  809. game.discard_characters_for(player)
  810. remove_user_from_game(player.user, game, false)
  811. if game.is_over?
  812. Channel(game.channel_name).send "Game is over! #{game.winner} wins!"
  813. Channel(game.channel_name).send "#{game.winner} was #{player_info(game, game.winner, show_secret: true)}."
  814. self.start_new_game(game)
  815. end
  816. end
  817. end
  818. def process_turn(game)
  819. return if game.is_over?
  820. turn = game.current_turn
  821. if turn.counteracted? && !turn.block_challenge_successful
  822. game.pay_for_current_turn
  823. Channel(game.channel_name).send "#{turn.active_player}'s #{turn.action.action.upcase} was blocked by #{turn.counteracting_player} with #{turn.counteraction.action.upcase}."
  824. self.start_new_turn(game)
  825. elsif !turn.action_challenge_successful
  826. self_target = turn.active_player == turn.target_player
  827. target_msg = self_target || turn.target_player.nil? ? "" : ": #{turn.target_player}"
  828. effect = self_target ? turn.action.self_effect : turn.action.effect
  829. effect = turn.action.effect_f.call(game) if turn.action.effect_f
  830. Channel(game.channel_name).send "#{game.current_player} proceeds with #{turn.action.action.upcase}. #{effect}#{target_msg}."
  831. game.pay_for_current_turn
  832. game.process_current_turn
  833. if turn.action.needs_decision?
  834. turn.wait_for_decision
  835. if turn.action.action == :coup || turn.action.action == :assassin
  836. # In a double-kill situation, the target may already be out.
  837. # If target is already out, just move on to next turn.
  838. if turn.target_player.influence == 2
  839. self.prompt_to_flip(turn.target_player)
  840. elsif turn.target_player.influence == 1
  841. i = turn.target_player.characters.index { |c| c.face_down? }
  842. self.couped(game, turn.target_player, i + 1)
  843. else
  844. self.start_new_turn(game)
  845. end
  846. elsif turn.action.action == :ambassador
  847. self.prompt_to_switch(game, turn.active_player)
  848. elsif turn.action.action == :inquisitor
  849. if turn.target_player == turn.active_player
  850. self.prompt_to_switch(game, turn.active_player, 1)
  851. elsif turn.target_player.influence == 2
  852. self.prompt_to_pick_card(turn.target_player, "show to #{turn.active_player}", 'show')
  853. else
  854. i = turn.target_player.characters.index { |c| c.face_down? }
  855. self._show_to_inquisitor(game, turn.target_player, i + 1, turn.active_player)
  856. end
  857. end
  858. else
  859. self.start_new_turn(game)
  860. end
  861. else
  862. self.start_new_turn(game)
  863. end
  864. end
  865. def start_new_turn(game)
  866. game.next_turn
  867. Channel(game.channel_name).send "#{game.current_player}: It is your turn. Please choose an action."
  868. end
  869. def start_new_game(game)
  870. Channel(game.channel_name).moderated = false
  871. game.players.each do |p|
  872. Channel(game.channel_name).devoice(p.user)
  873. @user_games.delete(p.user)
  874. end
  875. @games[game.channel_name] = Game.new(game.channel_name)
  876. @idle_timers[game.channel_name].start
  877. end
  878. def list_players(m, channel_name = nil)
  879. game = self.game_of(m, channel_name, ['list players', '!who'])
  880. return unless game
  881. if game.players.empty?
  882. m.reply "No one has joined the game yet."
  883. else
  884. m.reply game.players.map{ |p| dehighlight_nick(p.to_s) }.join(' ')
  885. end
  886. end
  887. def devoice_channel(channel)
  888. channel.voiced.each do |user|
  889. channel.devoice(user)
  890. end
  891. end
  892. def remove_user_from_game(user, game, announce = true)
  893. left = game.remove_player(user)
  894. unless left.nil?
  895. Channel(game.channel_name).send "#{user.nick} has left the game (#{game.players.count}/#{Game::MAX_PLAYERS})" if announce
  896. Channel(game.channel_name).devoice(user)
  897. @user_games.delete(user)
  898. end
  899. end
  900. def dehighlight_nick(nickname)
  901. nickname.chars.to_a.join(8203.chr('UTF-8'))
  902. end
  903. #--------------------------------------------------------------------------------
  904. # Mod commands
  905. #--------------------------------------------------------------------------------
  906. def is_mod?(nick)
  907. # make sure that the nick is in the mod list and the user in authenticated
  908. user = User(nick)
  909. user.authed? && @mods.include?(user.authname)
  910. end
  911. def reset_game(m, channel_name)
  912. return unless self.is_mod? m.user.nick
  913. game = self.game_of(m, channel_name, ['reset a game', '!reset'])
  914. return unless game
  915. channel = Channel(game.channel_name)
  916. # Show everyone's cards.
  917. channel.send(table_info(game, cheating: true).join("\n")) if game.started?
  918. game.players.each do |p|
  919. @user_games.delete(p.user)
  920. end
  921. @games[channel.name] = Game.new(channel.name)
  922. self.devoice_channel(channel)
  923. channel.send("The game has been reset.")
  924. @idle_timers[channel.name].start
  925. end
  926. def kick_user(m, channel_name, nick)
  927. return unless self.is_mod? m.user.nick
  928. game = self.game_of(m, channel_name, ['kick a user', '!kick'])
  929. return unless game
  930. if game.not_started?
  931. user = User(nick)
  932. self.remove_user_from_game(user, game)
  933. else
  934. m.user.send "You can't kick someone while a game is in progress."
  935. end
  936. end
  937. def replace_user(m, nick1, nick2)
  938. if self.is_mod? m.user.nick
  939. # find irc users based on nick
  940. user1 = User(nick1)
  941. user2 = User(nick2)
  942. # Find game based on user 1
  943. game = @user_games[user1]
  944. # Can't do it if user2 is in a different game!
  945. if (game2 = @user_games[user2])
  946. m.user.send("#{nick2} is already in the #{game2.channel_name} game.")
  947. return
  948. end
  949. # replace the users for the players
  950. player = game.find_player(user1)
  951. player.user = user2
  952. # devoice/voice the players
  953. Channel(game.channel_name).devoice(user1)
  954. Channel(game.channel_name).voice(user2)
  955. @user_games.delete(user1)
  956. @user_games[user2] = game
  957. # inform channel
  958. Channel(game.channel_name).send "#{user1.nick} has been replaced with #{user2.nick}"
  959. # tell characters to new player
  960. User(player.user).send "="*40
  961. self.tell_characters_to(game, player)
  962. end
  963. end
  964. def room_mode(m, channel_name, mode)
  965. channel = channel_name ? Channel(channel_name) : m.channel
  966. if self.is_mod? m.user.nick
  967. case mode
  968. when "silent"
  969. Channel(channel.name).moderated = true
  970. when "vocal"
  971. Channel(channel.name).moderated = false
  972. end
  973. end
  974. end
  975. def who_chars(m, channel_name)
  976. return unless self.is_mod? m.user.nick
  977. game = self.game_of(m, channel_name, ['see a game', '!chars'])
  978. return unless game
  979. if game.started?
  980. if game.has_player?(m.user)
  981. m.user.send('Cheater!!!')
  982. else
  983. m.user.send(table_info(game, cheating: true).join("\n"))
  984. end
  985. else
  986. m.user.send('There is no game going on.')
  987. end
  988. end
  989. #--------------------------------------------------------------------------------
  990. # Game Settings
  991. #--------------------------------------------------------------------------------
  992. def self.parse_game_settings(options)
  993. unrecognized = []
  994. settings = []
  995. options.split.each { |opt|
  996. case opt.downcase
  997. when 'base'
  998. settings.clear
  999. when 'twoplayer'
  1000. settings << :twoplayer
  1001. when 'inquisitor', 'inquisition'
  1002. settings << :inquisitor
  1003. when 'reformation'
  1004. settings << :reformation
  1005. settings.delete(:incorporation)
  1006. when 'incorporation'
  1007. settings << :incorporation
  1008. settings.delete(:reforation)
  1009. else
  1010. unrecognized << opt
  1011. end
  1012. }
  1013. [settings.uniq, unrecognized]
  1014. end
  1015. def get_game_settings(m, channel_name = nil)
  1016. game = self.game_of(m, channel_name, ['see settings', '!settings'])
  1017. return unless game
  1018. m.reply("Game settings: #{game_settings(game)}.")
  1019. end
  1020. def set_game_settings(m, channel_name = nil, options = "")
  1021. game = self.game_of(m, channel_name, ['change settings', '!settings'])
  1022. return unless game && !game.started?
  1023. unless Channel(game.channel_name).has_user?(m.user)
  1024. m.user.send("You need to be in #{game.channel_name} to change the settings.")
  1025. return
  1026. end
  1027. settings, _ = self.class.parse_game_settings(options)
  1028. game.settings = settings
  1029. change_prefix = m.channel ? "The game has been changed" : "#{m.user.nick} has changed the game"
  1030. Channel(game.channel_name).send("#{change_prefix} to #{game_settings(game)}.")
  1031. end
  1032. def game_settings(game)
  1033. game.settings.empty? ? 'Base' : game.settings.collect { |s| s.to_s.capitalize }.join(', ')
  1034. end
  1035. #--------------------------------------------------------------------------------
  1036. # Helpers
  1037. #--------------------------------------------------------------------------------
  1038. def game_of(m, channel_name = nil, warn_user = nil)
  1039. # If in a channel, must be for that channel.
  1040. return @games[m.channel.name] if m.channel
  1041. # If in private and channel specified, show that channel.
  1042. return game = @games[channel_name] if channel_name
  1043. # If in private and channel not specified, show the game the player is in.
  1044. game = @user_games[m.user]
  1045. # and advise them if they aren't in any
  1046. m.reply("To #{warn_user[0]} via PM you must specify the channel: #{warn_user[1]} #channel") if game.nil? && !warn_user.nil?
  1047. game
  1048. end
  1049. def noticeme(m, toggle)
  1050. if toggle && toggle.downcase == 'on'
  1051. $pm_users.delete(m.user.nick)
  1052. settings = load_settings || {}
  1053. settings['pm_users'] = $pm_users
  1054. save_settings(settings)
  1055. elsif toggle && toggle.downcase == 'off'
  1056. $pm_users.add(m.user.nick)
  1057. settings = load_settings || {}
  1058. settings['pm_users'] = $pm_users
  1059. save_settings(settings)
  1060. end
  1061. m.reply("Private communications to you will occur in #{$pm_users.include?(m.user.nick) ? 'PRIVMSG' : 'NOTICE'}")
  1062. end
  1063. def help(m, page)
  1064. if page.to_s.downcase == "mod" && self.is_mod?(m.user.nick)
  1065. User(m.user).send "--- HELP PAGE MOD ---"
  1066. User(m.user).send "!reset - completely resets the game to brand new"
  1067. User(m.user).send "!replace nick1 nick1 - replaces a player in-game with a player out-of-game"
  1068. User(m.user).send "!kick nick1 - removes a presumably unresponsive user from an unstarted game"
  1069. User(m.user).send "!room silent|vocal - switches the channel from voice only users and back"
  1070. m.user.send('!chars - the obligatory cheating command - NOT to be used while you are a participant of the game')
  1071. else
  1072. case page
  1073. when "2"
  1074. User(m.user).send "--- HELP PAGE 2/3 ---"
  1075. m.user.send('!me - PMs you your current character cards')
  1076. m.user.send('!table - examines the table, showing any face-up cards and how many coins each player has')
  1077. m.user.send('!who - shows a list of players in turn order')
  1078. m.user.send('!status - shows which phase the game is in, and who currently needs to take an action')
  1079. when "3"
  1080. User(m.user).send "--- HELP PAGE 3/3 ---"
  1081. m.user.send('!rules (actions|inquisitor|reformation) - provides rules for the game; when provided with an argument, provides specified rules')
  1082. m.user.send('!settings (modes) - changes the game to the specified game type. modes may be "twoplayer" and/or "inquisitor" plus one of ("reformation" or "incorporation"), or blank to see current settings')
  1083. User(m.user).send "!subscribe - subscribe your current nick to receive PMs when someone calls !invite"
  1084. User(m.user).send "!unsubscribe - remove your nick from the invitation list"
  1085. m.user.send('!notice (on|off) - controls whether CoupBot will use NOTICE or PRIVMSG to communicate private information')
  1086. User(m.user).send "!invite - invites #boardgames and subscribers to join the game"
  1087. User(m.user).send "!changelog (#) - shows changelog for the bot, when provided a number it showed details"
  1088. else
  1089. User(m.user).send "--- HELP PAGE 1/3 ---"
  1090. User(m.user).send "!join - joins the game"
  1091. User(m.user).send "!leave - leaves the game"
  1092. User(m.user).send "!start - starts the game"
  1093. m.user.send('!action actionname - uses an action on your turn. actionname may be: income, foreign aid, duke, ambassador')
  1094. m.user.send('!action actionname targetname - uses an action on your turn against the specified target. actionname may be: coup, assassin, captain')
  1095. m.user.send('!block character - uses the specified character (duke, ambassador, captain, contessa) to block an opponent\'s action')
  1096. m.user.send('!challenge - challenge an opponent\'s claim of influence over a given character')
  1097. m.user.send('!pass - pass on either a chance to challenge or a chance to block an opponent\'s action')
  1098. m.user.send('!flip 1|2 - flip one of your character cards in response to a challenge')
  1099. m.user.send('!help (#) - when provided a number, pulls up specified page. Page 2 lists commands that show information about the current game. Page 3 lists commands to give information about CoupBot or about variants of Coup')
  1100. end
  1101. end
  1102. end
  1103. def intro(m)
  1104. m.user.send("Welcome to CoupBot. You can join a game if there's one getting started with the command \"!join\". For more commands, type \"!help\". If you don't know how to play, you can read a rules summary with \"!rules\".")
  1105. end
  1106. def rules(m, section)
  1107. case section.to_s.downcase
  1108. when 'inquisition', 'inquisitor'
  1109. m.user.send('http://boardgamegeek.com/image/1825161/coup')
  1110. m.user.send('The Inquisitor is a new role that replaces the Ambassador.')
  1111. m.user.send('Like the Ambassador, the Inquisitor blocks the Captain from stealing coins from you.')
  1112. m.user.send('Target yourself with the Inquisitor action to draw one card from the Court Deck. You may exchange this card with one of your face-down characters. The card you choose not to keep is returned to the Court Deck.')
  1113. m.user.send('Target an opponent with the Inquisitor to force that opponent to show you one of their character cards (their choice which). You may then allow them to keep that card, or discard it and draw a new one from the Court Deck.')
  1114. when 'reformation'
  1115. m.user.send('In Reformation, each player can belong to one of two factions: the Protestants or the Catholics. The initial faction distribution alternates around the table.')
  1116. m.user.send('While there are members of the opposite faction in the game, you may not target your factionmates with Captain, Assassin, Inquisitor, Coup, nor may you block their Foreign Aid. You may still challenge your factionmates.')
  1117. m.user.send('There are now three new actions available:')
  1118. m.user.send("* Apostatize: Pay one coin to the Almshouse to change your own faction.")
  1119. m.user.send("* Convert: Pay two coins to the Almshouse to change another player's faction.")
  1120. m.user.send("* Embezzle: Take all coins from the Almshouse. You must NOT have influence over the Duke to perform this action--if challenged, you must reveal both of your face-down characters to prove it.")
  1121. m.user.send('When only one faction exists, that faction descends into in-fighting and anyone can be targeted. Of course, someone may be converted to the opposite faction again....')
  1122. when 'actions'
  1123. m.user.send('http://boardgamegeek.com/image/1812508/coup')
  1124. m.user.send('General actions: Always available')
  1125. m.user.send('* Income: Take one coin from the treasury.')
  1126. m.user.send('* Foreign Aid: Take two coin from the treasury (blockable by Duke).')
  1127. m.user.send('* Coup: Pay seven coins and launch a coup against an opponent, forcing that player to lose an influence. If you have ten coins, you must take this action.')
  1128. m.user.send('Character actions: Anyone may perform these actions, but if challenged they must show that they influence that character.')
  1129. m.user.send('* Duke: Take three coins from the treasury. Block someone from taking foreign aid.')
  1130. m.user.send('* Assassin: Pay three coins and try to assassinate another player\'s character (blockable by Contessa).')
  1131. m.user.send('* Contessa: Block an assassination attempt against yourself.')
  1132. m.user.send('* Captain: Take two coins from another player (blockable by Captain or Ambassador). Block someone from stealing coins from you.')
  1133. m.user.send('* Ambassador: Draw two character cards from the Court Deck, choose which (if any) to exchange with your face-down characters, then return two. Block someone from stealing coins from you.')
  1134. else
  1135. m.user.send 'Each player starts the game with two coins and two influence - i.e., two face-down character cards; the fifteen card deck consists of three copies of five different characters, each with a unique set of powers.'
  1136. m.user.send('You can see all possible actions with the command "!rules actions", or by consulting the player aid at http://boardgamegeek.com/image/1812508/coup')
  1137. m.user.send('On your turn, you can take any character\'s action, regardless of which characters you actually have in front of you, or you can take one three actions that require no character: Income, Foreign Aid, or Coup (if you have ten coins, you must Coup).')
  1138. m.user.send('When you take one of the character actions - whether actively on your turn, or defensively in response to someone else\'s action - that character\'s action automatically succeeds unless an opponent challenges you.')
  1139. m.user.send('When challenged to show a character, if you can\'t reveal the character (or choose not to), you lose an influence, turning one of your characters face-up. Face-up characters cannot be used, and if both of your characters are face-up, you\'re out of the game.')
  1140. m.user.send('If you do have the character in question and choose to reveal it, the opponent loses an influence, then you shuffle that character into the deck and draw a new one, perhaps getting the same character again and perhaps not.')
  1141. m.user.send('The last player to still have influence - that is, a face-down character - wins the game!')
  1142. end
  1143. end
  1144. def status(m)
  1145. game = self.game_of(m)
  1146. return unless game
  1147. if game.started?
  1148. turn = game.current_turn
  1149. action = "#{dehighlight_nick(turn.active_player.user.nick)}'s #{turn.action.to_s.upcase}" if turn.action
  1150. block = "#{dehighlight_nick(turn.counteracting_player.user.nick)}'s #{turn.counteraction.to_s.upcase} blocking #{action}" if turn.counteraction
  1151. if turn.waiting_for_action?
  1152. status = "Waiting on #{turn.active_player} to take an action"
  1153. elsif turn.waiting_for_action_challenge?
  1154. players = game.not_reacted.map(&:user).join(", ")
  1155. status = "Waiting on players to PASS or CHALLENGE #{action}: #{players}"
  1156. elsif turn.waiting_for_action_challenge_reply?
  1157. status = "Waiting on #{turn.active_player} to respond to challenge against #{action}"
  1158. elsif turn.waiting_for_action_challenge_loser?
  1159. status = "Waiting on #{turn.action_challenger} to pick character to lose"
  1160. elsif turn.waiting_for_block?
  1161. if turn.action.needs_target
  1162. players = turn.target_player.to_s
  1163. else
  1164. players = game.not_reacted.map(&:user).join(", ")
  1165. end
  1166. status = "Waiting on players to PASS or BLOCK #{action}: #{players}"
  1167. elsif turn.waiting_for_block_challenge?
  1168. players = game.not_reacted.map(&:user).join(", ")
  1169. status = "Waiting on players to PASS or CHALLENGE #{block}: #{players}"
  1170. elsif turn.waiting_for_block_challenge_reply?
  1171. status = "Waiting on #{turn.counteracting_player} to respond to challenge against #{block}"
  1172. elsif turn.waiting_for_block_challenge_loser?
  1173. status = "Waiting on #{turn.block_challenger} to pick character to lose"
  1174. elsif turn.waiting_for_decision?
  1175. status = "Waiting on #{turn.decider} to make decision on #{turn.action.to_s.upcase}"
  1176. elsif turn.waiting_for_initial_characters?
  1177. players = game.not_selected_initial_character.map(&:user).join(", ")
  1178. status = 'Waiting on players to pick character: ' + players
  1179. else
  1180. status = "Unknown status #{turn.state}"
  1181. end
  1182. else
  1183. if game.player_count.zero?
  1184. status = "No game in progress."
  1185. else
  1186. status = "A game is forming. #{game.player_count} players have joined: #{game.players.map(&:user).join(", ")}"
  1187. end
  1188. end
  1189. m.reply status
  1190. end
  1191. def changelog_dir(m)
  1192. @changelog.first(5).each_with_index do |changelog, i|
  1193. User(m.user).send "#{i+1} - #{changelog["date"]} - #{changelog["changes"].length} changes"
  1194. end
  1195. end
  1196. def changelog(m, page = 1)
  1197. changelog_page = @changelog[page.to_i-1]
  1198. User(m.user).send "Changes for #{changelog_page["date"]}:"
  1199. changelog_page["changes"].each do |change|
  1200. User(m.user).send "- #{change}"
  1201. end
  1202. end
  1203. def invite(m)
  1204. game = self.game_of(m)
  1205. return unless game
  1206. if game.accepting_players?
  1207. if game.invitation_sent?
  1208. m.reply "An invitation cannot be sent out again so soon."
  1209. else
  1210. game.mark_invitation_sent
  1211. User("BG3PO").send "!invite_to_coup_game"
  1212. User(m.user).send "Invitation has been sent."
  1213. settings = load_settings || {}
  1214. subscribers = settings["subscribers"]
  1215. current_players = game.players.map{ |p| p.user.nick }
  1216. subscribers.each do |subscriber|
  1217. unless current_players.include? subscriber
  1218. User(subscriber).refresh
  1219. if User(subscriber).online?
  1220. User(subscriber).send "A game of Coup is gathering in #playcoup ..."
  1221. end
  1222. end
  1223. end
  1224. # allow for reset after provided time
  1225. Timer(@invite_timer_length, shots: 1) do
  1226. game.reset_invitation
  1227. end
  1228. end
  1229. end
  1230. end
  1231. def subscribe(m)
  1232. settings = load_settings || {}
  1233. subscribers = settings["subscribers"] || []
  1234. if subscribers.include?(m.user.nick)
  1235. User(m.user).send "You are already subscribed to the invitation list."
  1236. else
  1237. if User(m.user).authed?
  1238. subscribers << m.user.nick
  1239. settings["subscribers"] = subscribers
  1240. save_settings(settings)
  1241. User(m.user).send "You've been subscribed to the invitation list."
  1242. else
  1243. User(m.user).send "Whoops. You need to be identified on freenode to be able to subscribe. Either identify (\"/msg Nickserv identify [password]\") if you are registered, or register your account (\"/msg Nickserv register [email] [password]\")"
  1244. User(m.user).send "See http://freenode.net/faq.shtml#registering for help"
  1245. end
  1246. end
  1247. end
  1248. def unsubscribe(m)
  1249. settings = load_settings || {}
  1250. subscribers = settings["subscribers"] || []
  1251. if subscribers.include?(m.user.nick)
  1252. if User(m.user).authed?
  1253. subscribers.delete_if{ |sub| sub == m.user.nick }
  1254. settings["subscribers"] = subscribers
  1255. save_settings(settings)
  1256. User(m.user).send "You've been unsubscribed to the invitation list."
  1257. else
  1258. User(m.user).send "Whoops. You need to be identified on freenode to be able to unsubscribe. Either identify (\"/msg Nickserv identify [password]\") if you are registered, or register your account (\"/msg Nickserv register [email] [password]\")"
  1259. User(m.user).send "See http://freenode.net/faq.shtml#registering for help"
  1260. end
  1261. else
  1262. User(m.user).send "You are not subscribed to the invitation list."
  1263. end
  1264. end
  1265. #--------------------------------------------------------------------------------
  1266. # Settings
  1267. #--------------------------------------------------------------------------------
  1268. def save_settings(settings)
  1269. output = File.new(@settings_file, 'w')
  1270. output.puts YAML.dump(settings)
  1271. output.close
  1272. end
  1273. def load_settings
  1274. output = File.new(@settings_file, 'r')
  1275. settings = YAML.load(output.read)
  1276. output.close
  1277. settings
  1278. end
  1279. def load_changelog
  1280. output = File.new(CHANGELOG_FILE, 'r')
  1281. changelog = YAML.load(output.read)
  1282. output.close
  1283. changelog
  1284. end
  1285. end
  1286. end
  1287. end