├── .gitignore ├── public ├── log.yml └── setting.yml ├── lingr_reudy.rb ├── lib └── reudy │ ├── reudy_common.rb │ ├── pstore.rb │ ├── gdbm.rb │ ├── tango-mecab.rb │ ├── word_searcher.rb │ ├── word_associator.rb │ ├── thought_view.rb │ ├── wtml_manager.rb │ ├── message_log.rb │ ├── wordset.rb │ ├── attention_decider.rb │ ├── response_estimator.rb │ ├── similar_searcher.rb │ ├── michiru.rb │ ├── tango-mgm.rb │ ├── bot_irc_client.rb │ ├── irc-client.rb │ └── reudy.rb ├── irc_reudy.rb ├── stdio_reudy.rb ├── README.mkd ├── manual ├── style.css ├── reudywin.html └── reudyman.html ├── twitter_reudy.rb └── LICENCE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | public/* 2 | -------------------------------------------------------------------------------- /public/log.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :body: こんにちは。 3 | :fromNick: Default 4 | --- 5 | :body: おはよう。 6 | :fromNick: Default 7 | --- 8 | :body: こんばんは。 9 | :fromNick: Default 10 | --- 11 | :body: 何? 12 | :fromNick: Default 13 | --- 14 | :body: 不確定だ。 15 | :fromNick: Default 16 | --- 17 | :body: どうして? 18 | :fromNick: Default 19 | --- 20 | :body: なんでやねん。 21 | :fromNick: Default 22 | -------------------------------------------------------------------------------- /lingr_reudy.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2011 Glass_saga 2 | $REUDY_DIR= "./lib/reudy" unless defined?($REUDY_DIR) 3 | 4 | require 'sinatra' 5 | require 'json' 6 | require $REUDY_DIR+'/reudy' 7 | 8 | class Lingr 9 | def initialize 10 | @message = "" 11 | end 12 | 13 | def speak(n) 14 | @message = n 15 | end 16 | 17 | attr_accessor :message 18 | end 19 | 20 | include Gimite 21 | 22 | reudy = Reudy.new("public") 23 | lingr = Lingr.new 24 | reudy.client = lingr 25 | 26 | post '/' do 27 | content_type :text 28 | data = JSON.parse(params[:json]) 29 | reudy.onOtherSpeak(data["events"][0]["message"]["nickname"],data["events"][0]["message"]["text"]) 30 | return lingr.message 31 | end 32 | -------------------------------------------------------------------------------- /lib/reudy/reudy_common.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | module Gimite 5 | #デバッグ出力 6 | def dprint(caption, *objs) 7 | objs.map!(&:inspect) 8 | warn("#{caption}: #{objs.join("/")}") 9 | end 10 | 11 | #contの全ての要素に対してpredが真を返すか。 12 | def for_all?(cont) 13 | cont.all?{|item| yield(item) } 14 | end 15 | 16 | #contの中にpredが真を返す要素が存在するか。 17 | def there_exists?(cont) 18 | cont.any?{|item| yield(item) } 19 | end 20 | 21 | def sigma(range) 22 | sum = nil 23 | range.each do |v| 24 | sum = sum ? sum + yield(v) : yield(v) 25 | end 26 | sum 27 | end 28 | 29 | module_function(:dprint, :for_all?, :there_exists?, :sigma) 30 | end 31 | -------------------------------------------------------------------------------- /lib/reudy/pstore.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2011 Glass_saga 2 | require 'pstore' 3 | 4 | module Gimite 5 | class DB 6 | def initialize(filename) 7 | @db = PStore.new(filename) 8 | end 9 | 10 | def [](key) 11 | @db.transaction do 12 | return @db[key] 13 | end 14 | end 15 | 16 | def []=(key, value) 17 | @db.transaction do 18 | @db[key] = value 19 | end 20 | end 21 | 22 | def keys 23 | @db.transaction { @db.roots } 24 | end 25 | 26 | def empty? 27 | @db.transaction { @db.roots.empty? } 28 | end 29 | 30 | def clear 31 | @db.transaction do 32 | @db.roots.each {|key| @db.delete(key) } 33 | end 34 | end 35 | 36 | def close; end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/reudy/gdbm.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | require 'gdbm' 5 | 6 | module Gimite 7 | #値が文字列以外でもOKなGDBM(手抜き) 8 | class DB 9 | def initialize(file_name) 10 | @gdbm = GDBM.new(file_name, 0666, GDBM::FAST) 11 | end 12 | 13 | def [](key) 14 | str = @gdbm[key] 15 | return str && Marshal.load(str).freeze 16 | #オブジェクトの中身を変更されてもDBに反映できないので、freeze()しておく 17 | end 18 | 19 | def []=(key, value) 20 | @gdbm[key] = Marshal.dump(value) 21 | end 22 | 23 | def keys 24 | @gdbm.keys 25 | end 26 | 27 | def empty? 28 | @gdbm.empty? 29 | end 30 | 31 | def clear 32 | @gdbm.clear 33 | end 34 | 35 | def close 36 | @gdbm.close 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /irc_reudy.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | $REUDY_DIR= "./lib/reudy" unless defined?($REUDY_DIR) 5 | 6 | require 'optparse' 7 | require $REUDY_DIR+'/bot_irc_client' 8 | require $REUDY_DIR+'/reudy' 9 | 10 | module Gimite 11 | STDOUT.sync = true 12 | STDERR.sync = true 13 | Thread.abort_on_exception = true 14 | 15 | opt = OptionParser.new 16 | 17 | directory = 'public' 18 | opt.on('-d DIRECTORY') { |v| directory = v; p v } 19 | 20 | db = 'pstore' 21 | opt.on('--db DB_TYPE') { |v| db = v } 22 | 23 | mecab = nil 24 | opt.on('-m','--mecab') { |v| mecab = true } 25 | 26 | opt.parse!(ARGV) 27 | directory = ARGV.first unless ARGV.empty? 28 | 29 | begin 30 | #IRC用ロイディを作成 31 | client= BotIRCClient.new(Reudy.new(directory,{},db,mecab)) 32 | client.processLoop 33 | rescue Interrupt 34 | #割り込み発生。 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/reudy/tango-mecab.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2011 Glass_saga 3 | 4 | require 'MeCab' 5 | 6 | class WordExtractor 7 | # WordExtractor(単語候補リストを保持する長さ,単語追加時のコールバック) 8 | 9 | POS_ID = [38,41,42,43,44,45,46,47] #単語として扱う品詞リスト 10 | 11 | def initialize(candlistlength=7,onaddword=nil) 12 | @onAddWord = onaddword 13 | @m = MeCab::Tagger.new 14 | end 15 | 16 | # 文中で使われている単語を取得 17 | def extractWords(line,words=[]) 18 | n = @m.parseToNode(line) 19 | 20 | while n = n.next do 21 | words << n.surface.force_encoding(Encoding::UTF_8) if !n.surface.empty? && POS_ID.include?(n.posid) 22 | end 23 | 24 | if @onAddWord 25 | words.each do |w| 26 | @onAddWord.call(w) 27 | end 28 | end 29 | 30 | return words 31 | end 32 | 33 | # 単語取得・単語候補リスト更新を1行分処理する 34 | def processLine(line) 35 | words = extractWords(line) 36 | return words 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/reudy/word_searcher.rb: -------------------------------------------------------------------------------- 1 | #ncoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | require $REUDY_DIR+'/wordset' 6 | 7 | KANA_AN = Regexp.compile("([ァ-ンー−][ァ-ンー−]|[a-zA-Z][a-zA-Z])") # #カタカナ又は英数 8 | 9 | module Gimite 10 | #文中から既知の単語を探す 11 | class WordSearcher 12 | include Gimite 13 | 14 | def initialize(wordSet) 15 | @wordSet = wordSet 16 | end 17 | 18 | #文章がその単語を含んでいるか 19 | def hasWord(sentence, word) 20 | return false unless sentence.include?(word.str) 21 | return false unless sentence =~ /(.|^)#{Regexp.escape(word.str)}(.|$)/ 22 | return false if ("#{$1}#{word.str[0]}") =~ KANA_AN || ("#{word.str[-1]}#{$2}") =~ KANA_AN #カタカナ列や英文字列を途中で切るような単語は不可 23 | return true 24 | end 25 | 26 | #文中から既知の単語を探す 27 | def searchWords(sentence) 28 | @wordSet.words.select{|word| hasWord(sentence, word) } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/reudy/word_associator.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | module Gimite 5 | #単語連想器 6 | class WordAssociator 7 | def initialize(file_name) 8 | @file_name = file_name 9 | @assoc_word_map = {} 10 | loadFromFile 11 | end 12 | 13 | def loadFromFile 14 | if File.exists?(@file_name) 15 | File.open(@file_name) do |file| 16 | file.each_line do |line| 17 | line.chomp! 18 | strs = line.split(/\t/) 19 | @assoc_word_map[strs.first] = strs[1..-1] if strs.size >= 2 20 | end 21 | end 22 | end 23 | end 24 | 25 | #1単語から連想された1単語を返す 26 | def associate(word_str) 27 | if strs = @assoc_word_map[word_str] 28 | strs.sample 29 | else 30 | nil 31 | end 32 | end 33 | 34 | #1単語から連想された全ての単語を返す 35 | def associateAll(word_str) 36 | @assoc_word_map[word_str] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/reudy/thought_view.rb: -------------------------------------------------------------------------------- 1 | $REUDY_DIR= "." unless defined?($REUDY_DIR) 2 | 3 | require "./message_log" 4 | require "ostruct" 5 | require "erb" 6 | 7 | module Gimite 8 | log = MessageLog.new(ARGV[0] + "/log.dat") 9 | 10 | data = [] 11 | File.open(ARGV[0] + "/thought.txt") do |f| 12 | f.each_line do |line| 13 | line.chomp! 14 | fields = line.split(/\t/) 15 | r = OpenStruct.new 16 | (r.input_mid, r.pattern, r.sim_mid, r.res_mid) = fields[0...4].map(&:to_i) 17 | (r.words_str, r.output) = fields[4...6] 18 | r.input = log[r.input_mid].body 19 | r.messages = [] 20 | (r.sim_mid...r.sim_mid+6).each do |mid| 21 | m = OpenStruct.new 22 | m.nick = log[mid].fromNick 23 | m.body = log[mid].body 24 | m.is_sim = mid == r.sim_mid 25 | m.is_res = mid == r.res_mid 26 | r.messages.push(m) 27 | end 28 | data.push(r) 29 | end 30 | end 31 | 32 | extend(ERB::Util) 33 | 34 | template = File.open("thought_view.html").read 35 | ERB.new(template).run(binding) 36 | end 37 | -------------------------------------------------------------------------------- /lib/reudy/wtml_manager.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | require $REUDY_DIR+'/wordset' 5 | require $REUDY_DIR+'/message_log' 6 | require $REUDY_DIR+'/word_searcher' 7 | 8 | module Gimite 9 | #「単語→発言番号」リストを管理するもの。 10 | class WordToMessageListManager 11 | def initialize(wordSet, log, wordSearcher) 12 | @wordSet = wordSet 13 | @log = log 14 | @wordSearcher = wordSearcher 15 | @log.addObserver(self) 16 | end 17 | 18 | def onAddMsg 19 | msgN = @log.size - 1 20 | @wordSearcher.searchWords(@log[msgN].body).each do |word| 21 | word.mids.push(msgN) 22 | end 23 | end 24 | 25 | def onClearLog 26 | @wordSet.words.each do |word| 27 | word.mids.clear 28 | end 29 | end 30 | 31 | #単語wordにmidsを付ける。 32 | def attachMsgList(word) 33 | word.mids = [] 34 | @log.each_with_index do |log, i| 35 | word.mids.push(i) if @wordSearcher.hasWord(log.body, word) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /public/setting.yml: -------------------------------------------------------------------------------- 1 | # このファイルの書き方について詳しくは 2 | # http://rogiken.org/SSB/reudyman.html#setting 3 | # を見てください。 4 | 5 | # 1. IRCに接続する場合は、接続先のホスト名、ポート番号、文字コード、チャンネルを書いてください。 6 | 7 | :host: irc.ircnet.ne.jp 8 | :port: 6668 9 | # Reudy1.9の内部ではここに入力された文字列でEncoding.findします。 10 | # UTF-8を指定する場合は"UTF-8"として下さい。その他のエンコーディングに関しては、irbなどでEncoding.name_listを参照して下さい。 11 | :encoding: ISO-2022-JP 12 | :channel: "#reudy_test" 13 | :info_channel: "#reudy_test" 14 | 15 | # 2. IRCのサーバ/チャンネルに入るのにパスワードが必要な場合は、指定してください。 16 | 17 | :login_password: 18 | :channel_key: 19 | 20 | # 3. 以下の項目は、変更しなくてもとりあえず動きます。必要に応じていじってください。 21 | 22 | :nick: ReudyTest 23 | :nicks: 24 | - Reudy 25 | - reudy 26 | - ロイディ 27 | :default_mode: 2 28 | :joining_message: こんにちは。 29 | :leaving_message: さようなら。 30 | :private_greeting: 31 | :disable_commands: false 32 | :disable_studying: false 33 | :respond_to_notice: false 34 | :respond_to_external: false 35 | :speak_with_privmsg: false 36 | :auto_reconnect: true 37 | :target_nick: 38 | :forbidden_nick: 39 | :wait_before_speak: 0 40 | :wait_before_info: 0.2 41 | :name: Reudy1.9 42 | :real_name: Bot Reudy 43 | :twitter: 44 | :key: "consumer key" 45 | :secret: "consumer secret" 46 | -------------------------------------------------------------------------------- /stdio_reudy.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | #日本語文字コード判定用コメント 5 | 6 | $OUT_KCODE= "UTF-8" #出力文字コード 7 | $REUDY_DIR= "./lib/reudy" unless defined?($REUDY_DIR) 8 | 9 | require 'optparse' 10 | require $REUDY_DIR+'/bot_irc_client' 11 | require $REUDY_DIR+'/reudy' 12 | require $REUDY_DIR+'/reudy_common' 13 | 14 | trap(:INT){ exit } 15 | 16 | module Gimite 17 | 18 | class StdioClient 19 | 20 | include(Gimite) 21 | 22 | def initialize(user, yourNick) 23 | @user = user 24 | @user.client = self 25 | @yourNick = yourNick 26 | greeting = @user.settings["joining_message"] 27 | puts greeting if greeting 28 | end 29 | 30 | def loop 31 | STDIN.each_line do |line| 32 | line = line.chomp 33 | if line.empty? 34 | @user.onSilent 35 | elsif @yourNick 36 | @user.onOtherSpeak(@yourNick, line) 37 | elsif line =~ /^(.+?) (.*)$/ 38 | @user.onOtherSpeak($1, $2) 39 | else 40 | $stderr.print("Error\n") 41 | end 42 | end 43 | end 44 | 45 | #補助情報を出力 46 | def outputInfo(s) 47 | puts "(#{s})" 48 | end 49 | 50 | #発言する 51 | def speak(s) 52 | puts s 53 | end 54 | end 55 | 56 | opt = OptionParser.new 57 | 58 | directory = 'public' 59 | opt.on('-d DIRECTORY') do |v| 60 | directory = v 61 | end 62 | 63 | db = 'pstore' 64 | opt.on('--db DB_TYPE') do |v| 65 | db = v 66 | end 67 | 68 | nick = 'test' 69 | opt.on('-n nickname') do |v| 70 | nick = v 71 | end 72 | 73 | mecab = nil 74 | opt.on('-m','--mecab') do |v| 75 | mecab = true 76 | end 77 | 78 | opt.parse!(ARGV) 79 | 80 | STDOUT.sync = true 81 | client = StdioClient.new(Reudy.new(directory,{},db,mecab),nick) #標準入出力用ロイディを作成 82 | client.loop 83 | 84 | end 85 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | # Reudy on Ruby1.9 2 | 3 | ## 改変した人 4 | 5 | * Glass_saga 6 | 7 | ## 説明 8 | 9 | 東京工業大学 ロボット技術研究会の市川 宙(Gimite)さんが中心となって作成した日本語人工無脳ロイディを、Ruby1.9で動作するようにしたものです。 10 | Ruby1.8では動作しません。 11 | 12 | ## 使い方 13 | 14 | ### IRCボットとして使う場合 15 | publicフォルダにあるsetting.ymlを編集してから 16 | >ruby irc_reudy.rb 17 | 18 | でIRCクライアントが立ち上がります。 19 |   20 | ### Twitterボットとして使う場合 21 | 動作にはrubytterとhighlineが必要です 22 | >gem install rubytter highline 23 | 24 | でインストールして下さい。 25 | 26 | http://dev.twitter.com/apps/new から新しいアプリを作成して、取得したConsumer key/secretをtwitter_reudy.rbに記入して使って下さい。
27 | 初回にアクセストークンを取得する為のURLが示されるので、
28 | そのURLにアクセスしてAllowをクリックし、表示された番号(PIN)を入力して下さい。
29 | アクセストークンはtwitter_reudy.rbと同じフォルダに「token」というファイル名で保存されます。
30 | 31 | ### Lingrボットとして使う場合 32 | 動作にはSinatraが必要です。 33 | >gem install sinatra 34 | 35 | でインストールして下さい。 36 | 37 | デフォルトではWEBrickが4567番ポートで動作するはずです。
38 | Lingrにアクセスし、SettingsのBotsからcreate a new botをクリックして、Callback URLにWEBrickへ到達可能なURLを指定して下さい。
39 | 40 | 41 | ## 改変の内容 42 | 43 | ### コード 44 | 45 | ソースコードをできる限り[Rubyコーディング規約](http://shugo.net/ruby-codeconv/codeconv.html)に沿うように改変しました。 46 | 47 | ### DBM周りの変更 48 | 49 | 本家ロイディは文尾辞書の保持にRuby/GDBMを使用していましたが、PStoreをデフォルトのDBMとしました。
50 | また、[],[]=などの数個のメソッドを定義したrbファイルを作成して lib/reudy/以下に置くだけで、利用出来るDBMを簡単に追加できるようになっています。 51 | 52 | ### Twitter用インターフェースの追加 53 | 54 | twitter_reudy.rbというTwitter用インターフェースを追加しています。 55 | 56 | ### Lingr用インターフェースの更新 57 | 58 | Lingrの仕様変更に対応しました。 59 | 60 | ### コマンドラインオプションの解釈 61 | コマンドラインオプションのパースにOptionParserを使うようになりました。 62 | 63 | ### 単語の抽出にMeCabを使えるように 64 | 単語の抽出に形態素解析エンジン[MeCab](http://mecab.sourceforge.net/)を利用できるようになりました。 65 | デフォルトでは本家ロイディと同じく正規表現によって単語の抽出を試みますが、コマンドラインオプション-m又は--mecabを付けて起動すると 66 | 単語の抽出にMeCabを使用します。 67 | 68 | MeCabを使用する為にはMeCabとMeCabから利用可能な辞書、MeCabのRuby用バインディングmecab-rubyが必要です。 69 | -------------------------------------------------------------------------------- /lib/reudy/message_log.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | require $REUDY_DIR+'/reudy_common' 5 | require "psych" #UTF-8をバイナリで書き出さないようにする 6 | require "yaml" 7 | 8 | module Gimite 9 | #個々の発言 10 | class Message 11 | def initialize(from_nick, body) 12 | @fromNick = from_nick 13 | @body = body 14 | end 15 | 16 | attr_accessor :fromNick,:body 17 | end 18 | 19 | #発言ログ 20 | class MessageLog 21 | include Gimite 22 | 23 | def initialize(inner_filename) 24 | @innerFileName = inner_filename 25 | @observers = [] 26 | File.open(inner_filename) do |f| 27 | @size = f.lines("\n---").count 28 | end 29 | end 30 | 31 | attr_accessor :size 32 | 33 | #観察者を追加。 34 | def addObserver(*observers) 35 | @observers.concat(observers) 36 | end 37 | 38 | #n番目の発言 39 | def [](n) 40 | n += @size if n < 0 #末尾からのインデックス 41 | File.open(@innerFileName) do |f| 42 | line = f.lines("\n---").find{ f.lineno > n } 43 | if line && line != "\n---" 44 | m = YAML.load(line) 45 | return Message.new(m[:fromNick], m[:body]) 46 | else 47 | return nil 48 | end 49 | end 50 | end 51 | 52 | #発言を追加 53 | def addMsg(from_nick, body, to_outer = true) 54 | File.open(@innerFileName, "a") do |f| 55 | YAML.dump({:fromNick => from_nick, :body => body}, f) 56 | end 57 | @size += 1 58 | @observers.each(&:onAddMsg) 59 | end 60 | 61 | private 62 | 63 | #内部データをクリア(デフォルトのログのみ残す) 64 | def clear 65 | File.open(@innerFileName, "w") do |f| 66 | default = f.lines("\n---").select{|s| YAML.load(s)[:fromNick] == "Default" } 67 | f.rewind 68 | f.puts default.join 69 | f.truncate(f.size) 70 | @size = default.size 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /manual/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | color: black; 4 | font-size: 10pt; 5 | font-family: verdana, arial, helvetica, Sans-Serif; 6 | } 7 | 8 | table { 9 | font-size: 10pt; 10 | } 11 | 12 | h1 { 13 | font-family: verdana, sans-serif; 14 | border-top: solid 2pt; 15 | border-bottom: solid 2pt; 16 | border-color: #AAAADD; 17 | padding: 3pt; 18 | margin-top: 10; 19 | margin-right: 0pt; 20 | margin-left: 0pt; 21 | color: darkblue; 22 | background-color: #EEEEFF 23 | } 24 | 25 | h2, h3, h4, h5, h6 { 26 | font-family: verdana, arial, helvetica, Sans-Serif; 27 | } 28 | 29 | h2 { 30 | font-weight: bold; 31 | border-bottom: solid thin; 32 | border-left: solid 20pt; 33 | border-color: #AAAADD; 34 | padding: 1pt; 35 | padding-left:20pt; 36 | margin-left:5pt; 37 | margin-right:0pt; 38 | color: black; 39 | font-weight: bold; 40 | background-color: #EEEEFF 41 | } 42 | 43 | h3 { 44 | margin-top: 20pt; 45 | margin-left: 10pt; 46 | color: black; 47 | font-weight: bold; 48 | text-decoration: underline; 49 | } 50 | 51 | div.header p.status { text-align: right; } 52 | div.header p.last-modified { text-align: left; } 53 | 54 | dt { 55 | font-weight: bold; 56 | margin-top: 2ex; 57 | margin-left: 1em; 58 | } 59 | 60 | pre { 61 | padding: 10pt; 62 | margin: 10pt; 63 | border: #646464 1px solid; 64 | background-color: #f5f5f5; 65 | color:darkblue; 66 | font-family: monospace; 67 | } 68 | 69 | p { 70 | padding-left: 10pt; 71 | padding-right: 10pt; 72 | } 73 | 74 | span.download { 75 | font-weight: normal; 76 | color: blue; 77 | background: #EEEEEE; 78 | } 79 | 80 | .path { 81 | font-family: Verdana, Arial, Helvetica, sans-serif; 82 | } 83 | 84 | .navi { 85 | text-align: right; 86 | } 87 | 88 | th { 89 | border-color: AAAADD; 90 | background-color: #ddeedd; 91 | background-color: #EEEEFF 92 | } 93 | .center{ 94 | text-align: center; 95 | } 96 | 97 | .left{ 98 | text-align: left; 99 | padding: 10pt; 100 | } 101 | 102 | .right{ 103 | text-align: right; 104 | padding: 10pt; 105 | } 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/reudy/wordset.rb: -------------------------------------------------------------------------------- 1 | #Copyright (C) 2003 Gimite 市川 2 | #Modified by Glass_saga 3 | 4 | require $REUDY_DIR+'/reudy_common' 5 | 6 | module Gimite 7 | class Word #単語クラス 8 | def initialize(str, author = "", mids = []) 9 | @str = str #単語の文字列。 10 | @author = author #単語を教えた人。 11 | @mids = mids #この単語を含む発言の番号。 12 | end 13 | 14 | attr_accessor :str,:author,:mids 15 | 16 | def ==(other) 17 | return @str == other.str 18 | end 19 | 20 | def eql?(other) 21 | return @str == other.str 22 | end 23 | 24 | def hash 25 | return @str.hash 26 | end 27 | 28 | def <=>(other) 29 | return @str <=> other.str 30 | end 31 | 32 | def inspect 33 | return "" 34 | end 35 | end 36 | 37 | # 単語集 38 | class WordSet 39 | include Gimite, Enumerable 40 | 41 | def initialize(filename) 42 | @filename = filename 43 | @added_words = [] 44 | File.open(filename, File::RDONLY|File::CREAT) do |f| 45 | @words = YAML.load(f) || [] 46 | end 47 | end 48 | 49 | attr_reader :words 50 | 51 | #単語を追加 52 | def addWord(str, author = "") 53 | return nil if str.empty? 54 | i = @words.find_index{|word| str.include?(word.str) } 55 | if i && @words[i].str == str # 重複する単語があった場合 56 | return nil 57 | else 58 | word = Word.new(str, author) 59 | if i 60 | @words.insert(i, word) 61 | else 62 | @words.push(word) 63 | end 64 | @added_words.push(word) 65 | return word 66 | end 67 | end 68 | 69 | #ファイルに保存 70 | def save 71 | File.open(@filename, "w") do |f| 72 | YAML.dump(@words, f) 73 | end 74 | end 75 | 76 | #単語イテレータ 77 | def each 78 | @words.each 79 | end 80 | 81 | #中身をテキスト形式で出力。 82 | def output(io) 83 | @words.each do |word| 84 | io.puts "#{word.str}\t#{word.author}\t#{word.mids.join(",")}" 85 | end 86 | end 87 | 88 | private 89 | 90 | #既存のファイルとかぶらないファイル名を作る。 91 | def makeNewFileName(base) 92 | return base unless File.exist?(base) 93 | i = 2 94 | loop do 95 | name = base + i.to_s 96 | return name unless File.exist?(name) 97 | i += 1 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/reudy/attention_decider.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | #文尾だけを使った類似判定。 6 | require $REUDY_DIR+'/reudy_common' 7 | 8 | module Gimite 9 | #注目判定器。 10 | class AttentionDecider 11 | include Gimite 12 | 13 | def initialize 14 | @lastNick = nil #最後の発言者。"!"なら、自分。 15 | @prob = 1.0 16 | @recentSpeakers = Array.new(10) #nilで初期化されている 17 | end 18 | 19 | #パラメータを設定する。 20 | def setParameter(probs) 21 | @minProb = probs[:min] #発言率の最低値。 22 | @maxProb = probs[:max] #発言率の最高値。 23 | @probs = probs[:default] #デフォルトの発言率。 24 | @calledProb = probs[:called] #名前を呼ばれた時の発言率の下限。 25 | @selfProb = probs[:self] #普段の自己発言の発言率。 26 | @ignoredProb = probs[:ignored] #無視された後の自己発言の発言率。 27 | @probRange = @maxProb - @minProb 28 | end 29 | 30 | #他人が発言した時にこれを呼ぶ。 31 | #発言率を返す。 32 | def onOtherSpeak(from_nick, sentence, called) 33 | updateRecentSpeakers(from_nick) 34 | 35 | #今回の発言率を求める。 36 | if called || recentOtherSpeakers.size == 1 37 | prob = @calledProb 38 | else 39 | prob = @prob 40 | end 41 | 42 | #発言率を更新。 43 | if called 44 | raiseProbability(1.0) #呼ばれたら、発言率を最高に。 45 | else 46 | raiseProbability(-0.2) #それ以外のケースでは、発言率は徐々に下がる。 47 | end 48 | 49 | prob 50 | end 51 | 52 | #自分が発言した時にこれを呼ぶ。 53 | def onSelfSpeak(usedWords) 54 | updateRecentSpeakers("!") 55 | end 56 | 57 | #沈黙がしばらく続いた時にこれを呼ぶ。発言率を返す。 58 | def onSilent 59 | updateRecentSpeakers(nil) 60 | puts self 61 | if @lastNick == "!" 62 | raiseProbability(-0.2) 63 | @ignoredProb 64 | elsif recentOtherSpeakers.size == 1 65 | @calledProb 66 | else 67 | @selfProb 68 | end 69 | end 70 | 71 | #現在の状態を表す文字列。 72 | def to_s 73 | "デフォルト発言率:%.2f, 最近の発言者: #{@recentSpeakers}" % @prob 74 | end 75 | 76 | private 77 | 78 | #発言率を上げ下げする。 79 | #上げ率rateは、発言率の変動範囲(@probRange)に対する割合で指定する。 80 | def raiseProbability(rate) 81 | @prob = [[@prob+rate*@probRange, @maxProb].min, @minProb].max 82 | end 83 | 84 | def updateRecentSpeakers(nick) 85 | @lastNick = nick if nick 86 | @recentSpeakers.shift 87 | @recentSpeakers.push(nick) 88 | end 89 | 90 | def recentOtherSpeakers 91 | t = @recentSpeakers - [nil, "!"] 92 | t.uniq! 93 | t 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /manual/reudywin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ロイディ Ver.3.09 Windowsでの準備から実行まで --- SSB 5 | 6 | 7 | 8 | 9 |

10 | [戻る] 11 | [ロ技研のトップ] 12 | [ロ技研掲示板] 13 | 14 |

ロイディ Ver.3.09 Windowsでの準備から実行まで

15 | 16 | 17 |

18 | Windowsでロイディを動かす準備から実行までを説明します。「ロイディ Ver.3.09 マニュアル」も読んでください。 19 |

20 | 21 |

必要なもののインストール

22 | 23 |

24 | ここではRubyのmingw版を例にしますが、cygwin版やmswin32版でもたぶん大丈夫です。 25 |

26 | 27 |
    28 |
  • Ruby 1.8のmingw版バイナリのページから、 ruby-1.8.4-***-i386-mingw32.tar.gz というファイル(いくつか有れば新しい方)をダウンロード。
  • 29 |
  • ダウンロードしたファイルを適当な場所に解凍*1。ここでは C:\ruby とします。つまり、 C:\ruby\bin の下に ruby.exe とかが有る状態です。
  • 30 |
  • RubyのWin32用拡張ライブラリのページから、 gdbm-***-mingw32.zip というファイルをダウンロード。解凍してできた bin\gdbm.dll を C:\ruby\bin の中にコピー。このファイル以外は要らないっぽいです。
  • 31 |
  • 最後にロイディ本体です。 reudy309.zip (バージョンアップの場合は reudy309up.zip )をダウンロードして、適当な場所に解凍してください。ここでは C:\reudy とします。
  • 32 |
33 | 34 |

35 | *1 tar.gzが解凍できない人は+Lhaca デラックス版でも入れてください。 36 |

37 | 38 |

実行(IRCで動かす場合)

39 | 40 |

41 | 実行する前に、「ロイディ Ver.3.09 マニュアル」を見ながら、 C:\reudy\public\setting.txt を編集してください。 42 |

43 | 44 |

45 | Windows NT/2000/XP/Vistaの場合:スタートメニューの[ファイル名を指定して実行]で cmd と入力して、[OK]をクリック。コマンドプロンプトが起動します。 46 |

47 | 48 |

49 | Windows 95/98/Meの場合:スタートメニューの[ファイル名を指定して実行]で command と入力して、[OK]をクリック。MS-DOSプロンプトが起動します。 50 |

51 | 52 |

53 | ここで、以下の順に入力します。(Enter)の所ではEnterキーを押します。 54 |

55 | 56 |

57 | 58 |     C:(Enter)
59 |     cd \reudy(Enter)
60 |     C:\ruby\bin\ruby irc_reudy.rb public(Enter)
61 |
62 |

63 | 64 |

65 | これで動きます。 66 | 67 |

68 |
69 |

70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/reudy/response_estimator.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | $REUDY_DIR= "." unless defined?($REUDY_DIR) 6 | 7 | require $REUDY_DIR+'/message_log' 8 | require $REUDY_DIR+'/wordset' 9 | require $REUDY_DIR+'/word_searcher' 10 | require $REUDY_DIR+'/reudy_common' 11 | 12 | module Gimite 13 | #指定の発言への返事を推定する。 14 | class ResponseEstimator 15 | include Gimite 16 | 17 | def initialize(log, wordSearcher, msgFilter = proc{ |n| true }, wordFilter = proc{ |w| true }) 18 | @cacheLimit = 40 19 | @log = log 20 | @wordSearcher = wordSearcher 21 | @msgFilter = msgFilter 22 | @wordFilter = wordFilter 23 | @cache = {} 24 | end 25 | 26 | #mid番目の発言への返事(と思われる発言)について、[発言番号,返事らしさ]を返す。 27 | #ただし、@msgFilter.call(返事の番号)を満たすのが条件。 28 | #該当するものが無ければ[nil,0]を返す。 29 | #debugが真なら、デバッグ出力をする。 30 | def responseTo(mid, debug = false) 31 | return [nil, 0] unless mid 32 | mid += @log.size if mid < 0 33 | return @cache[mid] if @cache[mid] && @msgFilter.call(@cache[mid].first) #キャッシュにヒット。 34 | 35 | numTargets = 5 36 | candMids = (mid+1..mid+numTargets).select{ |n| @msgFilter.call(n) } 37 | return [nil, 0] if candMids.empty? 38 | #この先の判定は重いので、先に「絶対nilになるケース」を除外。 39 | words = @wordSearcher.searchWords(@log[mid].body).select{ |w| @wordFilter.call(w) } 40 | resMid = nil 41 | 42 | #その発言からnumTargets行以内で、同じ単語を含むものが有れば、それを返事とみなす。 43 | #無ければ、直後の発言を返事とする。 44 | words.each do |word| 45 | word.mids.each do |n| 46 | if n > mid 47 | if n > mid + numTargets || (resMid && n >= resMid) 48 | break 49 | elsif candMids.include?(n) 50 | resMid = n 51 | break 52 | end 53 | end 54 | end 55 | end 56 | prob = resMid ? numTargets : 0 #同じ単語を含む方が、返事らしさが高い。 57 | resMid = candMids.first unless resMid 58 | prob += numTargets + 1 - (resMid-mid) #近い発言の方が、返事らしさが高い。 59 | 60 | #キャッシュしておく。 61 | @cache.clear if @cache.size >= @cacheLimit 62 | @cache[mid] = [resMid, prob] 63 | 64 | return [resMid, prob] 65 | end 66 | end 67 | 68 | if __FILE__ == $PROGRAM_NAME 69 | dir = ARGV[0] 70 | log = MessageLog.new(dir+"/log.dat") 71 | wordSet = WordSet.new(dir+"/words.dat") 72 | wordSearcher = WordSearcher.new(wordSet) 73 | resEst = ResponseEstimator.new(log, wordSearcher) 74 | ARGV[1..-1].map(&:to_i).each do |mid| 75 | printf("[%d]%s:\n", mid, log[mid].body) 76 | resMid, prob= resEst.responseTo(mid, true) 77 | printf(" [%d]%s (%d)\n", resMid, log[resMid].body, prob) if resMid 78 | end 79 | end 80 | end 81 | 82 | -------------------------------------------------------------------------------- /twitter_reudy.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | #Copyright (C) 2011 Glass_saga 3 | 4 | $REUDY_DIR= "./lib/reudy" unless defined?($REUDY_DIR) 5 | 6 | Interval = 60 # タイムラインを取得する間隔 7 | Abort_on_API_limit = false # API制限に引っかかった時にabortするかどうか 8 | 9 | trap(:INT){ exit } 10 | 11 | require 'optparse' 12 | require 'rubytter' 13 | require 'highline' 14 | require 'time' 15 | require $REUDY_DIR+'/bot_irc_client' 16 | require $REUDY_DIR+'/reudy' 17 | require $REUDY_DIR+'/reudy_common' 18 | 19 | module Gimite 20 | class TwitterClient 21 | 22 | include(Gimite) 23 | 24 | def initialize(user) 25 | @user = user 26 | @user.client = self 27 | @last_tweet = Time.now 28 | 29 | key = user.settings[:twitter][:key] 30 | secret = user.settings[:twitter][:secret] 31 | cons = OAuth::Consumer.new(key, secret, :site => "http://api.twitter.com") 32 | 33 | unless File.exist?(File.dirname(__FILE__)+"/token") 34 | request_token = cons.get_request_token 35 | puts "Access This URL and press 'Allow' => #{request_token.authorize_url}" 36 | pin = HighLine.new.ask('Input key shown by twitter: ') 37 | access_token = request_token.get_access_token(:oauth_verifier => pin) 38 | open(File.dirname(__FILE__)+"/token","w") do |f| 39 | f.puts access_token.token 40 | f.puts access_token.secret 41 | end 42 | end 43 | 44 | keys = File.read(File.dirname(__FILE__)+"/token").split(/\r?\n/).map(&:chomp) 45 | 46 | token = OAuth::AccessToken.new(cons, keys[0], keys[1]) 47 | 48 | @r = OAuthRubytter.new(token) 49 | end 50 | 51 | attr_accessor :r 52 | 53 | def onTweet(status) 54 | @user.onOtherSpeak(status.user.screen_name, status.text) 55 | end 56 | 57 | #補助情報を出力 58 | def outputInfo(s) 59 | puts "(#{s})" 60 | end 61 | 62 | #発言する 63 | def speak(s) 64 | time = Time.now 65 | if time - @last_tweet > Interval 66 | @r.update(s) 67 | puts "tweeted: #{s}" 68 | @last_tweet = time 69 | end 70 | end 71 | end 72 | 73 | opt = OptionParser.new 74 | 75 | directory = 'public' 76 | opt.on('-d DIRECTORY') do |v| 77 | directory = v 78 | end 79 | 80 | db = 'pstore' 81 | opt.on('--db DB_TYPE') do |v| 82 | db = v 83 | end 84 | 85 | mecab = nil 86 | opt.on('-m','--mecab') do |v| 87 | mecab = true 88 | end 89 | 90 | opt.parse!(ARGV) 91 | 92 | #twitter用ロイディを作成 93 | client = TwitterClient.new(Reudy.new(directory,{},db,mecab)) 94 | 95 | loop do 96 | begin 97 | since_id = -1 98 | client.r.friends_timeline(since_id: since_id).each do |status| 99 | puts "#{status.user.screen_name}: #{status.text}" 100 | since_id = status.id 101 | client.onTweet(status) 102 | end 103 | sleep(Interval) 104 | rescue => ex 105 | case ex.message 106 | when "Could not authenticate with OAuth." 107 | abort ex.message 108 | when /Rate limit exceeded./ 109 | if Abort_on_API_limit 110 | abort ex.message 111 | else 112 | reset_time = Time.parse(r.limit_status[:reset_time]) 113 | puts ex.message 114 | puts "API制限は#{reset_time}に解除されます。" 115 | sleep(reset_time - Time.now) 116 | end 117 | else 118 | puts ex.message 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/reudy/similar_searcher.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | #文尾だけを使った類似判定。 6 | $REUDY_DIR= "." unless defined?($REUDY_DIR) 7 | 8 | require 'set' 9 | require $REUDY_DIR+'/reudy_common' 10 | require $REUDY_DIR+'/message_log' 11 | 12 | module Gimite 13 | #類似発言検索器。 14 | class SimilarSearcher 15 | =begin 16 | 文尾@compLen文字が1文字違いの発言を類似発言とする。 17 | ただし、ひらがなと一部の記号のみが対象。 18 | @tailMapは、「文尾@compLen文字と、そこから任意の1文字を抜いた物」をキーとし、 19 | 発言番号の配列を値とする。 20 | 例えば、10行目が「答えが分かりませんでした。」という発言なら、 21 | @tailMap["ませんでした"].include?(10) 22 | @tailMap["せんでした"].include?(10) 23 | @tailMap["まんでした"].include?(10) 24 | @tailMap["ませでした"].include?(10) 25 | @tailMap["ませんした"].include?(10) 26 | @tailMap["ませんでた"].include?(10) 27 | @tailMap["ませんでし"].include?(10) 28 | は全てtrueになる。これを使って「文尾が同じor1文字違いの発言」を探す。 29 | =end 30 | include Gimite 31 | 32 | def initialize(fileName, log, db) 33 | require "#{$REUDY_DIR}/#{db}" 34 | @log = log 35 | @log.addObserver(self) 36 | @compLen = 6 #比較対象の文尾の長さ 37 | makeDictionary(fileName) 38 | end 39 | 40 | #inputに類似する各発言に対して、発言番号を引数にblockを呼ぶ。発言の順序は微妙にランダム。 41 | def eachSimilarMsg(input, &block) 42 | ws = normalizeMsg(input) 43 | return if ws.size <= 1 44 | if ws.size >= @compLen 45 | wtail = ws[-@compLen..-1] #文尾。 46 | randomEach(@tailMap[wtail], &block) 47 | 0.upto(@compLen.pred) do |i| 48 | randomEach(@tailMap[wtail[0...i] + wtail[i+1..-1] ], &block) #途中を1文字抜かしたもの。 49 | end 50 | else 51 | randomEach(@tailMap[ws], &block) 52 | end 53 | end 54 | 55 | #contの各要素について、ランダムな順序でblockを呼び出す。 56 | def randomEach(cont) 57 | if cont 58 | cont.shuffle.each do |c| 59 | yield(c) 60 | end 61 | end 62 | end 63 | 64 | #発言が追加された。 65 | def onAddMsg 66 | recordTail(-1) 67 | end 68 | 69 | #ログがクリアされた。 70 | def onClearLog 71 | @tailMap.clear 72 | end 73 | 74 | #文尾辞書(@tailMap)を生成。 75 | def makeDictionary(fileName) 76 | begin 77 | @tailMap = DB.new(fileName) 78 | rescue LoadError => ex 79 | warn ex.message 80 | warn "警告: 指定されたデータベースを利用できません。辞書を連想配列として保持する為、メモリを大量に消費します。" 81 | @tailMap = {} 82 | end 83 | if @tailMap.empty? 84 | warn "文尾辞書( #{fileName})を作成中..." 85 | 0.upto(@log.size.pred) do |i| 86 | warn "#{i+1}行目..." if ((i+1) % 1000).zero? 87 | recordTail(i) 88 | end 89 | end 90 | end 91 | 92 | #lineN番の発言の文尾を記録。 93 | def recordTail(line_n) 94 | ws = normalizeMsg(@log[line_n].body) 95 | return nil if ws.size <= 1 96 | if ws.size >= @compLen 97 | wtail = ws[-@compLen..-1] #文尾。 98 | addToTailMap(wtail, line_n) 99 | 0.upto(@compLen.pred) do |i| 100 | addToTailMap(wtail[0...i]+wtail[i+1..-1], line_n) #途中を1文字抜かしたもの。 101 | end 102 | else 103 | addToTailMap(ws, line_n) 104 | end 105 | end 106 | 107 | #@tailMapに追加。 108 | def addToTailMap(tail, line_n) 109 | line_n += @log.size if line_n < 0 110 | return if line_n < 0 111 | if @tailMap[tail] 112 | @tailMap[tail] += [line_n] 113 | else 114 | @tailMap[tail] = [line_n] 115 | end 116 | end 117 | 118 | #発言から「ひらがなと一部の記号」以外を消し、記号を統一する。 119 | def normalizeMsg(s) 120 | s = s.gsub(/[^ぁ-んー−?!\?!\.]+/, "") 121 | s.gsub!(/?/, "?") 122 | s.gsub!(/!/,"!") 123 | s.gsub!(/[ー−+]/, "ー") 124 | s 125 | end 126 | end 127 | 128 | if __FILE__ == $PROGRAM_NAME 129 | dir = ARGV[0] 130 | log = MessageLog.new(dir + "/log.dat") 131 | sim = SimilarSearcher.new(dir + "/db", log) 132 | sim.eachSimilarMsg(ARGV[1]) do |mid| 133 | printf("[%d] %s\n", mid, log[mid].body) 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The following is the licence for files except tango-mgm.rb, irc-client.rb. 2 | 3 | Copyright (c) 2003, Gimite Ichikawa 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | * Neither the name of Gimite Ichikawa nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | 16 | ---------------------------------------------------------------------------- 17 | The following is the licence for tango-mgm.rb. 18 | 19 | Copyright (c) 2003, mita-K, NAKAUE.T (Meister), Gimite Ichikawa 20 | All rights reserved. 21 | 22 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 23 | 24 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 25 | 26 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 27 | 28 | * Neither the name of mita-K, NAKAUE.T (Meister), Gimite Ichikawa nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 29 | 30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ---------------------------------------------------------------------------- 33 | The following is the licence for lib/reudy/pstore.rb, lib/reudy/tango-mecab.rb, twitter_reudy.rb, lingr_reudy.rb. 34 | 35 | Copyright (c) 2011, Glass_saga 36 | All rights reserved. 37 | 38 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 39 | 40 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 41 | 42 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 43 | 44 | * Neither the name of mita-K, NAKAUE.T (Meister), Gimite Ichikawa nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 45 | 46 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 47 | -------------------------------------------------------------------------------- /lib/reudy/michiru.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | 4 | require $REUDY_DIR+'/tango-mgm' 5 | require $REUDY_DIR+'/wordset' 6 | require $REUDY_DIR+'/word_searcher' 7 | require $REUDY_DIR+'/word_associator' 8 | require $REUDY_DIR+'/message_log' 9 | require $REUDY_DIR+'/similar_searcher5' 10 | require $REUDY_DIR+'/reudy_common' 11 | 12 | module Gimite 13 | 14 | #人工無能ミチル 15 | class Michiru 16 | include(Gimite) 17 | 18 | def initialize(dir, fixedSettings= {}) 19 | @recentWordsCt = 40 #最近使った単語を何個記憶するか 20 | @fixedSettings = fixedSettings 21 | @settingPath = dir + "/setting.txt" 22 | loadSettings 23 | puts "単語ロード中..." 24 | @wordSet = WordSet.new(dir + "/words.txt") 25 | @log = MessageLog.new(dir + "/log.txt", @autoSave) 26 | puts "類似検索用データ生成中..." 27 | @simSearcher = SimilarSearcher.new(dir + "/similar.gdbm", @log) 28 | @wordSearcher = WordSearcher.new(@wordSet) 29 | @extractor = WordExtractor.new(14, method(:onAddWord)) 30 | @associator = WordAssociator.new(dir + "/assoc.txt") 31 | @recentWordStrs = [] #最近使った単語 32 | @similarNicksMap = {} #Nick→その人の最近の発言の類似発言の発言者のリスト 33 | end 34 | 35 | #設定をファイルからロード 36 | def loadSettings 37 | file = Kernel.open(@settingPath) 38 | @settings = Hash.new 39 | file.each_line do |line| 40 | ss = line.chop.split(/\t/, 2) 41 | @settings[ss[0]] = ss[1] 42 | end 43 | file.close 44 | @fixedSettings.each do |key, val| 45 | @settings[key] = val 46 | end 47 | @myNicks = settings("nicks").split(",") 48 | @autoSave = settings("disable_auto_saving") != "true" 49 | end 50 | 51 | #チャットクライアントの指定 52 | attr_writer(:client) 53 | 54 | #チャットオブジェクト用の設定 55 | def settings(key) 56 | return @settings[key] 57 | end 58 | 59 | #Nickを相手のNickに変える 60 | def replaceNick(sentence, fromNick) 61 | nickReg = @myNicks.map{ |x| Regexp.escape(x) }.join("|") 62 | return sentence.gsub(Regexp.new(nickReg), fromNick) 63 | end 64 | 65 | #「最近使われた単語」を追加 66 | def addRecentWordStr(wordStr) 67 | @recentWordStrs.push(wordStr) 68 | @recentWordStrs.shift if @recentWordStrs.size > @recentWordsCt 69 | end 70 | 71 | #入力語からの連想を発言にする 72 | def associate 73 | inputWordStr = @inputWords[rand(@inputWords.size)].str 74 | assocWordStrs = @associator.associateAll(inputWordStr) 75 | return nil unless assocWordStrs 76 | outputWordStr = nil 77 | for wordStr in assocWordStrs 78 | unless @recentWordStrs.include?(wordStr) 79 | outputWordStr = wordStr 80 | break 81 | end 82 | end 83 | if outputWordStr 84 | addRecentWordStr(inputWordStr) 85 | addRecentWordStr(outputWordStr) 86 | return inputWordStr + "は" + outputWordStr + "です。" 87 | else 88 | return nil 89 | end 90 | end 91 | 92 | #指定の人の中の人を答える 93 | def innerPeople(nick) 94 | nicks = @similarNicksMap[nick] 95 | if !nicks || nicks.size == 0 96 | return nick + "の中の人はいません。" 97 | else 98 | nicks0 = nicks.uniq.sort.reverse 99 | str = "" 100 | for nick0 in nicks0 101 | ct = nicks.select(){ |x| x == nick0 }.size 102 | str += format("%s(%d%%) ", nick0, ct * 100 / nicks.size) 103 | end 104 | return nick + "の中の人は " + str + "です。" 105 | end 106 | end 107 | 108 | #学習する 109 | def study(input) 110 | @extractor.processLine(input) 111 | @log.addMsg(@fromNick, input) 112 | end 113 | 114 | #類似発言検索用フィルタ 115 | def similarFilter(lineN) 116 | return true 117 | end 118 | 119 | #類似発言データを蓄積する 120 | def storeSimilarData(fromNick, input) 121 | data = @simSearcher.searchSimilarMsg(input, method(:similarFilter)) 122 | return unless data 123 | lineN = data[0] 124 | nicks = @similarNicksMap[fromNick] 125 | nicks = [] unless nicks 126 | nicks.push(@log[lineN].fromNick) 127 | dprint("類似発言", @log[lineN].fromNick, @log[lineN].body) 128 | nicks.shift if nicks.size > 10 129 | @similarNicksMap[fromNick] = nicks 130 | end 131 | 132 | #単語が追加された 133 | def onAddWord(wordStr) 134 | if @wordSet.addWord(wordStr, @fromNick) 135 | # @client.outputInfo("単語「"+wordStr+"」を記憶した。") 136 | @wordSet.save if @autoSave 137 | end 138 | end 139 | 140 | #接続を開始した 141 | def onBeginConnecting 142 | puts "接続開始..." 143 | end 144 | 145 | #自分が入室した 146 | def onSelfJoin 147 | end 148 | 149 | #他人が入室した 150 | def onOtherJoin(fromNick) 151 | end 152 | 153 | #他人が発言した 154 | def onOtherSpeak(fromNick, input) 155 | @fromNick = fromNick 156 | output = nil #発言 157 | isCalled = false 158 | @myNicks.each do |nick| 159 | isCalled = true if (input.index(nick)) 160 | end 161 | storeSimilarData(fromNick, input) 162 | study(input) if settings("disable_studying") != "true" 163 | @inputWords = @wordSearcher.searchWords(input) 164 | @inputWords.delete_if{ |word| @myNicks.include?(word.str) } 165 | if input =~ /([-a-zA-Z0-9_]+)の中の人/ 166 | output = innerPeople($1) 167 | elsif input =~ /は.*である(。|.)?\s*/ 168 | output = "へぇ〜" 169 | elsif (isCalled || rand < 0.1) && @inputWords.size > 0 170 | #相手の単語から連想する 171 | output = associate 172 | end 173 | if isCalled && !output 174 | #質問が分からなかった場合は、そのまま訊き返す 175 | output = replaceNick(input, fromNick) 176 | end 177 | if output 178 | @client.speak(output) 179 | end 180 | end 181 | end 182 | 183 | end #module Gimite 184 | -------------------------------------------------------------------------------- /lib/reudy/tango-mgm.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #!/usr/bin/ruby 3 | #---------------------------------------------------------------------------- 4 | #Copyright (C) 2003 mita-K, NAKAUE.T (Meister), Gimite 市川 5 | # 6 | # mita-Kの単語取得ライブラリ 7 | # 8 | # Original works by mita-K 9 | # Extended by Gimite/Meister 10 | # Modified by Glass_saga(glass.saga@gmail.com) 11 | # 12 | # 2003.06.06 勝手にクラス化(Meister) 13 | # 基本的に文字コードに依存しないように、 14 | # 一部ソースの文字コード(SJIS)に依存 15 | # 2003.06.22 Gimite版と統合(Meister) 16 | # 語尾の除去等の処理は単純な候補除外フィルタに変更した 17 | # 現在の実装では文字列を総当りで調べるため、 18 | # わざわざ語尾を除外した単語を作らなくとも 19 | # 同じものが他の候補に含まれている 20 | # 2003.06.23 Gimite版の機能を追加移植(Gimite) 21 | # ひらがな交じりの語をほとんど抽出できない問題を修正 22 | # 単語抽出時の禁則処理を追加 23 | # 単語抽出時の語末のゴミの除去を追加 24 | # 非ひらがなの2文字以上の連続が無い語は原則として対象外に 25 | # checkWordCand()に渡すprestrとpoststrを配列から文字列に変更 26 | # 主語っぽいものの検出を強化 27 | # EUC用に書き換えたので、必要に応じて戻してください… 28 | # 2011.09.09 Ruby1.9に対応(Glass_saga) 29 | # 30 | #---------------------------------------------------------------------------- 31 | #---------------------------------------------------------------------------- 32 | # 文中から単語(らしき文字列)を探し出す 33 | 34 | class WordExtractor 35 | # コンストラクタ 36 | # WordExtractor(単語候補リストを保持する長さ,単語追加時のコールバック) 37 | def initialize(candlistlength=7,onaddword=nil) 38 | @candList = Array.new(candlistlength,[]) # 単語候補のリスト 39 | @onAddWord = onaddword 40 | end 41 | 42 | attr_accessor :candList 43 | 44 | # 単語候補のリストを整理して返す 45 | def getCandList 46 | candList = @candList.dup 47 | candList.uniq! 48 | candList.flatten! 49 | candList.compact! 50 | return candList 51 | end 52 | 53 | # 単語として適切かどうか判定する 54 | # 主語っぽい語などの特例には適用しない 55 | # 不適だとnilを返す 56 | def wordFilter1(word) 57 | return nil if !word || word.size == 1 #wordがnil又は一文字だけ 58 | case word 59 | when /^[ぁ-んー]+$/, #平仮名だけ 60 | /[^ぁ-んー][^ぁ-ん]/, # 非ひらがなの2文字以上の連続を含まない 61 | /[^ぁ-ん][のとな]$/, # 助詞っぽいものを含む 62 | /^.+(?:が|は)/ # 先頭以外に「が」「は」を含む 63 | return nil 64 | else 65 | return word 66 | end 67 | end 68 | 69 | # 単語として適切かどうか判定する 70 | # 主語っぽいなどの特例にも適用する 71 | # 不適だとnilを返す 72 | def wordFilter2(word) 73 | return nil if !word || word.empty? #wordがnil、又は空白 74 | case word 75 | when /^[  ]/, /[  ]$/, # 空白類 76 | /^[ぁ-んァ-ンー]$/, /^[ぁ-んー−][ぁ-んー−]$/, # かな一文字だけ、ひらがな2文字 77 | /^[-.\/+*:;,~_|&'"`()0-9]+$/, # 数値・記号だけ 78 | /(?:[、。.,!?()・…‾−_:;]|[<>「」『』【】〔〕]|[〜#→←↑←⇔⇒◎—¬Д⌒]|[()])/, # 記号を含む 79 | /^(?:[,]|[ーをんぁぃぅぇぉゃゅょっ]|[ー−ヲンァィゥェォャュョッヶヵ])/, /^[ぁ-ん][^ぁ-ん]/, # あり得ない文字から始まっている 80 | /&[#a-zA-Z0-9]+;/ # HTMLの文字参照 81 | return nil 82 | else 83 | return word 84 | end 85 | end 86 | 87 | # 単語として適切かどうか判定する 88 | # 前後の文字列も参考にする 89 | # 不適だとnilを返す 90 | def checkWordCand(word, prestr="", poststr="") 91 | unless ((prestr.empty? || prestr =~ /[、。.,!?()・…]$/) && poststr =~ /^[はが]([^ぁ-ん]|$)/ \ 92 | &&((word + poststr[0..0]) !~ /(?:では|だが|には|のが)$/) && (word =~ /^[ぁ-んー]+$/ || word =~ /^[^ぁ-ん]/) \ 93 | && word.size >= 3) \ 94 | || (prestr =~ /[><]$/ && poststr.empty?) 95 | word = wordFilter1(word) 96 | end 97 | return wordFilter2(word) 98 | end 99 | 100 | # 文字列を単語として追加べきかを判定する 101 | # 追加すべき単語(wordとは異なる場合も)またはnil(不適)を返す 102 | def checkWord(word) 103 | # 語末のゴミの除去 104 | while word =~ /^(.+)(とか|しなさい|ですか?|のよう|だから|する|って|という|して|したい?|まで|しなさい|ですか?|のよう|せず|される?|には|させる?|しか|ました|できる?)$/ || word =~ /^([^ぁ-ん]+)[しだすともにを]$/ 105 | word = $1 106 | end 107 | case word 108 | when /^(?:[ぁ-んァ-ンー]|[ぁ-んー ][ぁ-んー-])$/, # 禁則 109 | /ない|って|った|てる|んな|いる|から|とは|れる|れて|れる|れた|ます|いう|れば|のは|しい|にな|んで|なる|しく|を|だと|たと|られ 110 | くて|のか|だけ|いた|えて|れが|いと|され|うが|える|ため|ある|こと|して|する|だよ|した|ので|しま|なの|です|なん|でき|とか 111 | ような|だろう/, /[^ぁ-ん][でにを]/, /っ$/ #単語候補時に除外されてるはずだが、語末のゴミの除去で現れた可能性が有るのでもう1度 112 | return nil 113 | else 114 | return word 115 | end 116 | end 117 | 118 | # 文字列から単語侯補を獲得する 119 | # 主にマルチバイト文字列(日本語文字列)用だが、 120 | # 一応シングルバイト文字列を食わせても大丈夫なはず 121 | def extractCands(str) 122 | str = str.dup 123 | 124 | intact = str.scan(/[-_0-9a-zA-Z]+|[ー-ァ-ン]+/) #文字列から英数字やカタカナの連続を取り出し、配列に格納する 125 | str.delete!(*intact) unless intact.empty? 126 | 127 | result = intact.select{|str| checkWordCand(str) } #英数字、カタカナの連続はそのままcheckWordCandにかける 128 | 129 | str_size = str.size 130 | 0.upto(str_size) do |i| #それ以外 131 | i.upto(str_size) do |j| 132 | cand = checkWordCand(str[i..j], str[0...i], str[j+1..-1]) 133 | result << cand if cand 134 | end 135 | end 136 | 137 | return result 138 | end 139 | 140 | # 単語リスト中の包含関係にあるものを削除して単語リストを最適化する 141 | def optimizeWordList(wordcand) 142 | wordcand.combination(2) do |i,j| 143 | if j.include?(i) 144 | i.clear 145 | elsif i.include?(j) 146 | j.clear 147 | end 148 | end 149 | wordcand.reject!(&:empty?) 150 | return wordcand 151 | end 152 | 153 | # 文中で使われている単語を取得 154 | def extractWords(line,words=[]) 155 | wordcand = getCandList.select {|word| line.include?(word)} # 単語侯補が文章中に使われてたら単語にする 156 | 157 | # 新しく加わる単語同士に包含関係があったら短いほうを消去する 158 | # 例えば「なると」という単語が登録される時に 159 | # 「なる」「ると」が同時に単語と認識されてしまうのを防ぐ。 160 | wordcand = optimizeWordList(wordcand) unless wordcand.empty? 161 | 162 | # 禁則処理 163 | wordcand2 = wordcand.select{|word| checkWord(word) } 164 | 165 | words = words | wordcand2 # 新しい単語を本当に単語として認定する。ただしダブる場合は片方を消す。 166 | 167 | words.each{|w| @onAddWord.call(w) } if @onAddWord 168 | 169 | return words 170 | end 171 | 172 | # 単語侯補のリストを更新する 173 | def renewCandList(line) 174 | @candList.shift 175 | @candList.push(extractCands(line)) 176 | end 177 | 178 | # 単語取得・単語候補リスト更新を1行分処理する 179 | def processLine(line) 180 | words = extractWords(line) 181 | renewCandList(line) 182 | return words 183 | end 184 | 185 | #デバッグ出力 186 | def dprint(caption, obj) 187 | puts "#{caption} : #{obj.inspect}" 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /lib/reudy/bot_irc_client.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | require 'socket' 6 | require 'thread' 7 | require $REUDY_DIR + '/irc-client' 8 | require $REUDY_DIR + '/reudy_common' 9 | 10 | module Gimite 11 | #ボット用のIRCクライアント 12 | class BotIRCClient < IRCC 13 | include Gimite 14 | 15 | SILENT_SECOND = 20.0 #沈黙が続いたと判断する秒数。 16 | 17 | def initialize(user, logOut = STDOUT) 18 | @user = user 19 | @isExitting = false 20 | @channel = @user.settings[:channel] 21 | @infoChannel = @user.settings[:info_channel] 22 | @nick = @user.settings[:nick] 23 | @user.client = self 24 | @user.onBeginConnecting 25 | option = { 26 | 'user'=>@user.settings[:name], \ 27 | 'realname'=>@user.settings[:real_name], \ 28 | 'pass'=>@user.settings[:login_password].to_s, \ 29 | 'nick'=>@nick, \ 30 | 'channel'=>@channel, \ 31 | 'channel_key'=>@user.settings[:channel_key].to_s \ 32 | } 33 | super(nil, option, __ENCODING__.to_s, logOut, @user.settings[:encoding] || "ISO-2022-JP") 34 | end 35 | 36 | #IRCのメッセージをひたすら処理するループ。 37 | def processLoop 38 | loop do 39 | begin 40 | @isJoiningInfoChannel = false 41 | @prevTime = Time.now #onSilent用。 42 | @receiveQue = Queue.new #受け取った通常発言のキュー。 43 | @controlQue = Queue.new #受け取った制御発言のキュー。 44 | connect(TCPSocket.open(@user.settings[:host], @user.settings[:port].to_i,@user.settings[:localhost])) 45 | on_connect #ソケット接続時の処理。 46 | pingThread = Thread.new{ pingProcess } 47 | receiveThread = Thread.new{ receiveProcess } 48 | #受信ループ。 49 | while line = sock.gets 50 | on_recv(line.force_encoding(@user.settings[:encoding] || "ISO-2022-JP")) 51 | time = Time.now 52 | if time - @prevTime >= SILENT_SECOND 53 | @prevTime = time 54 | @user.onSilent 55 | #沈黙がしばらく続いた。 56 | #発言が何も無くてもpingProcess()のおかげで定期的にメッセージが飛んでくるので、 57 | #ここでチェックすればOK。 58 | end 59 | end 60 | puts "切断されました。" 61 | rescue SystemCallError, SocketError, IOError => ex 62 | puts "切断されました。#{ex.message}" 63 | end 64 | pingThread.exit if pingThread 65 | @receiveQue.push(nil) 66 | receiveThread.join if receiveThread 67 | break if @isExitting || @user.settings[:auto_reconnect] 68 | sleep(10) 69 | break unless queryReconnect 70 | puts "再接続中..." 71 | end 72 | end 73 | 74 | #補助情報を出力 75 | def outputInfo(s) 76 | sleep(@user.settings[:wait_before_info].to_f) if @user.settings[:wait_before_info] 77 | sendmess("NOTICE #{@infoChannel} :#{s}\n") 78 | end 79 | 80 | #発言する 81 | def speak(s) 82 | if @user.settings[:speak_with_privmsg] 83 | sendpriv(s) 84 | else 85 | sendnotice(s) 86 | end 87 | end 88 | 89 | #チャンネルを移動。接続中はこっちを使う。 90 | def moveChannel(channel) 91 | greeting = @user.settings[:leaving_message] 92 | speak(greeting) if greeting 93 | @channel = channel 94 | movechannel(@channel) 95 | end 96 | 97 | #チャンネルを変更。切断中はこっちを使う。 98 | def setChannel(channel) 99 | @channel = channel 100 | @irc_channel = channel 101 | end 102 | 103 | def status=(status) 104 | end 105 | 106 | #終了。 107 | def exit 108 | @isExitting = true 109 | greeting = @user.settings[:leaving_message] 110 | sendmess(greeting ? "QUIT :#{greeting}\r\n" : "QUIT\r\n") 111 | end 112 | 113 | #以下、IRCCのメソッドのオーバライド 114 | 115 | def on_priv(type, nick, mess) 116 | super(type, nick, mess) 117 | onPriv(type, nick, mess) 118 | end 119 | 120 | def on_external_priv(type, nick, to, mess) 121 | super(type, nick, to, mess) 122 | onExternalPriv(type, nick, to, mess) 123 | end 124 | 125 | def on_join(nick, channel) 126 | super(nick, channel) 127 | onJoin(nick, channel) 128 | end 129 | 130 | def on_myjoin(channel) 131 | #IRCC#on_myjoinの中ではon_joinが呼ばれてしまうので、 132 | #ここでsuperを呼んではいけない。 133 | onMyJoin(channel) 134 | end 135 | 136 | def on_myinvite(nick, channel) 137 | super(nick, channel) 138 | onInvite(nick, channel) 139 | end 140 | 141 | def on_error(code) 142 | onError(code) 143 | end 144 | 145 | #以下、派生クラスでオーバライド可能なメソッド 146 | 147 | #普通のメッセージ 148 | def onPriv(type, nick, mess) 149 | if nick != @nick && (@user.settings[:respond_to_notice] || type == "PRIVMSG") 150 | @prevTime= Time.now 151 | @receiveQue.push([nick, mess.strip]) 152 | end 153 | end 154 | 155 | #今いるチャンネルの外からの普通のメッセージ 156 | def onExternalPriv(type, nick, to, mess) 157 | return if nick == @nick || (!@user.settings[:respond_to_notice] && type != "PRIVMSG") 158 | @prevTime = Time.now 159 | if @user.settings[:respond_to_external] 160 | #チャンネル外からの発言は制御発言、という危険な仮仕様。 161 | @controlQue.push(mess.strip) 162 | @receiveQue.push(:nop) #メッセージ処理ループのブロックを解く。 163 | else 164 | @receiveQue.push([nick, mess.strip]) 165 | end 166 | end 167 | 168 | #他人がJOINした 169 | def onJoin(nick, channel) 170 | greeting = @user.settings[:private_greeting] 171 | sendmess("NOTICE #{nick} :#{greeting}\n") if greeting && !greeting.empty? 172 | @user.onOtherJoin(nick) 173 | end 174 | 175 | #自分がJOINした 176 | def onMyJoin(channel) 177 | channel.strip! 178 | channel.downcase! 179 | if channel == @channel.downcase 180 | greeting = @user.settings[:joining_message] 181 | speak(greeting) if greeting 182 | @user.onSelfJoin 183 | end 184 | unless @isJoiningInfoChannel 185 | sendmess("JOIN #{@infoChannel}\r\n") 186 | @isJoiningInfoChannel = true 187 | end 188 | end 189 | 190 | #招待された 191 | def onInvite(nick, channel) 192 | moveChannel(channel) 193 | end 194 | 195 | #再接続の前に呼び出される。 196 | #falseを返すと、再接続せずに終了する。 197 | def queryReconnect 198 | return true 199 | end 200 | 201 | #エラー 202 | def onError(code) 203 | if code == "433" #ERR_NICKNAMEINUSE ニックネームはすでに使用されている 204 | puts "Error: ニックネーム #{@nick} は、別の人に使われています。" 205 | else 206 | puts "Error: エラーコード #{code}" 207 | end 208 | sendmess("QUIT\r\n") #一度QUITして再接続。 209 | end 210 | 211 | private 212 | 213 | #受信してキューにたまっている発言を処理する。 214 | def receiveProcess 215 | while args = popMessage 216 | while args 217 | sleep(@user.settings[:wait_before_speak].to_f * (0.5 + rand)) if @user.settings[:wait_before_speak] 218 | if @receiveQue.empty? 219 | @user.onOtherSpeak(*(args+[false])) 220 | break 221 | end 222 | until @receiveQue.empty? && args 223 | #ウエイト中に他の人の発言が入った場合、前の発言は極力無視する。 224 | @user.onOtherSpeak(*(args+[true])) 225 | args = popMessage 226 | return unless args 227 | end 228 | end 229 | end 230 | end 231 | 232 | #受信してキューにたまっている発言を取り出す。 233 | #制御発言があれば優先して処理する。 234 | def popMessage 235 | loop do 236 | mess = @receiveQue.pop 237 | @user.onControlMsg(@controlQue.pop) until @controlQue.empty? 238 | return mess if mess != :nop 239 | end 240 | end 241 | 242 | #定期的に意味の無いメッセージを送り、通信が切れてないか確かめる。 243 | #通信が切れたら、sock.getsのブロック状態を解除させるためにsock.closeする。 244 | def pingProcess 245 | loop do 246 | sleep(SILENT_SECOND) 247 | begin 248 | sendmess("TOPIC #{@channel}\r\n") 249 | rescue 250 | sock.close 251 | Thread.exit 252 | end 253 | end 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/reudy/irc-client.rb: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------- 2 | # 3 | # IRCクライアントライブラリ 4 | # 5 | # Programed by NAKAUE.T (Meister) 6 | # Modified by Gimite 市川 7 | # Modified by Glass_saga(glass.saga@gmail.com) 8 | # 9 | # 2003.05.04 Version 1.0.0 使ってくれる人が増えたのでソースを整理 10 | # 2003.05.10 Version 1.1.0 NICK処理追加 11 | # 2003.07.24 Version 1.2.0g 中途半端にマルチチャンネル対応(Gimite) 12 | # 2003.09.27 Version 1.2.1 UltimateIRCdで認証前にPINGが来る問題に対処(Meister) 13 | # (thanks for bancho) 14 | # 2003.09.28 Version 1.2.2 文字コード変換を整理(Meister) 15 | # 外部とのやり取りを行うコードを指定する 16 | # (IRCはJISを使うことになっている) 17 | # initializeのパラメータが変更になったので注意! 18 | # 2003.09.28 Version 2.0.0 インターフェース整理(Meister) 19 | # 互換性が低くなったので一気にバージョンを上げる 20 | # 2003.10.01 Version 2.0.1 NICKのバグ修正(Meister) 21 | # 2004.01.01 Version 2.0.2 インスタンス生成後にソケットを渡せるようにした 22 | # 2004.03.03 Version 2.0.3g 接続が切れた時に、IRCC#connectで再接続できるように 23 | # IRCエラーを処理するIRCC#on_errorを追加(Gimite) 24 | # 2011.09.09 Version 2 0.4 Ruby1.9に対応(Glass_saga) 25 | # 26 | # 27 | # このソフトウェアはPublic Domain Softwareです。 28 | # 自由に利用・改変して構いません。 29 | # 改変の有無にかかわらず、自由に再配布することが出来ます。 30 | # 作者はこのソフトウェアに関して、全ての権利と全ての義務を放棄します。 31 | # 32 | #---------------------------------------------------------------------------- 33 | # IRCプロトコルについてはRFC2810-2813を参照のこと。日本語訳あります。 34 | #---------------------------------------------------------------------------- 35 | #---------------------------------------------------------------------------- 36 | 37 | class IRCC 38 | def initialize(sock, userinfo, internal_encoding, disp=STDOUT, irc_encoding) 39 | if sock 40 | @sock = sock 41 | @sock.set_encoding(@irc_encoding, @internal_encoding) unless @irc_encoding == @internal_encoding 42 | end 43 | @userinfo = userinfo 44 | @irc_nick = @userinfo['nick'] 45 | @irc_channel = @userinfo['channel'] # 自動でJOINするチャンネル。このチャンネルを抜けると終了する(仕様) 46 | @channel_key = @userinfo['channel_key'] || '' 47 | 48 | @nicklist = [] 49 | @joined_channel = nil 50 | 51 | @internal_encoding = internal_encoding ? Encoding.find(internal_encoding) : Encoding::UTF_8 52 | @irc_encoding = irc_encoding ? Encoding.find(irc_encoding) : Encoding::ISO_2022_JP 53 | 54 | @disp = disp 55 | end 56 | 57 | attr_accessor :sock,:userinfo,:nicklist,:irc_nick,:joined_channel 58 | 59 | # インスタンス生成後のソケット接続 60 | def connect(sock) 61 | @sock = sock 62 | @sock.set_encoding(@irc_encoding, @internal_encoding) unless @irc_encoding == @internal_encoding 63 | @myprefix = nil 64 | end 65 | 66 | # メッセージを送信(生) 67 | def sendmess(mess) 68 | @sock.print(mess) 69 | @disp.puts(mess.chop) 70 | end 71 | 72 | # メッセージの送信(通常のPRIVMSGで) 73 | def sendpriv(mess="") 74 | dispmess(">#{@irc_nick}<", mess) 75 | buff = "PRIVMSG #{@irc_channel} :#{mess}" 76 | sendmess(buff + "\r\n") 77 | end 78 | 79 | # メッセージの送信(NOTICEで) 80 | def sendnotice(mess="") 81 | dispmess(">#{@irc_nick}<", mess) 82 | buff = "NOTICE #{@irc_channel} :#{mess}" 83 | sendmess(buff + "\r\n") 84 | end 85 | 86 | # 別のチャンネルに移動 87 | def movechannel(channel) 88 | old_channel = @irc_channel 89 | @irc_channel = channel 90 | #PARTの前にこれを書き換えておかないとQUITしてしまう 91 | sendmess("PART #{old_channel}\r\n") 92 | sendmess("JOIN #{@irc_channel} #{@channel_key}\r\n") 93 | end 94 | 95 | # 終了する(実際にはチャンネルを抜けている) 96 | def quit 97 | sendmess("PART #{@irc_channel}\r\n") 98 | end 99 | 100 | # サーバから受け取ったメッセージを処理 101 | def on_recv(s) 102 | s.chomp! 103 | @disp.puts ">#{s}" 104 | 105 | prefix = ":unknown!unknown@unknown" 106 | prefix, param = s.split(' ', 2) if s[0..0] == ':' 107 | nick, prefix = prefix.split('!', 2) 108 | nick.slice!(0) 109 | param = s unless param 110 | 111 | param, param2 = param.split(/ :/, 2) 112 | param = param.split(' ') 113 | param << param2 if param2 114 | 115 | case param[0] 116 | when 'PRIVMSG', 'NOTICE' # 通常のメッセージ(NOTICEへのBOTの反応は禁止されている) 117 | if param[2][1..1] != "\001" 118 | mess = param[-1] 119 | if param[1].downcase == @irc_channel.downcase 120 | on_priv(param[0], nick, mess) 121 | else 122 | on_external_priv(param[0], nick, param[1], mess) # 今いるチャンネルの外からの発言 123 | end 124 | end 125 | when '372', '375' # MOTD(Message Of The Day) 126 | on_motd(param[-1]) 127 | when '353' # チャンネル参加メンバーのリスト 128 | @nicklist += param[-1].gsub(/@/,'').split 129 | when 'JOIN' # 誰かがチャンネルに参加した 130 | channel = param[1] 131 | if @myprefix == prefix 132 | @joined_channel = channel 133 | on_myjoin(channel) 134 | else 135 | @nicklist |= [nick] 136 | on_join(nick, channel) 137 | end 138 | when 'PART' # 誰かがチャンネルから抜けた 139 | channel = param[1] 140 | if @myprefix == prefix 141 | @nicklist = [] 142 | @joined_channel = nil 143 | on_mypart(channel) 144 | # 終了シーケンスだったらQUIT 145 | sendmess("QUIT\r\n") if param[1].downcase == @irc_channel.downcase 146 | else 147 | @nicklist.delete(nick) 148 | on_part(nick,channel) 149 | end 150 | when 'QUIT' # 誰かが終了した 151 | mess = param[-1] 152 | if @myprefix == prefix 153 | @nicklist = [] 154 | on_myquit(mess) 155 | else 156 | @nicklist.delete(nick) 157 | on_quit(nick,mess) 158 | end 159 | when 'KICK' # 誰かがチャンネルから蹴られた 160 | kicker = nick 161 | channel = param[1] 162 | nick = param[2] 163 | mess = param[3]||'' 164 | if nick == @irc_nick 165 | if param[1].downcase == @irc_channel.downcase 166 | @nicklist = [] 167 | @joined_channel=nil 168 | end 169 | on_mykick(channel,mess,kicker) 170 | sendmess("QUIT\r\n") if param[1].downcase == @irc_channel.downcase # 蹴られたのでQUIT 171 | else 172 | @nicklist.delete(nick) 173 | on_kick(nick,channel,mess,kicker) 174 | end 175 | when 'NICK' # 誰かがNICKを変更した 176 | nick_new = param[1] 177 | @irc_nick = nick_new if nick == @irc_nick 178 | @nicklist.delete(nick) 179 | @nicklist |= [nick_new] 180 | on_nick(nick,nick_new) 181 | when 'INVITE' # 誰かが自分を招待した 182 | on_myinvite(nick,param[-1]) if param[1] == @irc_nick 183 | when 'PING' # クライアントの生存確認 184 | if @myhostname 185 | sendmess("PONG #{@myhostname} #{param[1]}\r\n") 186 | else 187 | # UltimateIRCdではMOTDより前にPINGが来る 188 | # 正確なクライアントのホスト名が不明なため、適当なPONGを返す 189 | sendmess("PONG dummy #{param[1]}\r\n") 190 | end 191 | when '376','422' # MOTDの終わり=ログインシーケンスの終わり 192 | # 自分のprefixを確認するためWHOISを発行 193 | sendmess("WHOIS #{@irc_nick}\r\n") 194 | when '311' # WHOISへの応答 195 | unless @myprefix 196 | # 自分のprefixを取得 197 | @myhostname = param[4] 198 | @myprefix = "#{param[3]}@#{@myhostname}" 199 | on_login 200 | end 201 | when '433' # nickが重複した 202 | on_error('433') # 正しくは重複しないnickで再度NICKを発行 203 | when '451' # 認証されていない 204 | on_error('451') 205 | @disp.puts('unknown login sequence!!') 206 | end 207 | end 208 | 209 | # 接続確立時の処理 210 | def on_connect 211 | @disp.puts "connect" 212 | dispmess(nil, 'Login...') 213 | 214 | sendmess("PASS #{@userinfo['pass']}\r\n") if @userinfo['pass'] && !@userinfo['pass'].empty? 215 | sendmess("NICK #{@irc_nick}\r\n") 216 | sendmess("USER #{@userinfo['user']} 0 * :#{@userinfo['realname']}\r\n") 217 | end 218 | 219 | # ここから下はオーバーライドする事を想定している 220 | 221 | # メッセージを表示(文字コードは変換しない) 222 | def dispmess(nick,mess) 223 | buff = Time.now.strftime('%H:%M:%S ') 224 | if nick 225 | buff = "#{buff}#{nick} #{mess}" 226 | else 227 | buff = "#{buff}#{mess}" 228 | end 229 | @disp.puts buff 230 | end 231 | 232 | # 接続・認証が完了し、チャンネルにJOINできる 233 | def on_login 234 | sendmess("JOIN #{@irc_channel} #{@channel_key}\r\n") 235 | end 236 | 237 | # MOTD(サーバのログインメッセージ) 238 | def on_motd(mess) 239 | dispmess(nil,mess) 240 | end 241 | 242 | # 通常メッセージ受信時の処理 243 | def on_priv(type,nick,mess) 244 | dispmess("<#{nick}>",mess) 245 | end 246 | 247 | # 今いるチャンネルの外からの通常メッセージ受信時の処理 248 | def on_external_priv(type,nick,channel,mess) 249 | end 250 | 251 | # JOIN受信時の処理 252 | def on_join(nick,channel) 253 | dispmess(nick,"JOIN #{channel}") 254 | end 255 | 256 | # PART受信時の処理 257 | def on_part(nick,channel) 258 | dispmess(nick,"PART #{channel}") 259 | end 260 | 261 | # QUIT受信時の処理 262 | def on_quit(nick,mess) 263 | dispmess(nick,"QUIT #{mess}") 264 | end 265 | 266 | # KICK受信時の処理 267 | def on_kick(nick,channel,mess,kicker) 268 | dispmess(nick,"KICK #{channel} #{kicker} #{mess}") 269 | end 270 | 271 | # 自分のJOIN受信時の処理 272 | def on_myjoin(channel) 273 | on_join(@irc_nick,channel) 274 | end 275 | 276 | # 自分のPART受信時の処理 277 | def on_mypart(channel) 278 | on_part(@irc_nick,channel) 279 | end 280 | 281 | # 自分のQUIT受信時の処理 282 | def on_myquit(mess) 283 | on_quit(@irc_nick,mess) 284 | end 285 | 286 | # 自分のKICK受信時の処理 287 | def on_mykick(channel,mess,kicker) 288 | on_kick(@irc_nick,channel,mess,kicker) 289 | end 290 | 291 | # NICK受信時の処理 292 | def on_nick(nick_old,nick_new) 293 | dispmess(nick_old,"NICK #{nick_new}") 294 | end 295 | 296 | # 自分がINVITEされた時の処理 297 | def on_myinvite(nick,channel) 298 | dispmess(nick,"INVITE #{channel}") 299 | end 300 | 301 | # エラーの時の処理 302 | def on_error(code) 303 | @disp.puts "Error: #{code}" 304 | sendmess("QUIT\r\n") # 面倒なので終了にしている 305 | end 306 | end 307 | -------------------------------------------------------------------------------- /manual/reudyman.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ロイディ Ver.3.09 マニュアル --- SSB 5 | 6 | 7 | 8 | 9 |

