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