RubyKaigi 2019のクックパッドブースで"Cookpad Daily Ruby Puzzles"というのをやっていたらしくて、イベント終了後に正解と解説というエントリーも出ていました。
RubyKaigi 2019 Cookpad Daily Ruby Puzzles の正解と解説
この問題、結果的には全て問題のコードに1文字を追加するだけで解けるらしいので、全パターン試して解くという方法を試してみました。こんなコードです。
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", "Dz"]
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に置いておきました。
最初はevalでコードを実行して試していたのですが、evalで実行した結果、たとえば!
を再定義してしまって、次のループでの実行結果に影響を及ぼすことがあったりして、別ファイルにしてバッククオートでrubyコマンドで実行することにしました。
正解と解説に書いてありますが、"Problem 1-3"には「ブルートフォースよけ」が仕込まれています。まんまとはまってしまいました。Timeout.timeout(1) do ... end
で囲って、一秒以上かかる場合は諦めるようにしました。
CHARSには数字、アルファベット、記号、空白文字、改行文字、そして"Dz"を入れています。"Dz"は"Extra 2"のためだけに入れました。。。("Extra 2"はヒントがなければ全パターン試したつもりでも解けませんね。。。)
全パターン試して解こうと思ったモチベーションの1つは、別解を見つけることだったのですが、"Example"以外は別解が見つかりませんでした。"Example"の別解は、if !false
の代わりにif :false
を使うというものです。ただ、"Example"は解答が発表されているわけではないので、厳密には別解というわけではないですね。別解が見つからないということは、問題作成者の方も私がやったような全パターンを試すスクリプトを書いて事前に確認しているのだと思います。
"Extra 3"は1文字解答が2つあるらしいのですが、まだ解答が1つしか見つかっていません。全パターン試しているはずなのに、なんででしょうかね。。。
(追記:2019/05/03 08:07)
コードにミスがありました。バックスラッシュ(\
)とセミコロン(;
)を別々の文字列にしたかったのに、\ ;
と書いていて、空白とセミコロンの2文字の文字列ということになっていました。\\ ;
に書き換えました。それにより、"Extra 3"の2つ目の1文字解答も見つけることができました。