10 | [戻る] 11 | [ロ技研のトップ] 12 | [ロ技研掲示板] 13 | 14 |

ロイディ Ver.3.09 マニュアル

15 | 16 | 17 |

目次

18 | 19 | 35 | 36 |

37 | Windowsの人は「Windowsでの準備から実行まで」も併せてどうぞ。 38 |

39 | 40 |

バージョンアップの仕方(重要)

41 | 42 |

43 | 以前のバージョンからバージョンアップする場合は、 reudy309up.zip を解凍して、出てきたファイルを、以前のバージョンのファイルに上書きコピーしてください。 44 |

45 | 46 |

47 | Ver.3.04.1以前からバージョンアップする時には、自動でバックアップが取られるはずです。が、心配な人は public ディレクトリを手動でバックアップしておきましょう。 48 |

49 | 50 |

51 | まだ安定してないので、色々仕様が変わってすいません(汗)。当てはまる(●が付いている)注意点をよく読んでください。 52 |

53 | 54 |

55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
以前のバージョン注意点
3.01以下3.023.033.04.x3.05〜
ログを手動で追加したい時は、 public/log.txt に直接追加すればいいようになりました。 add_log.rb を使う必要は有りません。また、 public/log.txt を編集した時に、 public/setting.txt に remake_word_to_message_list を指定する必要も無くなりました。「ログを追加/編集する」参照。
Ruby/GDBM を使うようになりました。無くても動きますが、メモリ消費が激しくなります。「必要なもの」参照。
ログの中で、 < > &&lt; &gt; &amp; と書く必要は無くなりました。 今までのログの中の &lt; &gt; &amp; は、Ver.3.05以降での最初の起動時に、自動で < > & に変換されます。
public/words.txt のフォーマットが変わりました。Ver.3.05以降での最初の起動時に、自動的にフォーマットが変更されます。「単語を追加する」参照。
Ruby 1.8.1で動作確認するようになりました。Ruby 1.7以下では動かないかもしれません。
public/setting.txt の必須項目に nicks が追加されました。「設定」参照。
65 |

