RubyKaigi 2019 Cookpad Daily Ruby Puzzles を全パターン試して解く

RubyKaigi 2019のクックパッドブースで"Cookpad Daily Ruby Puzzles"というのをやっていたらしくて、イベント終了後に[正解と解説](https://techlife.cookpad.com/entry/2019/04/25/122520)というエントリーも出ていました。

[RubyKaigi 2019 Cookpad Daily Ruby Puzzles の正解と解説](https://techlife.cookpad.com/entry/2019/04/25/122520)

この問題、結果的には全て問題のコードに1文字を追加するだけで解けるらしいので、全パターン試して解くという方法を試してみました。こんなコードです。

```ruby:solv.rb
CODES = {}

CODES["Example"] = <<'EOS'
def foo
  "Hello world" if
       false
end
puts foo
EOS

CODES["Problem 1-1"] = <<'EOS'
# Hint: Use Ruby 2.6
puts "#{"Goodbye" .. "Hello"} world"
EOS

CODES["Problem 1-2"] = <<'EOS'
puts&.then {
  # Hint: &. is a safe
  # navigation operator
  "Hello world"
}
EOS

CODES["Problem 1-3"] = <<'EOS'
include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)
EOS

CODES["Problem 2-1"] = <<'EOS'
def say
  -> {
    "Hello world"
  }
  # Hint: you should call the Proc.
  yield
end

puts say { "Goodbye world" }
EOS

CODES["Problem 2-2"] = <<'EOS'
e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next
EOS

CODES["Problem 2-3"] = <<'EOS'
$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")
EOS

CODES["Problem 3-1"] = <<'EOS'
def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p
EOS

CODES["Problem 3-2"] = <<'EOS'
def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello
EOS

CODES["Problem 3-3"] = <<'EOS'
def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say
EOS

CODES["Extra 1"] = <<'EOS'
Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello() +
    " world"
end

puts Hello()
EOS

CODES["Extra 2"] = <<'EOS'
s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"
EOS

CODES["Extra 3"] = <<'EOS'
def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  tp.binding.local_variable_set(:t, 'Ruby')
  tp.disable
}.enable(target: method(:say))

say
EOS


require 'timeout'

CHARS =
  ('0'..'9').to_a +
  ('a'..'z').to_a +
  ('A'..'Z').to_a +
  %w( ! ? # % & | + - * / ^ ' . , < > = ~ $ @ _ " : ` \\ ; ) +
  [' ', "\n", "ǲ"]

CODES.each do |name, code|
  puts
  puts "#{name}"
  puts Time.now
  (0...code.length).each do |i|
    putc '.'
    CHARS.each do |char|
      code_inserted = code.clone.insert(i, char)
      File.open("code.rb", "w") { |f| f.puts(code_inserted) }
      begin
        Timeout.timeout(1) do
          result = `ruby code.rb 2>/dev/null`
          if result == "Hello world\n"
            puts
            puts '-----'
            puts code_inserted
            puts '-----'
          end
        end
      rescue Timeout::Error
      end
    end
  end
  puts
  puts Time.now
end
```

このコードと、実行結果は[gist](https://gist.github.com/Akiyah/dc344d02af97d63a4e37394103e57bd2
)に置いておきました。

最初はevalでコードを実行して試していたのですが、evalで実行した結果、たとえば`!`を再定義してしまって、次のループでの実行結果に影響を及ぼすことがあったりして、別ファイルにしてバッククオートでrubyコマンドで実行することにしました。

[正解と解説](https://techlife.cookpad.com/entry/2019/04/25/122520)に書いてありますが、"Problem 1-3"には「ブルートフォースよけ」が仕込まれています。まんまとはまってしまいました。`Timeout.timeout(1) do ... end`で囲って、一秒以上かかる場合は諦めるようにしました。

CHARSには数字、アルファベット、記号、空白文字、改行文字、そして"ǲ"を入れています。"ǲ"は"Extra 2"のためだけに入れました。。。（"Extra 2"はヒントがなければ全パターン試したつもりでも解けませんね。。。）

全パターン試して解こうと思ったモチベーションの１つは、別解を見つけることだったのですが、"Example"以外は別解が見つかりませんでした。"Example"の別解は、`if !false`の代わりに`if :false`を使うというものです。ただ、"Example"は解答が発表されているわけではないので、厳密には別解というわけではないですね。別解が見つからないということは、問題作成者の方も私がやったような全パターンを試すスクリプトを書いて事前に確認しているのだと思います。

~~"Extra 3"は1文字解答が２つあるらしいのですが、まだ解答が１つしか見つかっていません。全パターン試しているはずなのに、なんででしょうかね。。。~~

(追記:2019/05/03 08:07)
コードにミスがありました。バックスラッシュ(`\`)とセミコロン(`;`)を別々の文字列にしたかったのに、`\ ;`と書いていて、空白とセミコロンの2文字の文字列ということになっていました。`\\ ;`に書き換えました。それにより、"Extra 3"の２つ目の1文字解答も見つけることができました。