X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=data%2Frbot%2Fplugins%2Fgames%2Funo.rb;h=e619027ceb2a7ee14abd4240a50868ad1a0b2cb2;hb=d0cf3bdc334e903ed03c11ae9dcec003b84038d2;hp=6e602a69beda2f456eff05cbf64ba57c214a0fb2;hpb=2e87853789cc18c6a3450f9c79da32f94920d376;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/data/rbot/plugins/games/uno.rb b/data/rbot/plugins/games/uno.rb index 6e602a69..e619027c 100644 --- a/data/rbot/plugins/games/uno.rb +++ b/data/rbot/plugins/games/uno.rb @@ -13,9 +13,7 @@ # # TODO documentation # TODO allow full form card names for play -# TODO allow color specification with wild # TODO allow choice of rules re stacking + and playing Reverse with them -# TODO highscore table class UnoGame COLORS = %w{Red Green Blue Yellow} @@ -110,7 +108,7 @@ class UnoGame @color = 'Wild' raise if value and not value == '+4' if value - @value = value.dup + @value = value.dup @shortform = 'w'+value else @value = nil @@ -162,7 +160,13 @@ class UnoGame # number of cards to be picked if the player can't play an appropriate card attr_reader :picker - def initialize(plugin, channel) + # game start time + attr :start_time + + # the IRC user that created the game + attr_accessor :manager + + def initialize(plugin, channel, manager) @channel = channel @plugin = plugin @bot = plugin.bot @@ -180,6 +184,7 @@ class UnoGame @picker = 0 @last_picker = 0 @must_play = nil + @manager = manager end def get_player(user) @@ -206,6 +211,13 @@ class UnoGame @bot.notice player.user, msg, opts end + def notify_error(player, msg, opts={}) + announce _("you can't do that, %{p}") % { + :p => player.user + } + notify player, msg, opts + end + def make_base_stock @base_stock = COLORS.inject([]) do |list, clr| VALUES.each do |n| @@ -232,6 +244,7 @@ class UnoGame end def start_game + @join_timer = nil debug "Starting game" @players.shuffle! show_order @@ -324,21 +337,35 @@ class UnoGame def next_turn(opts={}) @players << @players.shift @player_has_picked = false - show_turn + show_turn unless opts[:silent] end def can_play(card) # if play is forced, check against the only allowed cards return false if @must_play and not @must_play.include?(card) - # When a +something is online, you can only play a +something of same or - # higher something, or a Reverse of the correct color, or a Reverse on - # a Reverse - # TODO make optional if @picker > 0 - return true if card.picker >= @last_picker - return true if card.value == 'Reverse' and (card.color == @color or @discard.value == card.value) - return false + # During a picker run (i.e. after a +something was played and before a + # player is forced to pick) you can only play pickers (+2, +4) and + # Reverse. Reverse can be played if the previous card matches by color or + # value (as usual), a +4 can always be played, a +2 can be played on a +2 + # of any color or on a Reverse of the correct color unless a +4 was + # played on it + # TODO make optional + case card.value + when 'Reverse' + # Reverse can be played if it matches color or value + return (card.color == @color) || (@discard.value == card.value) + when '+2' + return false if @last_picker > 2 + return true if @discard.value == card.value + return true if @discard.value == 'Reverse' and @color == card.color + return false + when '+4' + return true + else + return false + end else # You can always play a Wild return true if Wild === card @@ -371,14 +398,28 @@ class UnoGame toplay = (full == short) ? 1 : 2 end debug [full, short, jolly, jcolor, toplay].inspect - # r7r7 -> r7r7, r7, nil, nil - # r7 -> r7, r7, nil, nil - # w -> w, nil, w, nil - # wg -> wg, nil, w, g + # r7r7 -> r7r7, r7, nil, nil, 2 + # r7 -> r7, r7, nil, nil, 1 + # w -> w, nil, w, nil, 1 + # wg -> wg, nil, w, g, 1 + + # if @color is nil, the player just played a wild without specifying + # a color. (s)he should now use "co ", but we allow him to + # replay the wild _and_ specify the color, without actually replaying + # the card (which would otherwise happen if the player has another wild) + if @color.nil? + if jcolor + choose_color(p.user, jcolor) + else + announce _("you already played your card, ") + _("%{p}, choose a color with: co r|b|g|y") % { :p => p } + end + return + end + if cards = p.has_card?(short) debug cards unless can_play(cards.first) - announce _("you can't play that card") + notify_error p, _("you can't play that card") return end if cards.length >= toplay @@ -392,7 +433,7 @@ class UnoGame # only be possible if the first W+4 was illegal, so it wouldn't # apply for a W+4 played on a +2 anyway. # - if @picker == 0 and Wild === cards.first and cards.first.value + if @picker == 0 and Wild === cards.first and cards.first.value # save the previous discard in case of challenge @last_discard = @discard.dup # save the color too, in case it was a Wild @@ -432,10 +473,10 @@ class UnoGame announce _("%{p}, choose a color with: co r|b|g|y") % { :p => p } end else - announce _("you don't have two cards of that kind") + notify_error p, _("you don't have two cards of that kind") end else - announce _("you don't have that card") + notify_error p, _("you don't have that card") end end @@ -515,6 +556,13 @@ class UnoGame end def choose_color(user, color) + # you can only pick a color if the current color is unset + if @color + announce _("you can't pick a color now, %{p}") % { + :p => get_player(user) + } + return + end case color when 'r' @color = 'Red' @@ -560,7 +608,7 @@ class UnoGame end def has_turn?(source) - @players.first.user == source + @start_time && (@players.first.user == source) end def show_picker @@ -580,6 +628,7 @@ class UnoGame def show_user_cards(player) p = Player === player ? player : get_player(player) + return unless p notify p, _('Your cards: %{cards}') % { :cards => p.cards.join(' ') } @@ -637,9 +686,9 @@ class UnoGame end cards = 7 if @start_time - cards = @players.inject(0) do |s, pl| + cards = (@players.inject(0) do |s, pl| s +=pl.cards.length - end/@players.length + end*1.0/@players.length).ceil end p = Player.new(user) @players << p @@ -672,11 +721,18 @@ class UnoGame } case @players.length when 2 + if @join_timer + @bot.timer.remove(@join_timer) + announce _("game start countdown stopped") + @join_timer = nil + end if p == @players.first - next_turn + next_turn :silent => @start_time.nil? + end + if @start_time + end_game + return end - end_game - return when 1 end_game(true) return @@ -716,6 +772,11 @@ class UnoGame def end_game(halted = false) runtime = @start_time ? Time.now - @start_time : 0 + if @join_timer + @bot.timer.remove(@join_timer) + announce _("game start countdown stopped") + @join_timer = nil + end if halted if @start_time announce _("%{uno} game halted after %{time}") % { @@ -734,7 +795,11 @@ class UnoGame } end if @picker > 0 and not halted - p = @players[1] + if @discard.value == 'Reverse' + p = @players.last + else + p = @players[1] + end announce _("%{p} has to pick %{b}%{n}%{b} cards!") % { :p => p, :n => @picker, :b => Bold } @@ -796,7 +861,7 @@ class UnoPlugin < Plugin _("'od' to show the playing order"), _("'ti' to show play time"), _("'tu' to show whose turn it is") - ].join(" ; ") + ].join("; ") when 'challenge' _("A Wild +4 can only be played legally if you don't have normal (not special) cards of the current color. ") + _("The next player can challenge a W+4 by using the 'ch' command. ") + @@ -809,17 +874,47 @@ class UnoPlugin < Plugin _("Wild +4 also forces the next player to take 4 cards, but it can only be played if you can't play a color card. ") + _("you can play another +2 or +4 card on a +2 card, and a +4 on a +4, forcing the first player who can't play one to pick the cumulative sum of all cards. ") + _("you can also play a Reverse on a +2 or +4, bouncing the effect back to the previous player (that now comes next). ") + when /scor(?:e|ing)/, /points?/ + [ + _("The points won with a game of %{uno} are totalled from the cards remaining in the hands of the other players."), + _("Each normal (not special) card is worth its face value (from 0 to 9 points)."), + _("Each colored special card (+2, Reverse, Skip) is worth 20 points."), + _("Each Wild and Wild +4 is worth 50 points."), + help(plugin, 'top'), + help(plugin, 'topwin'), + ].join(" ") % { :uno => UnoGame::UNO } + when 'top' + _("You can see the scoring table with 'uno top N' where N is the number of top scores to show.") + when 'topwin' + _("You can see the winners table with 'uno topwin N' where N is the number of top winners to show.") + when /cards?/ + [ + _("There are 108 cards in a standard %{uno} deck."), + _("For each color (Blue, Green, Red, Yellow) there are 19 numbered cards (from 0 to 9), with two of each number except for 0."), + _("There are also 6 special cards for each color, two each of +2, Reverse, Skip."), + _("Finally, there are 4 Wild and 4 Wild +4 cards.") + ].join(" ") % { :uno => UnoGame::UNO } + when 'admin' + _("The game manager (the user that started the game) can execute the following commands to manage it: ") + + [ + _("'uno drop ' to drop a user from the game (any user can drop itself using 'uno drop')"), + _("'uno replace [with] ' to replace a player with someone else (useful in case of disconnects)"), + _("'uno transfer [to] ' to transfer game ownership to someone else"), + _("'uno end' to end the game before its natural completion") + ].join("; ") else - (_("%{uno} game. !uno to start a game. see help uno rules for the rules. commands: %{cmds}") % { + _("%{uno} game. !uno to start a game. see 'help uno rules' for the rules, 'help uno admin' for admin commands, 'help uno score' for scoring rules. In-game commands: %{cmds}.") % { :uno => UnoGame::UNO, :cmds => help(plugin, 'commands') - }) + } end end def message(m) return unless @games.key?(m.channel) + return unless m.plugin # skip messages such as: botname, g = @games[m.channel] + replied = true case m.plugin.intern when :jo # join game return if m.params @@ -838,7 +933,7 @@ class UnoPlugin < Plugin m.reply _("It's not your turn") end when :pa # pass turn - return if m.params + return if m.params or not g.start_time if g.has_turn?(m.source) g.pass(m.source) else @@ -860,7 +955,7 @@ class UnoPlugin < Plugin return if m.params g.show_all_cards(m.source) when :cd # show current discard - return if m.params + return if m.params or not g.start_time g.show_discard when :ch if g.has_turn?(m.source) @@ -881,25 +976,52 @@ class UnoPlugin < Plugin when :tu # show whose turn is it return if m.params if g.has_turn?(m.source) - m.nickreply _("it's your turn, sleepyhead") + m.reply _("it's your turn, sleepyhead"), :nick => true else g.show_turn(:cards => false) end + else + replied=false end + m.replied=true if replied end def create_game(m, p) if @games.key?(m.channel) - m.reply _("There is already an %{uno} game running here, say 'jo' to join in") % { :uno => UnoGame::UNO } + m.reply _("There is already an %{uno} game running here, managed by %{who}. say 'jo' to join in") % { + :who => @games[m.channel].manager, + :uno => UnoGame::UNO + } return end - @games[m.channel] = UnoGame.new(self, m.channel) + @games[m.channel] = UnoGame.new(self, m.channel, m.source) + @bot.auth.irc_to_botuser(m.source).set_temp_permission('uno::manage', true, m.channel) m.reply _("Ok, created %{uno} game on %{channel}, say 'jo' to join in") % { :uno => UnoGame::UNO, :channel => m.channel } end + def transfer_ownership(m, p) + unless @games.key?(m.channel) + m.reply _("There is no %{uno} game running here") % { :uno => UnoGame::UNO } + return + end + g = @games[m.channel] + old = g.manager + new = m.channel.get_user(p[:nick]) + if new + g.manager = new + @bot.auth.irc_to_botuser(old).reset_temp_permission('uno::manage', m.channel) + @bot.auth.irc_to_botuser(new).set_temp_permission('uno::manage', true, m.channel) + m.reply _("%{uno} game ownership transferred from %{old} to %{nick}") % { + :uno => UnoGame::UNO, :old => old, :nick => p[:nick] + } + else + m.reply _("who is this %{nick} you want me to transfer game ownership to?") % p + end + end + def end_game(m, p) unless @games.key?(m.channel) m.reply _("There is no %{uno} game running here") % { :uno => UnoGame::UNO } @@ -908,6 +1030,11 @@ class UnoPlugin < Plugin @games[m.channel].end_game(true) end + def cleanup + @games.each { |k, g| g.end_game(true) } + super + end + def chan_reg(channel) @registry.sub_registry(channel.downcase) end @@ -966,6 +1093,7 @@ class UnoPlugin < Plugin pstats[k] = pls end + @bot.auth.irc_to_botuser(@games[channel].manager).reset_temp_permission('uno::manage', channel) @games.delete(channel) end @@ -1067,14 +1195,22 @@ class UnoPlugin < Plugin scores << [v.won.inject(0) { |s, w| s+=w.score }, k] end + if wins.empty? + m.reply(_("no %{uno} games were completed here") % { + :uno => UnoGame::UNO + }) + return + end + + if n = p[:scorenum] msg = _("%{uno} %{num} highest scores: ") % { :uno => UnoGame::UNO, :num => p[:scorenum] } scores.sort! { |a1, a2| -(a1.first <=> a2.first) } scores = scores[0, n.to_i].compact + i = 0 if scores.length <= 5 - i = 0 list = "\n" + scores.map { |a| i+=1 _("%{i}. %{b}%{nick}%{b} with %{b}%{score}%{b} points") % { @@ -1095,8 +1231,8 @@ class UnoPlugin < Plugin } wins.sort! { |a1, a2| -(a1.first <=> a2.first) } wins = wins[0, n.to_i].compact + i = 0 if wins.length <= 5 - i = 0 list = "\n" + wins.map { |a| i+=1 _("%{i}. %{b}%{nick}%{b} with %{b}%{score}%{b} wins") % { @@ -1122,11 +1258,12 @@ end pg = UnoPlugin.new pg.map 'uno', :private => false, :action => :create_game -pg.map 'uno end', :private => false, :action => :end_game -pg.map 'uno drop', :private => false, :action => :drop_player -pg.map 'uno giveup', :private => false, :action => :drop_player -pg.map 'uno drop :nick', :private => false, :action => :drop_player, :auth_path => ':other' -pg.map 'uno replace :old [with] :new', :private => false, :action => :replace_player +pg.map 'uno end', :private => false, :action => :end_game, :auth_path => 'manage' +pg.map 'uno drop', :private => false, :action => :drop_player, :auth_path => 'manage::drop::self!' +pg.map 'uno giveup', :private => false, :action => :drop_player, :auth_path => 'manage::drop::self!' +pg.map 'uno drop :nick', :private => false, :action => :drop_player, :auth_path => 'manage::drop::other!' +pg.map 'uno replace :old [with] :new', :private => false, :action => :replace_player, :auth_path => 'manage' +pg.map 'uno transfer [game [ownership]] [to] :nick', :private => false, :action => :transfer_ownership, :auth_path => 'manage' pg.map 'uno stock', :private => false, :action => :print_stock pg.map 'uno chanstats', :private => false, :action => :do_chanstats pg.map 'uno stats [:nick]', :private => false, :action => :do_pstats @@ -1134,6 +1271,5 @@ pg.map 'uno top :scorenum', :private => false, :action => :do_top, :defaults => pg.map 'uno topwin :winnum', :private => false, :action => :do_top, :defaults => { :winnum => 5 } pg.default_auth('stock', false) -pg.default_auth('end', false) -pg.default_auth('drop::other', false) -pg.default_auth('replace', false) +pg.default_auth('manage', false) +pg.default_auth('manage::drop::self', true)