「プログラミングに詳しい香具師ちょっと来い」の390で提示された要件をRubyで実装したものを説明する。
Ver.3 2007/05/15 構成を修正。ほか細々
Ver.2 2007/05/10 AutoPlayerのバグを修正
rdefs.rbというスクリプトによって作成。
図はOpenOffice.org Draw。
module Rands def bool_rand def array_rand(array) def char_rand
class GameMaster def initialize def log_search(word) def log_size def dictionary_load(f) def entry(pl) def now def succ def each def twice?(word) private def turn_player
class Player def initialize(id, dictionary=[], gm=nil) def game_master=(gm) attr_reader :id attr_accessor :dictionary
class ManualPlayer < Player include Rands def answer(before) def before_input private def read_answer(before, prompt, err_msg)
class AutoPlayer < Player include Rands def answer(before) private def think_answer(before)
USAGE = "ruby shiritori.rb DICT_FILE" begin ##(省略)## rescue RuntimeError => ex $stderr.puts "Error: #{ex.message}" $stderr.puts "Usage: #{USAGE}" exit(1) end
しりとりゲームを処理しているメインの部分は後述する。
外枠のこの処理はそう複雑なものではない。Rubyの例外は最後まで
捕捉されないと、バックトレースを吐いてからプログラムを終了させる。
それはデバックに重宝するのだが、アプリケーションプログラムの終了
としてはちょっと見た目が悪い。そこで例外を捕捉して、エラーメッセージと
使用方法(USAGE)を言ってから、改めて死んでいる。
rescue文では、捕捉する例外クラスを指定しないと自動的にRuntimeError以下の
クラスを捕捉するようにしてくれる。今回省略しなかったのは単に趣味だ。実行時の
エラーだけを捕まえて、重大な例外は自前で異常終了させるぞ、という意思表示である。
ちなみに(名前で判るぞ、と言う方もおられるだろうが)$stderrは標準エラー出力を表す
IOオブシェクトだ。似たようなものに$stdinや$stdoutがある。グローバル変数である。
raise "辞書ファイルが指定されていません" if ARGV.empty? gm = GameMaster.new gm.dictionary_load ARGF
Rubyプログラムを起動するときに与えたコマンドライン引数は、
ARGVという定数に、Arrayオブジェクトとして収まっている。
それと名前がよく似たARGFという定数は、少々特殊な擬似的なIOオブジェクトだ。
(擬似的とは … p ARGF.class #=> Object)
ARGFはUnixのフィルタプログラムを作成する際に理想的な機能を備えている。
即ち、コマンドライン引数があればそれをファイル名としてオープンしていき、
引数がなければ標準入力を読む。ARGFの場合、いくら引数があっても、
ひとつの連続したファイルを開いているかのように扱える。
しかし、このコードではその便利な特性を一つ潰してしまっている。
Array#empty?でARGVが空かどうかを事前にチェックしているからだ。
ただ、このプログラムで、標準入力を使う必要性があまり感じられなかった
のでこういう措置を取った。sedかなにかで加工した辞書データを直接
流し入れたいという人は我慢して一旦ファイルにリダイレクトしてほしい。
pls = [ManualPlayer.new("あなた"), AutoPlayer.new("わたし")] pls.reverse! if Rands.bool_rand pls.each{|x| gm.entry x}
少し見づらいかもしれないが、plsに代入しているのは
リテラルで生成された配列だ。中身は見てのとおりである。
Array#reverse!は配列の順番を逆転させる。Array#reverseと違うのは、
前者は呼び出し元のオブジェクトを直接操作する(破壊的操作)が、
後者はその操作が施された新しいオブジェクトが生成されて返されるところである。
Rands.bool_randは半々の確率で真偽どちらかを返す。つまり
... if Rands.bool_rand
は、「50%の確率で...を実行する」と読める。
その後、順番にGameMasterにentryされているので、
これはプレイヤーとコンピュータの先攻後攻をランダムで決定しているのだ。
word = nil gm.each do |word| $stderr.puts "#{gm.now.id}の回答 ... #{word}" end
ここがしりとり勝負のループだ。wordにnilを代入しているのは、
しつこく言うが、Rubyのブロックローカルスコープ対策である。
if gm.now.id == "あなた" #now.id ... Looser log = gm.log_search(gm.now.before_input) $stderr.print "「その言葉は #{log[0]} 回目に #{log[1]} が使用しています。" $stderr.puts "わたしの勝ちです。" else $stderr.puts "「まいりました!あなたの勝ちです。" end $stderr.puts " 今回のしりとりでは #{gm.log_size} 個の単語を使用しました。」"
GameMaster#nowはループの外では直前の回答者を返す。故に、決着直後の
「直前の回答者」はつまり「敗者」のことだ。
要件によれば、プレイヤーの負けかコンピュータの負けかで通知することが違うので、
GameMaster#entryするときに設定したidで分岐している。
プレイヤーが負けの場合、敗因の回答であるbefore_inputを
log_searchにかけて、必要な情報を得ている。
以上、長々とひとつのプログラムを解説してきた。
正直このプログラムには反省点も多々ある。
例えばGameMaster#nowなどである。
が、そのへんの改良は皆さんへの課題としておく。
とか言って都合よく締めておきたい。
また、人にモノを教える文章など書く機会がないので、
「ここわかりづれーよ」と思ったらスレがどこかで吐き捨てて欲しい。
確実に改善できる保証はないが、可能な限り善処する。