summaryrefslogtreecommitdiff
path: root/data/rbot/plugins/games/uno.rb
diff options
context:
space:
mode:
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2008-04-09 14:30:26 +0200
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2008-04-09 14:30:26 +0200
commite97360e5b530c11747c91816bf02a85f9d3d157d (patch)
tree01239f53aac9b5d7eeeba4fbe5a003fb069a6890 /data/rbot/plugins/games/uno.rb
parent1bb149baa203f8c647fbe6647b329fd1635add0d (diff)
uno plugin: initial implementation of UNO! game, no endgame yet
Diffstat (limited to 'data/rbot/plugins/games/uno.rb')
-rw-r--r--data/rbot/plugins/games/uno.rb608
1 files changed, 608 insertions, 0 deletions
diff --git a/data/rbot/plugins/games/uno.rb b/data/rbot/plugins/games/uno.rb
new file mode 100644
index 00000000..73be2f5f
--- /dev/null
+++ b/data/rbot/plugins/games/uno.rb
@@ -0,0 +1,608 @@
+#-- vim:sw=2:et
+#++
+#
+# :title: Uno Game Plugin for rbot
+#
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+#
+# Copyright:: (C) 2008 Giuseppe Bilotta
+#
+# License:: GPL v2
+#
+# Uno Game: get rid of the cards you have
+
+class UnoGame
+ COLORS = %w{Red Green Blue Yellow}
+ SPECIALS = %w{+2 Reverse Skip}
+ NUMERICS = (0..9).to_a
+ VALUES = NUMERICS + SPECIALS
+
+ def UnoGame.color_map(clr)
+ case clr
+ when 'Red'
+ :red
+ when 'Blue'
+ :royal_blue
+ when 'Green'
+ :limegreen
+ when 'Yellow'
+ :yellow
+ end
+ end
+
+ def UnoGame.irc_color_bg(clr)
+ Irc.color([:white,:black][COLORS.index(clr)%2],UnoGame.color_map(clr))
+ end
+
+ def UnoGame.irc_color_fg(clr)
+ Irc.color(UnoGame.color_map(clr))
+ end
+
+ def UnoGame.colorify(str, fg=false)
+ ret = Bold.dup
+ str.length.times do |i|
+ ret << (fg ?
+ UnoGame.irc_color_fg(COLORS[i%4]) :
+ UnoGame.irc_color_bg(COLORS[i%4]) ) +str[i,1]
+ end
+ ret << NormalText
+ end
+
+ UNO = UnoGame.colorify('UNO!', true)
+
+ # Colored play cards
+ class Card
+ attr_reader :color
+ attr_reader :value
+ attr_reader :shortform
+ attr_reader :to_s
+ attr_reader :score
+
+ def initialize(color, value)
+ raise unless COLORS.include? color
+ @color = color.dup
+ raise unless VALUES.include? value
+ if NUMERICS.include? value
+ @value = value
+ @score = value
+ else
+ @value = value.dup
+ @score = 20
+ end
+ if @value == '+2'
+ @shortform = (@color[0,1]+@value).downcase
+ else
+ @shortform = (@color[0,1]+@value.to_s[0,1]).downcase
+ end
+ @to_s = UnoGame.irc_color_bg(@color) +
+ Bold + ['', @color, @value, ''].join(' ') + NormalText
+ end
+
+ def picker
+ return 0 unless @value.to_s[0,1] == '+'
+ return @value[1,1].to_i
+ end
+
+ def special?
+ SPECIALS.include?(@value)
+ end
+ end
+
+ # Wild, Wild +4 cards
+ class Wild < Card
+ def initialize(value=nil)
+ @color = 'Wild'
+ raise if value and not value == '+4'
+ if value
+ @value = value.dup
+ @shortform = 'w'+value
+ else
+ @value = nil
+ @shortform = 'w'
+ end
+ @score = 50
+ @to_s = UnoGame.colorify(['', @color, @value, ''].compact.join(' '))
+ end
+ def special?
+ @value
+ end
+ end
+
+ class Player
+ attr_accessor :cards
+ attr_reader :user
+ def initialize(user)
+ @user = user
+ @cards = []
+ end
+ def has_card?(short)
+ cards = []
+ @cards.each { |c|
+ cards << c if c.shortform == short
+ }
+ if cards.empty?
+ return false
+ else
+ return cards
+ end
+ end
+ def to_s
+ @user.to_s
+ end
+ end
+
+ attr_reader :stock
+ attr_reader :discard
+ attr_reader :channel
+ attr :players
+ attr_reader :player_has_picked
+ attr_reader :picker
+
+ def initialize(plugin, channel)
+ @channel = channel
+ @plugin = plugin
+ @bot = plugin.bot
+ @players = []
+ @discard = nil
+ make_base_stock
+ @stock = []
+ make_stock
+ @start_time = nil
+ @join_timer = nil
+ end
+
+ def get_player(user)
+ @players.each { |p| return p if p.user == user }
+ return nil
+ end
+
+ def announce(msg, opts={})
+ @bot.say channel, msg, opts
+ end
+
+ def notify(player, msg, opts={})
+ @bot.notice player.user, msg, opts
+ end
+
+ def make_base_stock
+ @base_stock = COLORS.inject([]) do |list, clr|
+ VALUES.each do |n|
+ list << Card.new(clr, n)
+ list << Card.new(clr, n) unless n == 0
+ end
+ list
+ end
+ 4.times do
+ @base_stock << Wild.new
+ @base_stock << Wild.new('+4')
+ end
+ end
+
+ def make_stock
+ @stock.replace @base_stock
+ # remove the cards in the players hand
+ @players.each { |p| p.cards.each { |c| @stock.delete_one c } }
+ # remove current top discarded card if present
+ if @discard
+ @stock.delete_one(discard)
+ end
+ @stock.shuffle!
+ end
+
+ def start_game
+ debug "Starting game"
+ @players.shuffle!
+ show_order
+ announce _("%{p} deals the first card from the stock") % {
+ :p => @players.first.user
+ }
+ card = @stock.shift
+ @picker = 0
+ @special = false
+ while Wild === card do
+ @stock.insert(rand(@stock.length), card)
+ card = @stock.shift
+ end
+ set_discard(card)
+ show_discard
+ if @special
+ do_special
+ end
+ next_turn
+ @start_time = Time.now
+ end
+
+ def reverse_turn
+ if @players.length > 2
+ @players.reverse!
+ # put the current player back in its place
+ @players.unshift @players.pop
+ announce _("Playing order was reversed!")
+ else
+ skip_turn
+ end
+ end
+
+ def skip_turn
+ @players << @players.shift
+ announce _("%{p} skips a turn!") % {
+ # this is first and not last because the actual
+ # turn change will be done by the following next_turn
+ :p => @players.first.user
+ }
+ end
+
+ def do_special
+ case @discard.value
+ when 'Reverse'
+ reverse_turn
+ @special = false
+ when 'Skip'
+ skip_turn
+ @special = false
+ end
+ end
+
+ def set_discard(card)
+ @discard = card
+ @value = card.value.dup rescue card.value
+ if Wild === card
+ @color = nil
+ else
+ @color = card.color.dup
+ end
+ if card.picker > 0
+ @picker += card.picker
+ else
+ @picker = 0
+ end
+ if card.special?
+ @special = true
+ else
+ @special = false
+ end
+ end
+
+ def next_turn(opts={})
+ @players << @players.shift
+ @player_has_picked = false
+ show_turn
+ end
+
+ def can_play(card)
+ # When a +something is online, you can only play
+ # a +something of same or higher something, or a Reverse
+ # TODO make optional
+ if @picker > 0
+ if card.value == 'Reverse' or card.picker >= @discard.picker
+ return true
+ else
+ return false
+ end
+ else
+ # You can always play a Wild
+ # FIXME W+4 can only be played if you don't have a proper card
+ # TODO make it playable anyway, and allow players to challenge
+ return true if Wild === card
+ # On a Wild, you must match the color
+ if Wild === @discard
+ return card.color == @color
+ else
+ # Otherwise, you can match either the value or the color
+ return (card.value == @value) || (card.color == @color)
+ end
+ end
+ end
+
+ def play_card(source, cards)
+ debug "Playing card #{cards}"
+ p = get_player(source)
+ shorts = cards.scan(/[rbgy]\s*(?:\+?\d|[rs])|w\s*(?:\+4)?/)
+ debug shorts.inspect
+ if shorts.length > 2 or shorts.length < 1
+ announce _("you can only play one or two cards")
+ return
+ end
+ if shorts.length == 2 and shorts.first != shorts.last
+ announce _("you can only play two cards if they are the same")
+ return
+ end
+ if cards = p.has_card?(shorts.first)
+ debug cards
+ unless can_play(cards.first)
+ announce _("you can't play that card")
+ return
+ end
+ if cards.length >= shorts.length
+ set_discard(p.cards.delete_one(cards.shift))
+ if shorts.length > 1
+ set_discard(p.cards.delete_one(cards.shift))
+ announce _("%{p} plays %{card} twice!") % {
+ :p => source,
+ :card => @discard
+ }
+ else
+ announce _("%{p} plays %{card}") % { :p => source, :card => @discard }
+ end
+ if p.cards.length == 1
+ announce _("%{p} has %{uno}!") % {
+ :p => source, :uno => UNO
+ }
+ elsif p.cards.length == 0
+ end_game
+ return
+ end
+ show_picker
+ if @color
+ if @special
+ do_special
+ end
+ next_turn
+ else
+ announce _("%{p}, choose a color with: co r|b|g|y") % {
+ :p => p.user
+ }
+ end
+ else
+ announce _("you don't have that card")
+ end
+ end
+ end
+
+ def pass(user)
+ p = get_player(user)
+ if @picker > 0
+ announce _("%{p} passes turn, and has to pick %{b}%{n}%{b} cards!") % {
+ :p => user, :b => Bold, :n => @picker
+ }
+ deal(p, @picker)
+ @picker = 0
+ else
+ if @player_has_picked
+ announce _("%{p} passes turn") % { :p => user }
+ else
+ announce _("you need to pick a card first")
+ return
+ end
+ end
+ next_turn
+ end
+
+ def choose_color(user, color)
+ case color
+ when 'r'
+ @color = 'Red'
+ when 'b'
+ @color = 'Blue'
+ when 'g'
+ @color = 'Green'
+ when 'y'
+ @color = 'Yellow'
+ else
+ announce _('what color is that?')
+ return
+ end
+ announce _('color is now %{c}') % { :c => @color.downcase }
+ next_turn
+ end
+
+ def show_time
+ if @start_time
+ announce _("This %{uno} game has been going on for %{time}") % {
+ :uno => UNO,
+ :time => Utils.secs_to_string(Time.now - @start_time)
+ }
+ else
+ announce _("The game hasn't started yet")
+ end
+ end
+
+ def show_order
+ announce _("%{uno} playing turn: %{players}") % {
+ :uno => UNO, :players => players.join(' ')
+ }
+ end
+
+ def show_turn(opts={})
+ cards = true
+ cards = opts[:cards] if opts.key?(:cards)
+ player = @players.first
+ announce _("it's %{player}'s turn") % { :player => player.user }
+ show_user_cards(player) if cards
+ end
+
+ def has_turn?(source)
+ @players.first.user == source
+ end
+
+ def show_picker
+ if @picker > 0
+ announce _("next player must respond correctly or pick %{b}%{n}%{b} cards") % {
+ :b => Bold, :n => @picker
+ }
+ end
+ end
+
+ def show_discard
+ announce _("Current discard: %{card} %{c}") % { :card => @discard,
+ :c => (Wild === @discard) ? UnoGame.irc_color_bg(@color) + " #{@color} " : nil
+ }
+ show_picker
+ end
+
+ def show_user_cards(player)
+ p = Player === player ? player : get_player(player)
+ notify p, _('Your cards: %{cards}') % {
+ :cards => p.cards.join(' ')
+ }
+ end
+
+ def show_all_cards(u=nil)
+ announce(@players.inject([]) { |list, p|
+ list << [p.user, p.cards.length].join(': ')
+ }.join(', '))
+ if u
+ show_user_cards(u)
+ end
+ end
+
+ def pick_card(user)
+ p = get_player(user)
+ announce _("%{player} picks a card") % { :player => p }
+ deal(p, 1)
+ @player_has_picked = true
+ end
+
+ def deal(player, num=1)
+ picked = []
+ num.times do
+ picked << @stock.delete_one
+ if @stock.length == 0
+ announce _("Shuffling discarded cards")
+ make_stock
+ if @stock.length == 0
+ announce _("No more cards!")
+ end_game # FIXME nope!
+ end
+ end
+ end
+ notify player, _("You picked %{picked}") % { :picked => picked.join(' ') }
+ player.cards += picked
+ end
+
+ def add_player(user)
+ return if get_player(user)
+ p = Player.new(user)
+ @players << p
+ deal(p, 7)
+ if @join_timer
+ @bot.timer.reschedule(@join_timer, 10)
+ elsif @players.length > 1
+ announce _("game will start in 20 seconds")
+ @join_timer = @bot.timer.add_once(20) {
+ start_game
+ }
+ end
+ end
+
+ def end_game
+ announce _('TODO end game')
+ @plugin.end_game(@channel)
+ end
+
+end
+
+class UnoPlugin < Plugin
+ attr :games
+ def initialize
+ super
+ @games = {}
+ end
+
+ def help(plugin, topic="")
+ (_("%{uno} game. !uno to start a game. in-game commands (no prefix): ") % {
+ :uno => UnoGame::UNO
+ }) + [
+ _("'jo' to join in"),
+ _("'pl <card>' to play <card>"),
+ _("'pe' to pick a card"),
+ _("'pa' to pass your turn"),
+ _("'co <color>' to pick a color"),
+ _("'ca' to show current cards"),
+ _("'cd' to show the current discard"),
+ _("'od' to show the playing order"),
+ _("'ti' to show play time"),
+ _("'tu' to show whose turn it is")
+ ].join(" ; ")
+ end
+
+ def message(m)
+ return unless @games.key?(m.channel)
+ g = @games[m.channel]
+ case m.plugin.intern
+ when :jo # join game
+ g.add_player(m.source)
+ when :pe # pick card
+ if g.has_turn?(m.source)
+ if g.player_has_picked
+ m.reply _("you already picked a card")
+ elsif g.picker > 0
+ m.reply _("you can't pick a card")
+ else
+ g.pick_card(m.source)
+ end
+ else
+ m.reply _("It's not your turn")
+ end
+ when :pa # pass turn
+ if g.has_turn?(m.source)
+ g.pass(m.source)
+ else
+ m.reply _("It's not your turn")
+ end
+ when :pl # play card
+ if g.has_turn?(m.source)
+ g.play_card(m.source, m.params)
+ else
+ m.reply _("It's not your turn")
+ end
+ when :co # pick color
+ if g.has_turn?(m.source)
+ g.choose_color(m.source, m.params.downcase)
+ else
+ m.reply _("It's not your turn")
+ end
+ when :ca # show current cards
+ g.show_all_cards(m.source)
+ when :cd # show current discard
+ g.show_discard
+ # TODO
+ # when :ch
+ # g.challenge
+ when :od # show playing order
+ g.show_order
+ when :ti # show play time
+ g.show_time
+ when :tu # show whose turn is it
+ if g.has_turn?(m.source)
+ m.nickreply _("it's your turn, sleepyhead")
+ else
+ g.show_turn(:cards => false)
+ end
+ end
+ 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 }
+ return
+ end
+ @games[m.channel] = UnoGame.new(self, m.channel)
+ m.reply _("Ok, created %{uno} game on %{channel}, say 'jo' to join in") % {
+ :uno => UnoGame::UNO,
+ :channel => m.channel
+ }
+ end
+
+ def end_game(channel)
+ @games.delete(channel)
+ end
+
+ def print_stock(m, p)
+ unless @games.key?(m.channel)
+ m.reply _("There is no %{uno} game running here") % { :uno => UnoGame::UNO }
+ return
+ end
+ stock = @games[m.channel].stock
+ m.reply(_("%{num} cards in stock: %{stock}") % {
+ :num => stock.length,
+ :stock => stock.join(' ')
+ }, :split_at => /#{NormalText}\s*/)
+ end
+end
+
+pg = UnoPlugin.new
+
+pg.map 'uno', :private => false, :action => :create_game
+pg.map 'uno stock', :private => false, :action => :print_stock
+pg.default_auth('stock', false)