スーパーマリオブラザーズにReplay機能をつける

authorNariさんのスーパーマリオブラザース(http://d.hatena.ne.jp/authorNari/20080422/1208880928)すごいですね。Rubyを知っている人はソースをぜひ読んでみてください。美しい!シンプル!スーパーマリオってこんなにシンプルに記述できるんだ!って思いました。
さて、authorNariさんといえばGCスーパーマリオブラザースはちょうどいいベンチマークになるんじゃないかなと思い、Replay機能をつけてみました。

使い方
1 main.rb中の$replayの代入文を
$replay = :record
にします

2 普通にプレーします。多分、反応が鈍くなってるんじゃないかと思います。

3 終わるときは必ずESCキーで終了してください。

4 main.rbのあるディレクトリにkey.logなるファイルが出来ています。これは、キー入力のログをMarshalで書き出したものです。

5 $replayの代入文を
$replay = :replay
にします

6 普通に起動するとあら不思議!

main.rbとgamestart.rbを書き換えています。diffを取ると面倒なので、ファイル全体を置きます。

main.rb

#
# super nario brother
# (c)2008, nari
# http://d.hatena.ne.jp/authorNari/
#

$: << File.join(File.dirname(__FILE__), 'mario')

require 'sdl'
require 'lib/fpstimer.rb'
require 'lib/input.rb'
require 'mario/scene'
require 'mario/material'


$test = false

# :record プレーを記録します
# :replay プレーを再生します
# その他 普通にゲームをします
$replay = :replay
require 'gamestart' unless $test
require 'test' if $test

gamestart.rb

#
# super nario brother
# (c)2008, nari
# http://d.hatena.ne.jp/authorNari/
#

require 'sdl'
require 'lib/fpstimer'
require 'lib/input'
require 'mario/scene'
require 'mario/material'
require 'mario/life'

KEYS = [:exit, :left, :right, :up, :down, :ok, :a, :b]

class FakeInput
  def initialize
    File.open("key.log", "r") do |fp|
      @event = Marshal.load(fp)
    end
    @time = 0
  end

  KEYS.each do |k|
    module_eval("def #{k};@event[@time]? @event[@time].include?(:#{k}):nil; end")
  end

  def poll
    @time += 1
  end

  def [](key)
    if @event.has_key?(@time) then
      @event[@time].include?(key)
    else
      nil
    end
  end
end

class Input
  define_key SDL::Key::ESCAPE, :exit
  define_key SDL::Key::LEFT, :left
  define_key SDL::Key::RIGHT, :right
  define_key SDL::Key::UP, :up
  define_key SDL::Key::DOWN, :down
  define_key SDL::Key::RETURN, :ok
  define_key SDL::Key::A, :a
  define_key SDL::Key::B, :b
end

def map_init(screen)
  map = Scene::Builder.new{
    mapping :title, Scene::Title.new {
      success :map_1
    }
    mapping :map_1, Scene::FlowWorld.new{
      success :title
      miss :title
    }
  }.scene_map

  map[:title].screen_build {
    background Material::BackGround.new_single_image(0, 0, SDL::Surface.load("mario/image/title.png"))
  }

  map[:map_1].screen_build {
    sky 12000
    ground 12000, 600

    block Material::ItemBox.new(800, 420)
    block Material::WeakBlock.new(1000, 420)
    block Material::ItemBox.new(1047, 420)
    block Material::WeakBlock.new(1094, 420)
    block Material::ItemBox.new(1141, 420)
    block Material::WeakBlock.new(1188, 420)
    block Material::ItemBox.new(1094, 240)
    block Material::Pipe.new(1370, 505)
    block Material::Pipe.new(1840, 460)
    block Material::Pipe.new(2280, 415)
    block Material::Pipe.new(2720, 415)
    block Material::WeakBlock.new(3730, 420)
    block Material::ItemBox.new(3777, 420)
    block Material::WeakBlock.new(3824, 420)
    block Material::WeakBlock.new(3871, 240)
    block Material::WeakBlock.new(3918, 240)
    block Material::WeakBlock.new(3965, 240)
    block Material::WeakBlock.new(4012, 240)
    block Material::WeakBlock.new(4059, 240)
    block Material::WeakBlock.new(4106, 240)
    block Material::WeakBlock.new(4153, 240)
    block Material::WeakBlock.new(4200, 240)
    block Material::WeakBlock.new(4400, 240)
    block Material::WeakBlock.new(4447, 240)
    block Material::WeakBlock.new(4494, 240)
    block Material::ItemBox.new(4541, 240)
    block Material::WeakBlock.new(4541, 420)
    block Material::WeakBlock.new(4900, 420)
    block Material::WeakBlock.new(4947, 420)
    block Material::ItemBox.new(5200, 420)
    block Material::ItemBox.new(5300, 420)
    block Material::ItemBox.new(5300, 240)
    block Material::ItemBox.new(5400, 420)
    block Material::WeakBlock.new(5600, 420)
    block Material::WeakBlock.new(5750, 240)
    block Material::WeakBlock.new(5797, 240)
    block Material::WeakBlock.new(5844, 240)
    block Material::WeakBlock.new(6050, 240)
    block Material::ItemBox.new(6097, 240)
    block Material::ItemBox.new(6144, 240)
    block Material::WeakBlock.new(6191, 240)
    block Material::WeakBlock.new(6097, 420)
    block Material::WeakBlock.new(6144, 420)
    block Material::Pipe.new(7900, 505)
    block Material::WeakBlock.new(8150, 420)
    block Material::WeakBlock.new(8197, 420)
    block Material::ItemBox.new(8244, 420)
    block Material::WeakBlock.new(8291, 420)
    block Material::Pipe.new(8700, 505)


    floor Material::Floor.new_fill_image(0, 600, 3270, 100, SDL::Surface.load("mario/image/floor_block.png"))
    floor Material::Floor.new_fill_image(3420, 600, 700, 100, SDL::Surface.load("mario/image/floor_block.png"))
    floor Material::Floor.new_fill_image(4300, 600, 3000, 100, SDL::Surface.load("mario/image/floor_block.png"))
    floor Material::Floor.new_fill_image(7450, 600, 3000, 100, SDL::Surface.load("mario/image/floor_block.png"))


    left_triangle_block Material::StrongBlock, 6350, 4
    right_triangle_block Material::StrongBlock, 6650, 4
    left_triangle_block Material::StrongBlock, 7082, 4, 5
    right_triangle_block Material::StrongBlock, 7450, 4
    left_triangle_block Material::StrongBlock, 8820, 8, 9

    goal 9860

    enemy Life::Kuribo.new(900, 550)
    enemy Life::Kuribo.new(2000, 550)
    enemy Life::Kuribo.new(2500, 550)
    enemy Life::Kuribo.new(2560, 550)
    enemy Life::Kuribo.new(3830, 200)
    enemy Life::Kuribo.new(3890, 200)
    enemy Life::Kuribo.new(4670, 550)
    enemy Life::Kuribo.new(4730, 550)
    enemy Life::NokoNoko.new(5100, 550)
    enemy Life::Kuribo.new(5430, 550)
    enemy Life::Kuribo.new(5490, 550)
    enemy Life::Kuribo.new(5750, 550)
    enemy Life::Kuribo.new(5810, 550)
    enemy Life::Kuribo.new(6100, 550)
    enemy Life::Kuribo.new(6160, 550)
    enemy Life::Kuribo.new(8300, 550)
    enemy Life::Kuribo.new(8360, 550)

    player Life::Mario.new(200, 550)
  }

  map
end

def main(buf)
  SDL.init(SDL::INIT_VIDEO|SDL::INIT_AUDIO|SDL::INIT_JOYSTICK)
  SDL::Mixer.open
  SDL::TTF.init

  if defined?(SDL::RELEASE_MODE)
    SDL::Mouse.hide
    screen = SDL.set_video_mode(Scene::SCREEN_WIDTH, Scene::SCREEN_HIGHT, 16, SDL::HWSURFACE|SDL::DOUBLEBUF|SDL::FULLSCREEN)
  else
    screen = SDL.set_video_mode(Scene::SCREEN_WIDTH, Scene::SCREEN_HIGHT, 16, SDL::HWSURFACE|SDL::DOUBLEBUF)
  end

  if $replay == :replay then
    input = FakeInput.new
  else
    input = Input.new
  end

  map = map_init(screen)
  scene = map[:title]
  timer = FPSTimerLight.new
  timer.reset
  tc = 0
  loop do
    input.poll
    tc += 1
    
    if $replay == :record then
      KEYS.each do |key|
        if input[key] then
          if buf[tc] == nil then
            buf[tc] = []
          end
          buf[tc].push key
        end
      end
    end
    break if input[:exit]

    s_next = scene.act(input)
    scene.render(screen)
    scene = map[s_next].rebuild if s_next
    timer.wait_frame{
      if defined?(SDL::RELEASE_MODE)
        screen.flip
      else
        screen.update_rect(0, 0, Scene::SCREEN_WIDTH, Scene::SCREEN_HIGHT)
      end
    }
  end
end

buf = {}
main(buf)

if $replay == :record then
  File.open("key.log", "w") do |fp|
    Marshal.dump(buf, fp)
  end
end