]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/uno.rb
0b7f8ba6e76c4488adc9fb6d9c7761b3289a1798
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / uno.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Uno Game Plugin for rbot
5 #
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
7 #
8 # Copyright:: (C) 2008 Giuseppe Bilotta
9 #
10 # License:: GPL v2
11 #
12 # Uno Game: get rid of the cards you have
13
14 class UnoGame
15   COLORS = %w{Red Green Blue Yellow}
16   SPECIALS = %w{+2 Reverse Skip}
17   NUMERICS = (0..9).to_a
18   VALUES = NUMERICS + SPECIALS
19
20   def UnoGame.color_map(clr)
21     case clr
22     when 'Red'
23       :red
24     when 'Blue'
25       :royal_blue
26     when 'Green'
27       :limegreen
28     when 'Yellow'
29       :yellow
30     end
31   end
32
33   def UnoGame.irc_color_bg(clr)
34     Irc.color([:white,:black][COLORS.index(clr)%2],UnoGame.color_map(clr))
35   end
36
37   def UnoGame.irc_color_fg(clr)
38     Irc.color(UnoGame.color_map(clr))
39   end
40
41   def UnoGame.colorify(str, fg=false)
42     ret = Bold.dup
43     str.length.times do |i|
44       ret << (fg ?
45               UnoGame.irc_color_fg(COLORS[i%4]) :
46               UnoGame.irc_color_bg(COLORS[i%4]) ) +str[i,1]
47     end
48     ret << NormalText
49   end
50
51   UNO = UnoGame.colorify('UNO!', true)
52
53   # Colored play cards
54   class Card
55     attr_reader :color
56     attr_reader :value
57     attr_reader :shortform
58     attr_reader :to_s
59     attr_reader :score
60
61     def initialize(color, value)
62       raise unless COLORS.include? color
63       @color = color.dup
64       raise unless VALUES.include? value
65       if NUMERICS.include? value
66         @value = value
67         @score = value
68       else
69         @value = value.dup
70         @score = 20
71       end
72       if @value == '+2'
73         @shortform = (@color[0,1]+@value).downcase
74       else
75         @shortform = (@color[0,1]+@value.to_s[0,1]).downcase
76       end
77       @to_s = UnoGame.irc_color_bg(@color) +
78         Bold + ['', @color, @value, ''].join(' ') + NormalText
79     end
80
81     def picker
82       return 0 unless @value.to_s[0,1] == '+'
83       return @value[1,1].to_i
84     end
85
86     def special?
87       SPECIALS.include?(@value)
88     end
89
90     def <=>(other)
91       cc = self.color <=> other.color
92       if cc == 0
93         return self.value.to_s <=> other.value.to_s
94       else
95         return cc
96       end
97     end
98     include Comparable
99   end
100
101   # Wild, Wild +4 cards
102   class Wild < Card
103     def initialize(value=nil)
104       @color = 'Wild'
105       raise if value and not value == '+4'
106       if value
107         @value = value.dup 
108         @shortform = 'w'+value
109       else
110         @value = nil
111         @shortform = 'w'
112       end
113       @score = 50
114       @to_s = UnoGame.colorify(['', @color, @value, ''].compact.join(' '))
115     end
116     def special?
117       @value
118     end
119   end
120
121   class Player
122     attr_accessor :cards
123     attr_reader :user
124     def initialize(user)
125       @user = user
126       @cards = []
127     end
128     def has_card?(short)
129       cards = []
130       @cards.each { |c|
131         cards << c if c.shortform == short
132       }
133       if cards.empty?
134         return false
135       else
136         return cards
137       end
138     end
139     def to_s
140       Bold + @user.to_s + Bold
141     end
142   end
143
144   attr_reader :stock
145   attr_reader :discard
146   attr_reader :channel
147   attr :players
148   attr_reader :player_has_picked
149   attr_reader :picker
150
151   def initialize(plugin, channel)
152     @channel = channel
153     @plugin = plugin
154     @bot = plugin.bot
155     @players = []
156     @discard = nil
157     make_base_stock
158     @stock = []
159     make_stock
160     @start_time = nil
161     @join_timer = nil
162   end
163
164   def get_player(user)
165     @players.each { |p| return p if p.user == user }
166     return nil
167   end
168
169   def announce(msg, opts={})
170     @bot.say channel, msg, opts
171   end
172
173   def notify(player, msg, opts={})
174     @bot.notice player.user, msg, opts
175   end
176
177   def make_base_stock
178     @base_stock = COLORS.inject([]) do |list, clr|
179       VALUES.each do |n|
180         list << Card.new(clr, n)
181         list << Card.new(clr, n) unless n == 0
182       end
183       list
184     end
185     4.times do
186       @base_stock << Wild.new
187       @base_stock << Wild.new('+4')
188     end
189   end
190
191   def make_stock
192     @stock.replace @base_stock
193     # remove the cards in the players hand
194     @players.each { |p| p.cards.each { |c| @stock.delete_one c } }
195     # remove current top discarded card if present
196     if @discard
197       @stock.delete_one(discard)
198     end
199     @stock.shuffle!
200   end
201
202   def start_game
203     debug "Starting game"
204     @players.shuffle!
205     show_order
206     announce _("%{p} deals the first card from the stock") % {
207       :p => @players.first
208     }
209     card = @stock.shift
210     @picker = 0
211     @special = false
212     while Wild === card do
213       @stock.insert(rand(@stock.length), card)
214       card = @stock.shift
215     end
216     set_discard(card)
217     show_discard
218     if @special
219       do_special
220     end
221     next_turn
222     @start_time = Time.now
223   end
224
225   def reverse_turn
226     if @players.length > 2
227       @players.reverse!
228       # put the current player back in its place
229       @players.unshift @players.pop
230       announce _("Playing order was reversed!")
231     else
232       skip_turn
233     end
234   end
235
236   def skip_turn
237     @players << @players.shift
238     announce _("%{p} skips a turn!") % {
239       # this is first and not last because the actual
240       # turn change will be done by the following next_turn
241       :p => @players.first
242     }
243   end
244
245   def do_special
246     case @discard.value
247     when 'Reverse'
248       reverse_turn
249       @special = false
250     when 'Skip'
251       skip_turn
252       @special = false
253     end
254   end
255
256   def set_discard(card)
257     @discard = card
258     @value = card.value.dup rescue card.value
259     if Wild === card
260       @color = nil
261     else
262       @color = card.color.dup
263     end
264     if card.picker > 0
265       @picker += card.picker
266     end
267     if card.special?
268       @special = true
269     else
270       @special = false
271     end
272   end
273
274   def next_turn(opts={})
275     @players << @players.shift
276     @player_has_picked = false
277     show_turn
278   end
279
280   def can_play(card)
281     # When a +something is online, you can only play
282     # a +something of same or higher something, or a Reverse of
283     # the correct color
284     # TODO make optional
285     if @picker > 0
286       if (card.value == 'Reverse' and card.color == @color) or card.picker >= @discard.picker
287         return true
288       else
289         return false
290       end
291     else
292       # You can always play a Wild
293       # FIXME W+4 can only be played if you don't have a proper card
294       # TODO make it playable anyway, and allow players to challenge
295       return true if Wild === card
296       # On a Wild, you must match the color
297       if Wild === @discard
298         return card.color == @color
299       else
300         # Otherwise, you can match either the value or the color
301         return (card.value == @value) || (card.color == @color)
302       end
303     end
304   end
305
306   def play_card(source, cards)
307     debug "Playing card #{cards}"
308     p = get_player(source)
309     shorts = cards.scan(/[rbgy]\s*(?:\+?\d|[rs])|w\s*(?:\+4)?/)
310     debug shorts.inspect
311     if shorts.length > 2 or shorts.length < 1
312       announce _("you can only play one or two cards")
313       return
314     end
315     if shorts.length == 2 and shorts.first != shorts.last
316       announce _("you can only play two cards if they are the same")
317       return
318     end
319     if cards = p.has_card?(shorts.first)
320       debug cards
321       unless can_play(cards.first)
322         announce _("you can't play that card")
323         return
324       end
325       if cards.length >= shorts.length
326         set_discard(p.cards.delete_one(cards.shift))
327         if shorts.length > 1
328           set_discard(p.cards.delete_one(cards.shift))
329           announce _("%{p} plays %{card} twice!") % {
330             :p => source,
331             :card => @discard
332           }
333         else
334           announce _("%{p} plays %{card}") % { :p => source, :card => @discard }
335         end
336         if p.cards.length == 1
337           announce _("%{p} has %{uno}!") % {
338             :p => source, :uno => UNO
339           }
340         elsif p.cards.length == 0
341           end_game
342           return
343         end
344         show_picker
345         if @color
346           if @special
347             do_special
348           end
349           next_turn
350         else
351           announce _("%{p}, choose a color with: co r|b|g|y") % { :p => p }
352         end
353       else
354         announce _("you don't have that card")
355       end
356     end
357   end
358
359   def pass(user)
360     p = get_player(user)
361     if @picker > 0
362       announce _("%{p} passes turn, and has to pick %{b}%{n}%{b} cards!") % {
363         :p => user, :b => Bold, :n => @picker
364       }
365       deal(p, @picker)
366       @picker = 0
367     else
368       if @player_has_picked
369         announce _("%{p} passes turn") % { :p => user }
370       else
371         announce _("you need to pick a card first")
372         return
373       end
374     end
375     next_turn
376   end
377
378   def choose_color(user, color)
379     case color
380     when 'r'
381       @color = 'Red'
382     when 'b'
383       @color = 'Blue'
384     when 'g'
385       @color = 'Green'
386     when 'y'
387       @color = 'Yellow'
388     else
389       announce _('what color is that?')
390       return
391     end
392     announce _('color is now %{c}') % { :c => @color.downcase }
393     next_turn
394   end
395
396   def show_time
397     if @start_time
398       announce _("This %{uno} game has been going on for %{time}") % {
399         :uno => UNO,
400         :time => Utils.secs_to_string(Time.now - @start_time)
401       }
402     else
403       announce _("The game hasn't started yet")
404     end
405   end
406
407   def show_order
408     announce _("%{uno} playing turn: %{players}") % {
409       :uno => UNO, :players => players.join(' ')
410     }
411   end
412
413   def show_turn(opts={})
414     cards = true
415     cards = opts[:cards] if opts.key?(:cards)
416     player = @players.first
417     announce _("it's %{player}'s turn") % { :player => player }
418     show_user_cards(player) if cards
419   end
420
421   def has_turn?(source)
422     @players.first.user == source
423   end
424
425   def show_picker
426     if @picker > 0
427       announce _("next player must respond correctly or pick %{b}%{n}%{b} cards") % {
428         :b => Bold, :n => @picker
429       }
430     end
431   end
432
433   def show_discard
434     announce _("Current discard: %{card} %{c}") % { :card => @discard,
435       :c => (Wild === @discard) ? UnoGame.irc_color_bg(@color) + " #{@color} " : nil
436     }
437     show_picker
438   end
439
440   def show_user_cards(player)
441     p = Player === player ? player : get_player(player)
442     notify p, _('Your cards: %{cards}') % {
443       :cards => p.cards.join(' ')
444     }
445   end
446
447   def show_all_cards(u=nil)
448     announce(@players.inject([]) { |list, p|
449       list << [p, p.cards.length].join(': ')
450     }.join(', '))
451     if u
452       show_user_cards(u)
453     end
454   end
455
456   def pick_card(user)
457     p = get_player(user)
458     announce _("%{player} picks a card") % { :player => p }
459     deal(p, 1)
460     @player_has_picked = true
461   end
462
463   def deal(player, num=1)
464     picked = []
465     num.times do
466       picked << @stock.delete_one
467       if @stock.length == 0
468         announce _("Shuffling discarded cards")
469         make_stock
470         if @stock.length == 0
471           announce _("No more cards!")
472           end_game # FIXME nope!
473         end
474       end
475     end
476     notify player, _("You picked %{picked}") % { :picked => picked.join(' ') }
477     player.cards += picked
478     player.cards.sort!
479   end
480
481   def add_player(user)
482     return if get_player(user)
483     p = Player.new(user)
484     @players << p
485     deal(p, 7)
486     if @join_timer
487       @bot.timer.reschedule(@join_timer, 10)
488     elsif @players.length > 1
489       announce _("game will start in 20 seconds")
490       @join_timer = @bot.timer.add_once(20) {
491         start_game
492       }
493     end
494   end
495
496   def end_game
497     announce _("%{uno} game finished! The winner is %{p}") % {
498       :uno => UNO, :p => @players.first
499     }
500     if @picker > 0
501       p = @player[1]
502       announce _("%{p} has to pick %{b}%{n}%{b} cards!") % {
503         :p => p, :n => @picker, :b => Bold
504       }
505       deal(@player[1], @picker)
506       @picker = 0
507     end
508     score = @players.inject(0) do |sum, p|
509       if p.cards.length > 0
510         announce _("%{p} still had %{cards}") % {
511           :p => p, :cards => p.cards.join(' ')
512         }
513         sum += p.cards.inject(0) do |cs, c|
514           cs += c.score
515         end
516       end
517       sum
518     end
519     announce _("%{p} wins with %{b}%{score}%{b} points!") % {
520         :p => @players.first, :score => score, :b => Bold
521     }
522     @plugin.end_game(@channel)
523   end
524
525 end
526
527 class UnoPlugin < Plugin
528   attr :games
529   def initialize
530     super
531     @games = {}
532   end
533
534   def help(plugin, topic="")
535     (_("%{uno} game. !uno to start a game. in-game commands (no prefix): ") % {
536       :uno => UnoGame::UNO
537     }) + [
538       _("'jo' to join in"),
539       _("'pl <card>' to play <card>"),
540       _("'pe' to pick a card"),
541       _("'pa' to pass your turn"),
542       _("'co <color>' to pick a color"),
543       _("'ca' to show current cards"),
544       _("'cd' to show the current discard"),
545       _("'od' to show the playing order"),
546       _("'ti' to show play time"),
547       _("'tu' to show whose turn it is")
548     ].join(" ; ")
549   end
550
551   def message(m)
552     return unless @games.key?(m.channel)
553     g = @games[m.channel]
554     case m.plugin.intern
555     when :jo # join game
556       g.add_player(m.source)
557     when :pe # pick card
558       if g.has_turn?(m.source)
559         if g.player_has_picked
560           m.reply _("you already picked a card")
561         elsif g.picker > 0
562           m.reply _("you can't pick a card")
563         else
564           g.pick_card(m.source)
565         end
566       else
567         m.reply _("It's not your turn")
568       end
569     when :pa # pass turn
570       if g.has_turn?(m.source)
571         g.pass(m.source)
572       else
573         m.reply _("It's not your turn")
574       end
575     when :pl # play card
576       if g.has_turn?(m.source)
577         g.play_card(m.source, m.params.downcase)
578       else
579         m.reply _("It's not your turn")
580       end
581     when :co # pick color
582       if g.has_turn?(m.source)
583         g.choose_color(m.source, m.params.downcase)
584       else
585         m.reply _("It's not your turn")
586       end
587     when :ca # show current cards
588       g.show_all_cards(m.source)
589     when :cd # show current discard
590       g.show_discard
591     # TODO
592     # when :ch
593     #   g.challenge
594     when :od # show playing order
595       g.show_order
596     when :ti # show play time
597       g.show_time
598     when :tu # show whose turn is it
599       if g.has_turn?(m.source)
600         m.nickreply _("it's your turn, sleepyhead")
601       else
602         g.show_turn(:cards => false)
603       end
604     end
605   end
606
607   def create_game(m, p)
608     if @games.key?(m.channel)
609       m.reply _("There is already an %{uno} game running here, say 'jo' to join in") % { :uno => UnoGame::UNO }
610       return
611     end
612     @games[m.channel] = UnoGame.new(self, m.channel)
613     m.reply _("Ok, created %{uno} game on %{channel}, say 'jo' to join in") % {
614       :uno => UnoGame::UNO,
615       :channel => m.channel
616     }
617   end
618
619   def end_game(channel)
620     @games.delete(channel)
621   end
622
623   def print_stock(m, p)
624     unless @games.key?(m.channel)
625       m.reply _("There is no %{uno} game running here") % { :uno => UnoGame::UNO }
626       return
627     end
628     stock = @games[m.channel].stock
629     m.reply(_("%{num} cards in stock: %{stock}") % {
630       :num => stock.length,
631       :stock => stock.join(' ')
632     }, :split_at => /#{NormalText}\s*/)
633   end
634 end
635
636 pg = UnoPlugin.new
637
638 pg.map 'uno', :private => false, :action => :create_game
639 pg.map 'uno stock', :private => false, :action => :print_stock
640 pg.default_auth('stock', false)