66 | 67 |

必要なもの

68 | 69 |
    70 |
  • Ruby 1.8.x。必須。Ruby 1.9.xでは動作しません。Windowsでのインストール方法はこちら
  • 71 |
  • Ruby/GDBM。強く推奨。無いとメモリを大量消費します。*1*2Windowsでのインストール方法はこちら
  • 72 |
  • それなりのメモリ。*1
  • 73 |
  • ロイディのソース
  • 74 |
75 | 76 |

77 | *1 ログ約10万行(約4MB)、単語約1万語の場合、GDBM有りなら約10MB、GDBM無しなら約90MB消費。
78 | *2 mingw版/mswin32版のRubyでは、Ruby/GDBMが無いとエラーダイアログが出ます。Ruby/GDBMを入れるか、Rubyのディレクトリの中から gdbm.so を探して削除してください。 79 |

80 | 81 |

IRC版の使い方

82 | 83 |

84 | ここではIRCでロイディを動かす方法を説明します。 85 |

86 | 87 |

88 | まず、 public/setting.txt を編集します。 host 、 port にIRCサーバのホスト名とポート番号を、 channel と info_channel にロイディを動かしたいチャンネルの名前を指定します。 public/setting.txt の各項目についての詳細は「設定」を見てください。 89 |

90 | 91 |

