スーパーマリオブラザーズの「次の面」を求める 〜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"]