summaryrefslogtreecommitdiff
path: root/data/rbot/plugins/shiritori.rb
diff options
context:
space:
mode:
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2007-03-16 20:48:28 +0000
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2007-03-16 20:48:28 +0000
commit2e6f388addc0bb9ddc0cb991af50220c1e5e64c3 (patch)
tree580a3d0a086c95392903df10be302186f9f3cebb /data/rbot/plugins/shiritori.rb
parent8669de30cea27aee0116bb4420cbf15232c8c352 (diff)
Plugins: move games into their own directory
Diffstat (limited to 'data/rbot/plugins/shiritori.rb')
-rw-r--r--data/rbot/plugins/shiritori.rb448
1 files changed, 0 insertions, 448 deletions
diff --git a/data/rbot/plugins/shiritori.rb b/data/rbot/plugins/shiritori.rb
deleted file mode 100644
index f13afeb8..00000000
--- a/data/rbot/plugins/shiritori.rb
+++ /dev/null
@@ -1,448 +0,0 @@
-#-- vim:sw=2:et
-#kate: indent-width 2
-#++
-#
-# :title: Shiritori Plugin for RBot
-#
-# Author:: Yaohan Chen <yaohan.chen@gmail.com>
-# Copyright:: (c) 2007 Yaohan Chen
-# License:: GNU Public License
-#
-#
-# Shiritori is a word game where a few people take turns to continue a chain of words.
-# To continue a word, the next word must start with the ending of the previous word,
-# usually defined as the one to few letters/characters at the end. This plugin allows
-# playing several games, each per channel. A game can be turn-based, where only new
-# players can interrupt a turn to join, or a free mode where anyone can speak at any
-# time.
-#
-# TODO
-# * a system to describe settings, so they can be displayed, changed and saved
-# * adjust settings during game
-# * allow other definitions of continues?
-# * read default settings from configuration
-# * keep statistics
-# * other forms of dictionaries
-
-
-# Abstract class representing a dictionary used by Shiritori
-class Dictionary
- # whether string s is a word
- def has_word?(s)
- raise NotImplementedError
- end
-
- # whether any word starts with prefix, excluding words in excludes. This can be
- # possible with non-enumerable dictionaries since some dictionary engines provide
- # prefix searching.
- def any_word_starting?(prefix, excludes)
- raise NotImplementedError
- end
-end
-
-# A Dictionary that uses a enumrable word list.
-class WordlistDictionary < Dictionary
- def initialize(words)
- super()
- @words = words
- debug "Created dictionary with #{@words.length} words"
- end
-
- # whether string s is a word
- def has_word?(s)
- @words.include? s
- end
-
- # whether any word starts with prefix, excluding words in excludes
- def any_word_starting?(prefix, excludes)
- # (@words - except).any? {|w| w =~ /\A#{prefix}.+/}
- # this seems to be faster:
- !(@words.grep(/\A#{prefix}.+/) - excludes).empty?
- end
-end
-
-# Logic of shiritori game, deals with checking whether words continue the chain, and
-# whether it's possible to continue a word
-class Shiritori
- attr_reader :used_words
-
- # dictionary:: a Dictionary object
- # overlap_lengths:: a Range for allowed lengths to overlap when continuing words
- # check_continuable:: whether all words are checked whether they're continuable,
- # before being commited
- # allow_reuse:: whether words are allowed to be used again
- def initialize(dictionary, overlap_lengths, check_continuable, allow_reuse)
- @dictionary = dictionary
- @overlap_lengths = overlap_lengths
- @check_continuable = check_continuable
- @allow_reuse = allow_reuse
- @used_words = []
- end
-
- # Prefix of s with length n
- def head_of(s, n)
- # TODO ruby2 unicode
- s.split(//u)[0, n].join
- end
- # Suffix of s with length n
- def tail_of(s, n)
- # TODO ruby2 unicode
- s.split(//u)[-n, n].join
- end
- # Number of unicode characters in string
- def len(s)
- # TODO ruby2 unicode
- s.split(//u).length
- end
- # return subrange of range r that's under n
- def range_under(r, n)
- r.begin .. [r.end, n-1].min
- end
-
- # TODO allow the ruleset to customize this
- def continues?(w2, w1)
- # this uses the definition w1[-n,n] == w2[0,n] && n < [w1.length, w2.length].min
- # TODO it might be worth allowing <= for the second clause
- range_under(@overlap_lengths, [len(w1), len(w2)].min).any? {|n|
- tail_of(w1, n)== head_of(w2, n)}
- end
-
- # Checks whether *any* unused word in the dictionary completes the word
- # This has the limitation that it can't detect when a word is continuable, but the
- # only continuers aren't continuable
- def continuable_from?(s)
- range_under(@overlap_lengths, len(s)).any? {|n|
- @dictionary.any_word_starting?(tail_of(s, n), @used_words) }
- end
-
- # Given a string, give a verdict based on current shiritori state and dictionary
- def process(s)
- # TODO optionally allow used words
- # TODO ruby2 unicode
- if len(s) < @overlap_lengths.min || !@dictionary.has_word?(s)
- debug "#{s} is too short or not in dictionary"
- :ignore
- elsif @used_words.empty?
- if !@check_continuable || continuable_from?(s)
- @used_words << s
- :start
- else
- :start_end
- end
- elsif continues?(s, @used_words.last)
- if !@allow_reuse && @used_words.include?(s)
- :used
- elsif !@check_continuable || continuable_from?(s)
- @used_words << s
- :next
- else
- :end
- end
- else
- :ignore
- end
- end
-end
-
-# A shiritori game on a channel. keeps track of rules related to timing and turns,
-# and interacts with players
-class ShiritoriGame
- # timer:: the bot.timer object
- # say:: a Proc which says the given message on the channel
- # when_die:: a Proc that removes the game from plugin's list of games
- def initialize(channel, ruleset, timer, say, when_die)
- raise ArgumentError unless [:words, :overlap_lengths, :check_continuable,
- :end_when_uncontinuable, :allow_reuse, :listen, :normalize, :time_limit,
- :lose_when_timeout].all? {|r| ruleset.has_key?(r)}
- @last_word = nil
- @players = []
- @booted_players = []
- @ruleset = ruleset
- @channel = channel
- @timer = timer
- @timer_handle = nil
- @say = say
- @when_die = when_die
-
- # TODO allow other forms of dictionaries
- dictionary = WordlistDictionary.new(@ruleset[:words])
- @game = Shiritori.new(dictionary, @ruleset[:overlap_lengths],
- @ruleset[:check_continuable],
- @ruleset[:allow_reuse])
- end
-
- # Whether the players must take turns
- # * when there is only one player, turns are not enforced
- # * when time_limit > 0, new players can join at any time, but existing players must
- # take turns, each of which expires after time_limit
- # * when time_imit is 0, anyone can speak in the game at any time
- def take_turns?
- @players.length > 1 && @ruleset[:time_limit] > 0
- end
-
- # the player who has the current turn
- def current_player
- @players.first
- end
- # the word to continue in the current turn
- def current_word
- @game.used_words[-1]
- end
- # the word in the chain before current_word
- def previous_word
- @game.used_words[-2]
- end
-
- # announce the current word, and player if take_turns?
- def announce
- if take_turns?
- @say.call "#{current_player}, it's your turn. #{previous_word} -> #{current_word}"
- elsif @players.empty?
- @say.call "No one has given the first word yet. Say the first word to start."
- else
- @say.call "Poor #{current_player} is playing alone! Anyone care to join? #{previous_word} -> #{current_word}"
- end
- end
- # create/reschedule timer
- def restart_timer
- # the first time the method is called, a new timer is added
- @timer_handle = @timer.add(@ruleset[:time_limit]) {time_out}
- # afterwards, it will reschdule the timer
- instance_eval do
- def restart_timer
- @timer.reschedule(@timer_handle, @ruleset[:time_limit])
- end
- end
- end
- # switch to the next player's turn if take_turns?, and announce current words
- def next_player
- # when there's only one player, turns and timer are meaningless
- if take_turns?
- # place the current player to the last position, to implement circular queue
- @players << @players.shift
- # stop previous timer and set time for this turn
- restart_timer
- end
- announce
- end
-
- # handle when turn time limit goes out
- def time_out
- if @ruleset[:lose_when_timeout]
- @say.call "#{current_player} took too long and is out of the game. Try again next game!"
- if @players.length == 2
- # 2 players before, and one should remain now
- # since the game is ending, save the trouble of removing and booting the player
- @say.call "#{@players[1]} is the last remaining player and the winner! Congratulations!"
- die
- else
- @booted_players << @players.shift
- announce
- end
- else
- @say.call "#{current_player} took too long and skipped the turn."
- next_player
- end
- end
-
- # change the rules, and update state when necessary
- def change_rules(rules)
- @ruleset.update! rules
- end
-
- # handle a message to @channel
- def handle_message(m)
- message = m.message
- speaker = m.sourcenick.to_s
-
- return unless @ruleset[:listen] =~ message
-
- # in take_turns mode, only new players are allowed to interrupt a turn
- return if @booted_players.include? speaker ||
- (take_turns? &&
- speaker != current_player &&
- (@players.length > 1 && @players.include?(speaker)))
-
- # let Shiritori process the message, and act according to result
- case @game.process @ruleset[:normalize].call(message)
- when :start
- @players << speaker
- m.reply "#{speaker} has given the first word: #{current_word}"
- when :next
- if !@players.include?(speaker)
- # A new player
- @players.unshift speaker
- m.reply "Welcome to shiritori, #{speaker}."
- end
- next_player
- when :used
- m.reply "The word #{message} has been used. Retry from #{current_word}"
- when :end
- # TODO respect shiritori.end_when_uncontinuable setting
- if @ruleset[:end_when_uncontinuable]
- m.reply "It's impossible to continue the chain from #{message}. The game has ended. Thanks a lot, #{speaker}! :("
- die
- else
- m.reply "It's impossible to continue the chain from #{message}. Retry from #{current_word}"
- end
- when :start_end
- # when the first word is uncontinuable, the game doesn't stop, as presumably
- # someone wanted to play
- m.reply "It's impossible to continue the chain from #{message}. Start with another word."
- end
- end
-
- # end the game
- def die
- # redefine restart_timer to no-op
- instance_eval do
- def restart_timer
- end
- end
- # remove any registered timer
- @timer.remove @timer_handle unless @timer_handle.nil?
- # should remove the game object from plugin's @games list
- @when_die.call
- end
-end
-
-# shiritori plugin for rbot
-class ShiritoriPlugin < Plugin
- def help(plugin, topic="")
- "A game in which each player must continue the previous player's word, by using its last one or few characters/letters of the word to start a new word. 'shiritori <ruleset>' => Play shiritori with a set of rules. Available rulesets: #{@rulesets.keys.join ', '}. 'shiritori stop' => Stop the current shiritori game."
- end
-
- def initialize()
- super
- @games = {}
-
- # TODO make rulesets more easily customizable
- # TODO initialize default ruleset from config
- # Default values of rulesets
- @default_ruleset = {
- # the range of the length of "tail" that must be followed to continue the chain
- :overlap_lengths => 1..2,
- # messages cared about, pre-normalize
- :listen => /\A\S+\Z/u,
- # normalize messages with this function before checking with Shiritori
- :normalize => lambda {|w| w},
- # number of seconds for each player's turn
- :time_limit => 60,
- # when the time limit is reached, the player's booted out of the game and cannot
- # join until the next game
- :lose_when_timeout => true,
- # check whether the word is continuable before adding it into chain
- :check_continuable => true,
- # allow reusing used words
- :allow_reuse => false,
- # end the game when an uncontinuable word is said
- :end_when_uncontinuable => true
- }
- @rulesets = {
- 'english' => {
- :wordlist_file => 'english',
- :listen => /\A[a-zA-Z]+\Z/,
- :overlap_lengths => 2..5,
- :normalize => lambda {|w| w.downcase},
- :desc => 'Use English words; case insensitive; 2-6 letters at the beginning of the next word must overlap with those at the end of the previous word.'
- },
- 'japanese' => {
- :wordlist_file => 'japanese',
- :listen => /\A\S+\Z/u,
- :overlap_lengths => 1..4,
- :desc => 'Use Japanese words in hiragana; 1-4 kana at the beginning of the next word must overlap with those at the end of the previous word.',
- # Optionally use a module to normalize Japanese words, enabling input in multiple writing systems
- }
- }
- @rulesets.each_value do |ruleset|
- # set default values for each rule to default_ruleset's values
- ruleset.replace @default_ruleset.merge(ruleset)
- unless ruleset.has_key?(:words)
- if ruleset.has_key?(:wordlist_file)
- # TODO read words only when rule is used
- # read words separated by newlines from file
- ruleset[:words] =
- File.new("#{@bot.botclass}/shiritori/#{ruleset[:wordlist_file]}").grep(
- ruleset[:listen]) {|l| ruleset[:normalize].call l.chomp}
- else
- raise NotImplementedError
- end
- end
- end
- end
-
- # start shiritori in a channel
- def cmd_shiritori(m, params)
- if @games.has_key?( m.channel )
- m.reply "Already playing shiritori here"
- @games[m.channel].announce
- else
- if @rulesets.has_key? params[:ruleset]
- @games[m.channel] = ShiritoriGame.new(
- m.channel, @rulesets[params[:ruleset]],
- @bot.timer,
- lambda {|msg| m.reply msg},
- lambda {remove_game m.channel} )
- m.reply "Shiritori has started. Please say the first word"
- else
- m.reply "There is no defined ruleset named #{params[:ruleset]}"
- end
- end
- end
-
- # change rules for current game
- def cmd_set(m, params)
- require 'enumerator'
- new_rules = {}
- params[:rules].each_slice(2) {|opt, value| new_rules[opt] = value}
- raise NotImplementedError
- end
-
- # stop the current game
- def cmd_stop(m, params)
- if @games.has_key? m.channel
- # TODO display statistics
- @games[m.channel].die
- m.reply "Shiritori has stopped. Hope you had fun!"
- else
- # TODO display statistics
- m.reply "No game to stop here, because no game is being played."
- end
- end
-
- # remove the game, so channel messages are no longer processed, and timer removed
- def remove_game(channel)
- @games.delete channel
- end
-
- # all messages from a channel is sent to its shiritori game if any
- def listen(m)
- return unless m.kind_of?(PrivMessage)
- return unless @games.has_key?(m.channel)
- # send the message to the game in the channel to handle it
- @games[m.channel].handle_message m
- end
-
- # remove all games
- def cleanup
- @games.each_key {|g| g.die}
- @games.clear
- end
-end
-
-plugin = ShiritoriPlugin.new
-plugin.default_auth( 'edit', false )
-
-# Normal commandsi have a stop_gamei have a stop_game
-plugin.map 'shiritori stop',
- :action => 'cmd_stop',
- :private => false
-# plugin.map 'shiritori set ',
-# :action => 'cmd_set'
-# :private => false
-# plugin.map 'shiritori challenge',
-# :action => 'cmd_challenge'
-plugin.map 'shiritori [:ruleset]',
- :action => 'cmd_shiritori',
- :defaults => {:ruleset => 'japanese'},
- :private => false