92 | 設定が終わったら、ターミナル(やコマンドプロンプト)で 93 |

94 | 95 |

96 |     ruby irc_reudy.rb public 97 |

98 | 99 |

100 | と入力してください。*5 101 |

102 | 103 |

104 | 105 |     ログロード中...
106 |     単語ロード中...
107 |     接続開始...
108 |
109 |

110 | 111 |

112 | となれば、成功です。 public/setting.txt で指定したチャンネルにLimeChatやChocoaなどのIRCクライアントでアクセスして、会話をお楽しみください。といっても、最初のうちは全然まともにしゃべれませんが…。 113 |

114 | 115 |

116 | ロイディを終了するには、ターミナルでCtrl+Cを押すか、IRC上で「ロイディ、終了しなさい」と発言してください。 117 |

118 | 119 |

120 | コマンドで指定した public というのは、 settng.txt とかを含むディレクトリ名です。 public 以外のディレクトリを作って、複数の設定/記憶を使い分ける事ができます。 121 |

122 | 123 |

124 | 125 | *5 irc_reudy.rb を含むディレクトリに移動(cd)して実行しないとエラーになります。 126 | 127 |

128 | 129 |

設定

130 | 131 |

132 | public/setting.txt には、各行に「項目名」と「値」を半角スペース(or タブ)で区切って書きます。文字コードはEUCにしてください。 133 |

