PageRenderTime 54ms CodeModel.GetById 17ms 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

Large files files are truncated, but you can click here to view the full 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

Large files files are truncated, but you can click here to view the full file