スーパーマリオブラザーズの「次の面」を求める 〜Rangeとsuccメソッドの甘い(?)関係〜
Range#eachはsuccメソッドを内部で呼ぶ、だからRangeにStringを指定することもできる。
しかーし、String#succが常に「正しい」次の文字列を返すとは限らない!そこで、おれおれsuccを定義してやろうじゃないかというお話。
平成生まれの人は知らないかもしれないが、昔懐かしのスーパーマリオブラザーズ1、2の面構成は8ワールドあって、それぞれのワールドには4エリアがある。たとえば2ワールド4エリアは「2-4」と書く。で、次の面は「3-1」だ。ちなみに、4エリアにはクッパが待ち構えている。
「2-4」の次を「3-1」を返すようなおれおれsuccを定義する。ここはRubyらしくModule#extendで特異メソッド(厳密には違うが事実上そういうことで)を動的に定義する。
最後の数字が4のときはワールドの数字「self[0,1]」を次の数字にする。そうでないときはオリジナルのString#succを呼ぶ。ここはsuperを使おう。succなんて書いてしまったら無限ループになってしまうぜ。
で、返り値の文字列にもおれおれsuccを使ってほしいのでextendしておく。
ちなみに「self[0]」だとRuby 1.8では文字コードが返ってきてしまうので動かない。Ruby 1.9なら動く。
最後に、「"1-1".extend(MarioLevelSucc)」なんて書くのはだりぃから「level("1-1")」と書けるように関数を定義しとく。
module MarioLevelSucc def succ (self[-1] == ?4 ? "#{self[0,1].succ}-1" : super).extend MarioLevelSucc end end def level(str) str.extend MarioLevelSucc end level("1-4").succ # => "2-1" (level("1-1") .. level("3-4")).to_a # => ["1-1", # "1-2", # "1-3", # "1-4", # "2-1", # "2-2", # "2-3", # "2-4", # "3-1", # "3-2", # "3-3", # "3-4"]
MarioLevelSuccはクラスにしてもいいが、<=>も定義する必要があってめんどいw
class MarioLevel def initialize(s) @s = s end attr :s def succ MarioLevel.new(@s[-1] == ?4 ? "#{@s[0,1].succ}-1" : @s.succ) end def <=>(o) @s <=> o.s end end def level(str) MarioLevel.new str end level("1-4").succ # => #<MarioLevel:0xb7e0cd44 @s="2-1"> (level("1-1") .. level("3-4")).to_a # => [#<MarioLevel:0xb7de6860 @s="1-1">, # #<MarioLevel:0xb7de6734 @s="1-2">, # #<MarioLevel:0xb7de670c @s="1-3">, # #<MarioLevel:0xb7de66e4 @s="1-4">, # #<MarioLevel:0xb7de6694 @s="2-1">, # #<MarioLevel:0xb7de666c @s="2-2">, # #<MarioLevel:0xb7de6644 @s="2-3">, # #<MarioLevel:0xb7de661c @s="2-4">, # #<MarioLevel:0xb7de65cc @s="3-1">, # #<MarioLevel:0xb7de65a4 @s="3-2">, # #<MarioLevel:0xb7de657c @s="3-3">, # #<MarioLevel:0xb7de6554 @s="3-4">]
[2008/06/16]追記:裏面も入れてみる
マリオ2でワープ使わずに8-4をクリアしたら9-1がプレイできるんだっけ〜?
で、8回クリアしたらA-1〜D-4があったけ。
MarioLevelSucc#succをちょい書き換え。ネストした条件式はたまに出てくる。
module MarioLevelSucc def succ (self == "9-4" ? "A-1" : self[-1] == ?4 ? "#{self[0,1].succ}-1" : super).extend MarioLevelSucc end end def level(str) str.extend MarioLevelSucc end (level("8-1") .. level("D-4")).to_a # => ["8-1", # "8-2", # "8-3", # "8-4", # "9-1", # "9-2", # "9-3", # "9-4", # "A-1", # "A-2", # "A-3", # "A-4", # "B-1", # "B-2", # "B-3", # "B-4", # "C-1", # "C-2", # "C-3", # "C-4", # "D-1", # "D-2", # "D-3", # "D-4"]