134 | 135 |

136 | の項目は必ず設定してください。それ以外はそのままでもとりあえず動きます。 137 |

138 | 139 |

140 |

IRC専用の設定項目

141 |

142 | 143 |

144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
項目名意味値の例
hostIRCサーバ名。irc.fujisawa.wide.ad.jp
portIRCサーバのポート。6667
encodingIRCサーバの文字コード。LimeChatで同じサーバにログインするときに、サーバのプロパティの[文字コード]にISO-2022-JP(デフォルト)を指定している場合は JIS 、UTF-8を指定している場合は UTF8 と書いてください。JIS
channelロイディが会話をするチャンネル。メインチャンネルと呼びます。#reudy_test
info_channelロイディが覚えた単語が流れるチャンネル。中の人モードでも使います。Infoチャンネルと呼びます。 channel と同じでもいいです。#reudy_test
login_passwordIRCサーバのログインパスワード。Wideとかでは不要。(空欄)
channel_keyパスワード付きのチャンネルに入る場合に必要。login_password(サーバに入るのに必要なパスワード)とは違うので注意してください。(空欄)
respond_to_noticetrueなら、noticeメッセージにも反応する。true / false
respond_to_externaltrueなら、チャンネル外部のメッセージにも反応する。true / false
speak_with_privmsgtrueなら、privmsgで話す。falseなら、noticeで話す。true / false
auto_reconnecttrueなら、回線が切れたときに、自動で再接続する。true / false
teacher_mode中の人モード」を有効にするかどうか。true / false
nameIRCの /whois で「名前」として表示されるもの。適当でいいです。Reudy
real_nameIRCの /whois で「本名」として表示されるもの。適当でいいです。Bot Reudy
161 |

162 | 163 |

164 |

共通の設定項目

165 |

166 | 167 |

168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
nickチャット用のニックネーム。IRCでは、他の人とかぶるとログインできません。MyReudy
nicks自分の名前として認識する単語。半角カンマで区切って複数指定できます。Reudy,reudy,ロイディ
default_mode最初のモード。0が沈黙モード、1が寡黙モード、2が通常モード、3が饒舌モード。0 / 1 / 2 / 3
joining_message入室時のメッセージ。省略可。こんにちは。
leaving_message移動時の退室メッセージ。省略可。さようなら。
private_greeting入室した他人に個人宛てで送るメッセージ。省略可。こんにちは。
disable_commandstrueなら、「設定を更新」以外のコマンドを無効化。true / false
disable_studyingtrueなら、反応するだけで学習はしない。true / false
target_nickニックネーム(Nick)を指定すると、その人の物まねをする。*6Gimite
forbidden_nickここでニックネーム(Nick)を指定した人の発言は使わない。*6tobocchi|ProzaKc|hAnE
wait_before_speak反応するまでに何秒 間(ま)を置くかの目安。小数も可。あまり短くするとサーバに蹴られる事が有ります。2
wait_before_info単語記憶メッセージを流す間隔(秒)。小数も可。あまり短くするとサーバに蹴られる事が有ります。0.2
182 |

183 | 184 |

185 | *6 正規表現を使えます。「正規表現について」参照。 186 |

187 | 188 |

コマンド

189 | 190 |

191 | ロイディがいるチャンネルで発言すると、ロイディにコマンドを送れます。 192 |

193 | 194 |

195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
コマンド(発言)効果
ロイディ、設定を更新。settings.txtを読み直します。ただし、更新できない設定も有ります。
ロイディ、沈黙モード。沈黙モードになります。ほとんどしゃべりません。
ロイディ、寡黙モード。寡黙モードになります。時々しゃべります。
ロイディ、通常モード。通常モードになります。普通にしゃべります。
ロイディ、饒舌モード。饒舌モードになります。しゃべりまくります。
ロイディ、「〜」を覚えさせたのは誰?その単語を誰に教わったかを答えます。
ロイディ、〜のものまねをしなさい。その人のものまねを始めます。
ロイディ、終了しなさい。プログラムを終了します。
206 |

207 | 208 |

209 | 設定で nicks を書き換えた場合は、「ロイディ」の部分を読み替えてください。 210 |

211 | 212 |

213 | 設定で disable_commands を true にすると、「設定を更新」以外のコマンドが無効になります。 214 |

215 | 216 |

ログを追加/編集する

217 | 218 |

219 | ロイディのログは public/log.txt に入っています。各行が 220 |

221 | 222 |

223 |     発言者 タブ 発言内容 224 |

225 | 226 |

227 | となっています。このフォーマットを守れば、自由にログを追加、編集できます。文字コードはEUCにしてください。EUC以外で保存すると、データが壊れます。 228 |

229 | 230 |

231 | ロイディ実行中に public/log.txt をいじらないでください。ややこしい事態になります。 232 |

233 | 234 |

235 | ログをファイルの最後に追加すると、次の起動の時に 236 |

237 | 238 |

239 |     public/log.txt に追加されたログを読み込み中... 240 |

241 | 242 |

243 | と表示されます。追加分について単語の抽出とかをするので、しばらく時間がかかります。 244 |

245 | 246 |

247 | public/log.txt の途中を変更すると、 248 |

249 | 250 |

251 |     public/log.txt の途中が変更されています。内部データを作り直します... 252 |

253 | 254 |

255 | と表示されます。この場合は内部データを一から作り直すので、かなり時間がかかります。 256 |

257 | 258 |

単語を追加する

259 | 260 |

261 | ロイディが覚えた単語は public/words.txt に入っています。Ver.3.05からフォーマットが変わって、単純に1行に1個の単語を書き並べたものになりました。 262 |

263 | 264 |

265 | ロイディは自動で単語を覚えますが、 public/words.txt に手動で書き足すのもOKです。文字コードはEUCにしてください。EUC以外で保存すると、データが壊れます。 266 |

267 | 268 |

269 | ロイディ実行中に public/words.txt をいじらないでください。ややこしい事態になります。 270 |

271 | 272 |

273 | public/words.txt を変更すると、 274 |

275 | 276 |

277 |     public/words.txt が変更されたようです。単語を読み込み中... 278 |

279 | 280 |

281 | と表示されます。追加された単語を探しにいくので、しばらく時間がかかります。 282 |

283 | 284 |

285 | 現バージョンでは、単語の削除はできません。( public/words.txt から削除しても、内部のデータからは削除されずに残ります)。 286 |

287 | 288 |

ロイディを素早く起動する

289 | 290 |

291 | ロイディは起動時に public/log.txt が手動で変更されてないかどうかをチェックします。ログが大きくなってくると、これはそれなりに時間がかかります。 292 |

293 | 294 |

295 | 296 |     ruby irc_reudy.rb -f public 297 | 298 |

299 | 300 |

301 | のように -f をつけると、このチェックを省略できます。 302 |

303 | 304 |

中の人モード

305 | 306 |

307 | この機能はVer.3.07で追加されました。また、今のところIRC版でのみ有効です。 308 |

309 | 310 |

311 | ロイディは通常のモードでは、人間同士の会話や、ロイディと人間との会話から、自動的に反応を学習します。 312 |

313 | 314 |

315 | これに対して「中の人モード」では、特定の「中の人」が、ロイディの発言を「修正」していくことで、ロイディに反応を覚えこませることができます。 316 |

317 | 318 |

319 | 中の人モードを有効にするには、設定の teacher_mode を true にします。 320 |

321 | 322 |

323 | 中の人がロイディの発言を修正するには、Infoチャンネルで発言します。例えば、こんな感じです。 channel が #reudyroom 、 info_channel が #reudyinfo になってるとします。 324 | 325 |

326 | 327 |
328 | <#reudyroom:Human1> こんちわ〜。
329 | <#reudyroom:RReudy> さようなら。
330 | <#reudyinfo:Teacher> こんにちは。
331 | 
332 | 333 |

334 | これでロイディは「こんちわ〜。」→「こんにちは。」という反応のパターンを覚えました。 335 |

336 | 337 |

338 | この方法では、中の人はロイディの発言を「修正」するだけです。ロイディが反応しなかった発言に対する反応は登録できません。 339 |

340 | 341 |

342 | メインチャンネルの会話とは関係なく、ロイディに反応を覚えさせるには、次のようにします。 343 | 344 |

345 | 346 |
347 | <#reudyinfo:Teacher> こんちわ〜。→→こんにちは。
348 | 
349 | 350 |

351 | 「→」記号を2つ続ける所に注意してください。 352 |

353 | 354 |

355 | teacher_mode を true にすると、 356 |

357 | 358 |
    359 |
  • メインチャンネルの会話からの学習は無効になります。
  • 360 |
  • ロイディの発言には、できるだけ中の人から教わった反応を使うようになります。過去にメインチャンネルで学習したデータは、原則として使いません。
  • 361 |
362 | 363 |

正規表現について

364 | 365 |

366 | 設定の target_nick と forbidden_nick には、正規表現を使えます。詳しくはぐぐったりしてみてください。ここでは、簡単な使い方だけ。 367 |

368 | 369 |

370 | 複数のニックネームを指定するには、 | で区切ります。例えば、 371 |

372 | 373 |

374 |     forbidden_nick  RuThemis|ProzaKc 375 |

376 | 377 |

378 | は、「RuThemisとProzaKcの発言を使わない」という意味になります。 379 |

380 | 381 |

382 | 記号には、(上の | 以外にも)特別な意味を持つものが有ります。普通の記号として使うには、記号の前に \ を入れてください。例えば、 [DIABL0] さんの物まねをさせるには、 383 |

384 | 385 |

386 |     target_nick  \[DIABL0\] 387 |

388 | 389 |

390 | とします。 391 |

392 | 393 |

出力文字コードを変える

394 | 395 |

396 | ロイディのターミナルへの出力文字コードは、標準ではShift-JISです。これを変えるには、 irc_reudy.rb をエディタで開いて、6行目の 397 |

398 | 399 |

400 |     $OUT_KCODE= "SJIS" #出力文字コード 401 |

402 | 403 |

404 | の "SJIS" の部分を書き換えます。 "SJIS" 以外に、 "EUC""UTF-8""JIS" を指定できます。 405 |

406 | 407 |

ファイル一覧

408 | 409 |

410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 |
ファイル名説明
irc_reudy.rbこのファイルを使ってIRC版ロイディを起動します。「IRC版の使い方」参照。
lib/*ロイディの内部処理で使われるもの。直接は使いません。
public/setting.txtロイディの設定をここに書いてください。「設定」参照。
public/log.txtロイディが覚えた発言のログ。追加/編集できます。「ログを追加/編集する」参照。
public/words.txtロイディが覚えた単語のリスト。追加もできます。「単語を追加する」参照。
public/log.dat内部使用のファイル。中身は log.txt とほとんど同じですが、このファイルをいじってはいけません。データが壊れます。
public/words.dat
public/similar.gdbm
public/version.dat
内部使用のファイル。いじったり、消したりしないでください。
420 |

421 | 422 |

ライセンス

423 | 424 |

425 | 修正BSDライセンスです。つまり、著作権表示さえすれば、改造、再配布、これを使ったソフトの配布などは全て自由です。ただし、何も保証しないし、仮に何か起きても僕は責任を取りません。詳しくはLICENCE.txtをどうぞ。 426 |

427 | 428 |

429 | ただし、irc-client.rbだけはPublic Domain Softwareです。 430 |

431 | 432 |

配布者

433 | 434 |

435 | 質問、バグ、要望など有りましたら、掲示板までどうぞ。 436 |

437 | 438 |

439 | 元祖ロイディの居場所とロイディの解説は、こちら。
440 | http://www.rogiken.org/SSB/ 441 | 442 |

443 |
444 |

445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /lib/reudy/reudy.rb: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #Copyright (C) 2003 Gimite 市川 3 | #Modified by Glass_saga 4 | 5 | require $REUDY_DIR+'/wordset' 6 | require $REUDY_DIR+'/word_searcher' 7 | require $REUDY_DIR+'/message_log' 8 | require $REUDY_DIR+'/similar_searcher' 9 | require $REUDY_DIR+'/word_associator' 10 | require $REUDY_DIR+'/wtml_manager' 11 | require $REUDY_DIR+'/attention_decider' 12 | require $REUDY_DIR+'/response_estimator' 13 | require $REUDY_DIR+'/reudy_common' 14 | require 'yaml' 15 | 16 | unless Encoding.default_external == __ENCODING__ 17 | STDOUT.set_encoding(Encoding.default_external, __ENCODING__) 18 | STDERR.set_encoding(Encoding.default_external, __ENCODING__) 19 | STDIN.set_encoding(Encoding.default_external, __ENCODING__) 20 | end 21 | 22 | module Gimite 23 | class Reudy 24 | include Gimite 25 | 26 | def initialize(dir, fixedSettings = {},db="pstore",mecab=nil) 27 | @attention = nil 28 | 29 | #設定を読み込む。 30 | @db = db #使用するDBの名前 31 | if mecab 32 | begin 33 | require $REUDY_DIR+'/tango-mecab' #単語の抽出にmecabを使用する 34 | rescue => ex 35 | warn ex.message 36 | require $REUDY_DIR+'/tango-mgm' 37 | end 38 | else 39 | require $REUDY_DIR+'/tango-mgm' 40 | end 41 | @fixedSettings = fixedSettings 42 | @settingPath = dir + '/setting.yml' 43 | @settings = {} 44 | loadSettings 45 | @autoSave = !@settings[:disable_auto_saving] 46 | 47 | #働き者のオブジェクト達を作る。 48 | @log = MessageLog.new(dir + '/log.yml') 49 | @log.addObserver(self) 50 | warn "ログロード終了" 51 | @wordSet = WordSet.new(dir + '/words.yml') 52 | @wordSearcher = WordSearcher.new(@wordSet) 53 | @wtmlManager = WordToMessageListManager.new(@wordSet, @log, @wordSearcher) 54 | @extractor = WordExtractor.new(14, method(:onAddWord)) 55 | @simSearcher = SimilarSearcher.new(dir + '/db', @log,@db) 56 | @associator = WordAssociator.new(dir + '/assoc.txt') 57 | @attention = AttentionDecider.new 58 | @attention.setParameter(attentionParameters) 59 | @resEst = ResponseEstimator.new(@log, @wordSearcher, method(:isUsableBaseMsg), method(:canAdoptWord)) 60 | warn "単語ロード終了" 61 | #その他インスタンス変数の初期化。 62 | @client = nil 63 | @lastSpeachInput = nil 64 | @lastSpeach = nil 65 | @inputWords = [] 66 | @newInputWords = [] 67 | @recentUnusedCt = 100 #最近n個の発言は対象としない 68 | @repeatProofCt = 50 #過去n発言で使ったベース発言は再利用しない 69 | @recentBaseMsgNs = Array.new(@repeatProofCt) #最近使ったベース発言番号 70 | @thoughtFile = open(dir + "/thought.txt", "a") #思考過程を記録するファイル 71 | @thoughtFile.sync = true 72 | 73 | setWordAdoptBorder 74 | end 75 | 76 | #設定をファイルからロード 77 | def loadSettings 78 | File.open(@settingPath) do |file| 79 | @settings = YAML.load(file) 80 | end 81 | @settings.merge!(@fixedSettings) 82 | #メンバ変数を更新 83 | @targetNickReg = Regexp.new(@settings[:target_nick] || "", Regexp::IGNORECASE) 84 | #これにマッチしないNickの発言は、ベース発言として使用不能 85 | if @settings[:forbidden_nick] && !@settings[:forbidden_nick].empty? 86 | @forbiddenNickReg = Regexp.new(@settings[:forbidden_nick], Regexp::IGNORECASE) 87 | else 88 | @forbiddenNickReg = /(?!)/o #何にもマッチしない正規表現 89 | end 90 | @myNicks = @settings[:nicks] #これにマッチするNickの発言は、ベース発言として使用不能 91 | @my_nicks_regexp = Regexp.new(@myNicks.map{|n| Regexp.escape(n) }.join("|")) 92 | changeMode(@settings[:default_mode].to_i) #デフォルトのモードに変更 93 | end 94 | 95 | #チャットクライアントの指定 96 | attr_accessor :client,:settings 97 | 98 | #モードを変更 99 | def changeMode(mode) 100 | return false if mode == @mode 101 | @mode = mode 102 | @attention.setParameter(attentionParameters) if @attention 103 | updateStatus 104 | return true 105 | end 106 | 107 | def updateStatus 108 | @client.status = ["沈黙", "寡黙", nil, "饒舌"][@mode] if @client 109 | end 110 | 111 | #注目判定器に与えるパラメータ。 112 | def attentionParameters 113 | case @mode 114 | when 0 #沈黙モード。 115 | return { \ 116 | :min => 0.001, \ 117 | :max => 0.001, \ 118 | :default => 0.001, \ 119 | :called => 0.001, \ 120 | :self => 0.0, \ 121 | :ignored => 0.0 \ 122 | } 123 | when 1 #寡黙モード。 124 | return { \ 125 | :min => 0.1, \ 126 | :max => 0.3, \ 127 | :default => 0.1, \ 128 | :called => 1.1, \ 129 | :self => 0.005, \ 130 | :ignored => 0.002 \ 131 | } 132 | when 2 #通常モード。 133 | return { \ 134 | :min => 0.5, \ 135 | :max => 1.1, \ 136 | :default => 0.5, \ 137 | :called => 1.1, \ 138 | :self => 0.3, \ 139 | :ignored => 0.002 \ 140 | } 141 | when 3 #饒舌モード。 142 | return { \ 143 | :min => 0.8, \ 144 | :max => 1.1, \ 145 | :default => 0.8, \ 146 | :called => 1.1, \ 147 | :self => 0.8, \ 148 | :ignored => 0.01 \ 149 | } 150 | when 4 #必ず応答するモード。 151 | return { \ 152 | :min => 1.1, \ 153 | :max => 1.1, \ 154 | :default => 1.1, \ 155 | :called => 1.1, \ 156 | :self => 0.8, \ 157 | :ignored => 0.003 \ 158 | } 159 | end 160 | end 161 | 162 | #単語がこれより多く出現してたら置換などの対象にしない、という 163 | #ボーダを求めて@wordAdoptBorderに代入。 164 | def setWordAdoptBorder 165 | if @wordSet.words.empty? 166 | @wordAdoptBorder = 0 167 | else 168 | msgCts = @wordSet.words.map{|w| w.mids.size } 169 | msgCts.sort! 170 | msgCts.reverse! 171 | @wordAdoptBorder = msgCts[msgCts.size / 50] 172 | end 173 | end 174 | 175 | #その単語が置換などの対象になるか 176 | def canAdoptWord(word) 177 | word.mids.size < @wordAdoptBorder 178 | end 179 | 180 | #発言をベース発言として使用可能か。 181 | def isUsableBaseMsg(msgN) 182 | size = @log.size 183 | return false if msgN >= size #存在しない発言。 184 | msg = @log[msgN] 185 | return unless msg #空行。削除された発言など。 186 | return false if !@settings[:teacher_mode] && size > @recentUnusedCt && msgN >= size - @recentUnusedCt #発言が新しすぎる。(中の人モードでは無効) 187 | nick = msg.fromNick 188 | return false if nick == "!" #自分自身の発言。 189 | return false if !(nick =~ @targetNickReg) || nick =~ @forbiddenNickReg #この発言者の発言は使えない。 190 | return false if @recentBaseMsgNs.include?(msgN) #最近そのベース発言を使った。 191 | return true 192 | end 193 | 194 | #mid番目の発言への返事(と思われる発言)について、[発言番号,返事らしさ]を返す。 195 | #ただし、ベース発言として使用できるものだけが対象。 196 | #該当するものが無ければ[nil,0]を返す。 197 | def responseTo(mid, debug = false) 198 | if @settings[:teacher_mode] 199 | if isUsableBaseMsg(mid+1) && @log[mid].fromNick == "!input" 200 | return [mid+1, 20] 201 | else 202 | return [nil, 0] 203 | end 204 | else 205 | return @resEst.responseTo(mid, debug) 206 | end 207 | end 208 | 209 | #類似発言検索用のフィルタ 210 | def similarSearchFilter(msgN) 211 | !responseTo(msgN).first.nil? 212 | end 213 | 214 | #sentence中の自分のNickをtargetに置き換える。 215 | def replaceMyNicks(sentence, target) 216 | sentence.gsub(@my_nicks_regexp, target) 217 | end 218 | 219 | #入力文章から既知単語を拾う。 220 | def pickUpInputWords(input) 221 | input = replaceMyNicks(input, " ") 222 | @newInputWords = @wordSearcher.searchWords(input).select{ |w| canAdoptWord(w) } #入力に含まれる単語を列挙 223 | #入力に単語が無い場合は、時々入力語をランダムに変更 224 | if @newInputWords.empty? && rand(50).zero? 225 | word = @wordSet.words.sample 226 | @newInputWords.push(word) if canAdoptWord(word) 227 | end 228 | #連想される単語を追加 229 | assoc_words = @newInputWords.map{|w| @associator.associate(w.str) } 230 | assoc_words.compact! 231 | assoc_words.map!{|s| Word.new(s) } 232 | @newInputWords.concat(assoc_words) 233 | #入力語の更新 234 | unless @newInputWords.empty? 235 | if rand(5).nonzero? 236 | @inputWords.replace(@newInputWords) 237 | else 238 | @inputWords.concat(@newInputWords) 239 | end 240 | end 241 | end 242 | 243 | #「単語を除く文字数」から発言を採用するかを決める。 244 | #「単語だけ」に等しい発言は採用されにくいようにする。 245 | #単語が無い発言は確実に採用され、このメソッドは使われない。 246 | def shouldAdoptSaying(additionalLen) 247 | case additionalLen 248 | when 0 249 | false 250 | when 1 251 | rand < 0.125 252 | when 2, 3 253 | rand < 0.25 254 | when 4...7 255 | rand < 0.75 256 | else 257 | true 258 | end 259 | end 260 | 261 | #inputWords中の単語を含む各発言について、ブロックを繰り返す。 262 | #ブロックは発言番号を引数に取る。 263 | #発言の順序はランダム。 264 | def eachMsgContainingWords(input_words) 265 | input_words.shuffle.each do |word| 266 | word.mids.shuffle.each do |mid| 267 | yield(mid) 268 | end 269 | end 270 | end 271 | 272 | #共通の単語を持つ発言と、その返事の発言番号を返す。 273 | #適切なものが無ければ、[nil, nil]。 274 | def getBaseMsgUsingKeyword(inputWords) 275 | maxMid = maxResMid = nil 276 | maxProb = 0 277 | i = 0 278 | eachMsgContainingWords(inputWords) do |mid| 279 | resMid, prob = responseTo(mid, true) 280 | if resMid 281 | if prob > maxProb 282 | maxMid = mid 283 | maxResMid = resMid 284 | maxProb = prob 285 | end 286 | i += 1 287 | break if i >= 5 288 | end 289 | end 290 | dprint("共通単語発言", @log[maxMid].body) if maxMid 291 | return [maxMid, maxResMid] 292 | end 293 | 294 | #類似発言と、その返事の発言番号を返す。 295 | #適切なものが無ければ、[nil, nil]。 296 | def getBaseMsgUsingSimilarity(sentence) 297 | maxMid = maxResMid = nil 298 | maxProb = 0 299 | i = 0 300 | @simSearcher.eachSimilarMsg(sentence) do |mid| 301 | resMid, prob = responseTo(mid, true) 302 | if resMid 303 | if prob > maxProb 304 | maxMid = mid 305 | maxResMid = resMid 306 | maxProb = prob 307 | end 308 | i += 1 309 | break if i >= 5 310 | end 311 | end 312 | dprint("類似発言", @log[maxMid].body, maxProb) if maxMid 313 | return [maxMid, maxResMid] 314 | end 315 | 316 | #msgN番の発言を使ったベース発言の文字列。 317 | def getBaseMsgStr(msg_n) 318 | str = @log[msg_n].body 319 | str.replace($1) if str =~ /^(.*)[<>]/ && $1.size >= str.size / 2 #文の後半に[<>]が有れば、その後ろはカット。 320 | str 321 | end 322 | 323 | #base内の既知単語をnewWordsで置換したものを返す。 324 | #toForceがfalseの場合、短すぎる文章になってしまった場合はnilを返す。 325 | def replaceWords(base, new_words, toForce) 326 | #baseを単語の前後で分割してpartsにする。 327 | parts = [base] 328 | @wordSet.words.each do |word| 329 | next if word.str.empty? 330 | if @wordSearcher.hasWord(base, word) && canAdoptWord(word) 331 | newParts = [] 332 | parts.each_with_index do |part,i| 333 | if (i % 2).zero? 334 | word_regexp = /^(.*?)#{Regexp.escape(word.str)}(.*)$/ 335 | while part =~ word_regexp 336 | newParts.push($1, word.str) 337 | part = $2 338 | end 339 | end 340 | newParts.push(part) 341 | end 342 | parts = newParts 343 | end 344 | end 345 | #先頭から2番目以降の単語の直前でカットしたりしなかったり。 346 | wordCt = (parts.size-1) / 2 347 | if parts.size > 1 348 | cutPos = rand(wordCt) * 2 + 1 349 | parts.replace(parts[cutPos..-1].unshift("")) if cutPos > 1 350 | end 351 | #単語を除いた文章が短すぎるものはある確率で却下。 352 | if wordCt.nonzero? && !toForce && !shouldAdoptSaying(sigma(0...parts.size){|i| (i % 2).zero? ? parts[i].size : 0 }) 353 | return nil 354 | end 355 | #単語を置換。 356 | new_words.shuffle.each do |new_word| 357 | new_word_str = new_word.str 358 | old_word_str = parts[rand(wordCt)*2+1] 359 | 0.upto(wordCt-1) do |j| 360 | parts[j*2+1] = new_word_str if parts[j*2+1] == old_word_str 361 | end 362 | break if rand < 0.5 363 | end 364 | output = parts.join 365 | #閉じ括弧が残った場合に開き括弧を補う。 366 | #入れ子になってたりしたら知らない。 367 | case output 368 | when /^[^「」]*」/ 369 | output.replace("「#{output}") 370 | when /^[^()]*)/ 371 | output.replace("(#{output}") 372 | when /^[^()]*\)/ 373 | output.replace("(#{output}") 374 | end 375 | return output 376 | end 377 | 378 | #自由発言の選び方を記録する。 379 | def recordThought(pattern, simMid, resMid, words, output) 380 | @thoughtFile.puts [@log.size-1, pattern, simMid, resMid, words.map{ |w| w.str }.join(","), output].join("\t") 381 | end 382 | 383 | #自由に発言する。 384 | def speakFreely(fromNick, origInput, mustRespond) 385 | input = replaceMyNicks(origInput, " ") 386 | output = nil 387 | simMsgN, baseMsgN = getBaseMsgUsingSimilarity(input) #まず、類似性を使ってベース発言を求める。 388 | if !@newInputWords.empty? 389 | if baseMsgN 390 | output = replaceWords(getBaseMsgStr(baseMsgN), @inputWords, mustRespond) 391 | recordThought(1, simMsgN, baseMsgN, @newInputWords, output) if output 392 | else 393 | simMsgN, baseMsgN = getBaseMsgUsingKeyword(@newInputWords) 394 | output = getBaseMsgStr(baseMsgN) if baseMsgN 395 | recordThought(2, simMsgN, baseMsgN, @newInputWords, output) if output 396 | end 397 | else 398 | if baseMsgN 399 | output = getBaseMsgStr(baseMsgN) 400 | unless @wordSearcher.searchWords(output).empty? 401 | if mustRespond 402 | output = replaceWords(output, @inputWords, true) 403 | else 404 | output = nil 405 | end 406 | end 407 | recordThought(3, simMsgN, baseMsgN, @inputWords, output) if output 408 | else 409 | if mustRespond && !@inputWords.empty? 410 | simMsgN, baseMsgN = getBaseMsgUsingKeyword(@inputWords) #最新でない入力語も使ってキーワード検索。 411 | output = getBaseMsgStr(baseMsgN) if baseMsgN 412 | recordThought(4, simMsgN, baseMsgN, @inputWords, output) if output 413 | end 414 | end 415 | end 416 | if mustRespond && !output 417 | log_size = @log.size 418 | 2000.times do 419 | msgN = rand(log_size) 420 | if isUsableBaseMsg(msgN) 421 | baseMsgN = msgN 422 | output = getBaseMsgStr(baseMsgN) 423 | break 424 | end 425 | end 426 | end 427 | if output 428 | #最近使ったベース発言を更新 429 | @recentBaseMsgNs.shift 430 | @recentBaseMsgNs.push(baseMsgN) 431 | output = replaceMyNicks(output, fromNick) #発言中の自分のNickを相手のNickに変換 432 | speak(origInput, output) #実際に発言。 433 | end 434 | end 435 | 436 | #自由発話として発言する。 437 | def speak(input, output) 438 | @lastSpeachInput = input 439 | @lastSpeach = output 440 | studyMsg("!", output) #自分の発言を記憶する。 441 | @client.outputInfo("「#{input}」に反応した。") if @settings[:teacher_mode] 442 | @attention.onSelfSpeak(@wordSearcher.searchWords(output)) 443 | @client.speak(output) 444 | end 445 | 446 | #定型コマンドを処理。 447 | #入力が定型コマンドであれば応答メッセージを返す。 448 | #そうでなければnilを返す。ただし、終了コマンドだったら:exitを返す。 449 | def processCommand(input) 450 | if input =~ /設定を更新/ 451 | loadSettings 452 | return "設定を更新しました。" 453 | end 454 | return nil if @settings[:disable_commands] #コマンドが禁止されている場合 455 | case input 456 | when /黙れ|黙りなさい|黙ってろ|沈黙モード/ 457 | return changeMode(0) ? "沈黙モードに切り替える。" : "" 458 | when /寡黙モード/ 459 | return changeMode(1) ? "寡黙モードに切り替える。" : "" 460 | when /通常モード/ 461 | return changeMode(2) ? "通常モードに切り替える。" : "" 462 | when /饒舌モード/ 463 | return changeMode(3) ? "饒舌モードに切り替える。" : "" 464 | when /休んで良いよ|終了しなさい/ 465 | save 466 | @client.exit 467 | return :exit 468 | when /([\x21-\x7e]+)の(?:もの|モノ|物)(?:まね|真似)/ #半角文字を抽出する正規表現 469 | begin 470 | @targetNickReg = Regexp.new($1, Regexp::IGNORECASE) 471 | return "#{$1}のものまねを開始する。" 472 | rescue RegexpError 473 | return "正規表現が間違っている。" 474 | end 475 | when /(?:もの|モノ|物)(?:まね|真似).*(?:解除|中止|終了|やめろ|やめて)/ 476 | @targetNickReg = /(?!)/ 477 | return "物まねを解除する。" 478 | end 479 | if input =~ /覚えさせた|教わった/ && input.include?("誰") && input =~ /「(.+?)」/ 480 | wordStr = $1 481 | if wordIdx = @wordSet.words.index(Word.new(wordStr)) 482 | author = @wordSet.words[wordIdx].author 483 | if !author.empty? 484 | return "#{author}さんに。>#{wordStr}" 485 | else 486 | return "不確定だ。>#{wordStr}" 487 | end 488 | else 489 | return "その単語は記憶していない。" 490 | end 491 | end 492 | return nil #定型コマンドではない。 493 | end 494 | 495 | #通常の発言を学習。 496 | def studyMsg(fromNick, input) 497 | return if @settings[:disable_studying] 498 | if @settings[:teacher_mode] 499 | @fromNick = fromNick 500 | @extractor.processLine(input) #単語の抽出のみ。 501 | else 502 | @log.addMsg(fromNick, input) 503 | end 504 | end 505 | 506 | #学習内容を手動保存 507 | def save 508 | @wordSet.save 509 | end 510 | 511 | #ログに発言が追加された。 512 | def onAddMsg 513 | msg = @log[-1] 514 | @fromNick = msg.fromNick unless msg.fromNick == "!" 515 | @extractor.processLine(msg.body) unless @settings[:teacher_mode] #中の人モードでは、単語の抽出は別にやる。 516 | #@extractor以外のオブジェクトは自力で@logを監視しているので、 517 | #ここで何かする必要は無い。 518 | end 519 | 520 | #ログがクリアされた。 521 | def onClearLog 522 | end 523 | 524 | #単語が追加された 525 | def onAddWord(wordStr) 526 | if @wordSet.addWord(wordStr, @fromNick) 527 | if @client 528 | @client.outputInfo("単語「#{wordStr}」を記憶した。") 529 | else 530 | puts "単語「#{wordStr}」を記憶した。" 531 | end 532 | @wordSet.save if @autoSave 533 | end 534 | end 535 | 536 | #接続を開始した 537 | def onBeginConnecting 538 | warn "接続開始..." 539 | end 540 | 541 | #自分が入室した 542 | def onSelfJoin 543 | updateStatus 544 | end 545 | 546 | #他人が入室した 547 | def onOtherJoin(fromNick) 548 | end 549 | 550 | #他人が発言した。 551 | def onOtherSpeak(from_nick, input, should_ignore = false) 552 | output = nil #発言。 553 | called = @myNicks.any?{|n| input.include?(n) } 554 | output = called ? processCommand(input) : nil 555 | if output 556 | @client.speak(output) if output != :exit && !output.empty? 557 | else #定型コマンドではない。 558 | @lastSpeach = input 559 | studyMsg(from_nick, input) 560 | pickUpInputWords(input) 561 | prob = @attention.onOtherSpeak(from_nick, input, called) 562 | dprint("発言率", prob, @attention.to_s) #発言率を求める。 563 | speakFreely(from_nick, input, prob > 1.0) if (!should_ignore && rand < prob) || prob > 1.0 #自由発話。 564 | end 565 | end 566 | 567 | #制御発言(infoでの発言)があった。 568 | def onControlMsg(str) 569 | return if @settings[:disable_studying] || !@settings[:teacher_mode] 570 | if str =~ /^(.+)→→(.+)$/ 571 | input = $1 572 | output = $2 573 | else 574 | input = @lastSpeachInput 575 | output = str 576 | end 577 | if input 578 | @log.addMsg("!input", input) 579 | @log.addMsg("!teacher", output) 580 | @client.outputInfo("反応「#{input}→→#{output}」を学習した。" ) if @client 581 | end 582 | end 583 | 584 | #沈黙がしばらく続いた。 585 | def onSilent 586 | prob = @attention.onSilent 587 | if rand < prob && @lastSpeach 588 | speakFreely(@fromNick, @lastSpeach, prob > rand * 1.1) #自発発言。 589 | #自発発言では、発言が無い限り、同じ発言を対象にしつづける。 590 | #このせいで全然しゃべらなくなるのを防ぐため、時々mustRespondをONにする。 591 | end 592 | end 593 | end 594 | end 595 | --------------------------------------------------------------------------------