アニメ「Sonny Boy」の『難解』プログラムの解説

『Sonny Boy』というアニメが放送されています。学校が異次元に漂流してしまい、超能力に目覚めた生徒たちがサバイバルしながら、さまざまな奇妙な現象の裏にあるルールを解き明かし、元の世界に変える方法を探す、というストーリーです。ルールが分かったあとで何度も見直したくなります。

anime.shochiku.co.jp

さて今回、『Sonny Boy』に、プログラムを寄稿しました。プログラムでおもしろいCGを作ったとかではなく、プログラムの実行の様子そのものが『Sonny Boy』の5話の中で放送されました。

こういうプログラムです。

f:id:ku-ma-me:20210812015757p:plain
nankai.rb

このプログラムがどういうものだったかを解説します。

どんなプログラム?

実行すると、「難解」という文字がほどけてなくなるアニメーションをします。

起動したらまず、プログラム自身が画面に表示されます。 しばらくしたら「難解」が左から右へほどけていきます。 その後上からヒモが降ってきて、"Solved" という単語になったあと、再度ほどけて終了。 実行時間5秒程度のちょっとしたデモアプリです。

手元で試してみたい人は、Rubyをインストールした上で、nankai.rbを保存しruby nankai.rbと起動します。 端末のサイズを 180 x 40 以上に広げて実行してください。

なお、プログラムはGitHubに置いてあります。

github.com

制作の背景

拙作のASCII Fluid(下記動画)が、Sonny Boyの監督の夏目真悟さんの目に止まったのがきっかけです。

www.youtube.com

「解ける(ほどける、とける)」というテーマで、数秒程度の難解なプログラムを自由に作ってほしい、と声をかけていただいたので、そのまま『難解』をほどくプログラムにしました。 作中に出てくる、『解く』という能力を示すデモになってます。 アニメで使ってもらうということで、わりと動きにこだわって作ってみたつもりです。

ちょっとだけ技術解説

アニメーションはハードコードしているのではなく、実際に「難解」の文字を分解することで表現しています。 「難解」を17領域くらいに分割し、それぞれの中でスパニングツリーを構成します。 その端っこを引っ張るような感じでアニメーションします。なので正確には一本のヒモではなく、何本かのヒモが絡まったような形になってます。

プログラム自身を表示するのは単にソースコードをファイル読み込みするのではなく、クワインという方法で実現しています。 端末でのアニメーションはエスケープシーケンスを使ってやってます。 エスケープシーケンスは無害化して出力されているので、エスケープシーケンスを含むコードを含めて再実行できるようになっています。

$ ruby nankai.rb > nankai.output

$ ruby nankai.output

Rubyというプログラミング言語はとても実用的な言語ですが、こんな変わった使い方もできて、とても良い言語です。もしこういうプログラムに興味があったら、こんな感じのプログラムばっかり集めた本を書いているのでぜひ見てみてください(宣伝)。

まとめ

ということで、「難解」という文字が自発的に解けていくというプログラムでした。アニメ見てた方に「おっ」と思っていただけていたら嬉しいです。見てなかった方は、いろいろ配信があるので、5話だけと言わず、ぜひ1話から追っかけてください。

...

以下は、読まなくていいです。

おまけ

ここからはギークプログラマ向けの、ちょっとしたおまけです。

このプログラムは「ほどいても」動作するようになってます。 どういうことかというと、左半分の「難」だけを切り出しても実行できるプログラムになっています。

     q=%q!s     =q[/#{          N=?\n}(    #{Z=?\~
     s}*)/,     1];E=3          3.chr;     $><<t=~
(N*12+s+"q=%q#{E+q+E};eval#    {Z*8}n=    '#{n}'~
"+N*13+?#).gsub(/^.*/){(Z*(   35-s.si    ze)+$&)
     |c|d=d     *90+(c       -2)%91};d};b=116;c=0;i=D
     `?oT`r     #?jn%:      _(7AF5]7lZ}|N,),slN15Ap8<
  ]];g=[];F=->x,m{g<<[t[x  ],b>0?[0]*c+[x]*b+m:[0]*(c
  map{|i|i+=x;s<=i&&i<s+n -170&&(i-s    )%W<n%W
  <<x;v   <1&&v=D   ["axC4?ap'5.bbU     lui?h|b
  ".split(??)[(x%W+x/W)%8]];(d+=1;v/=2)while+v%2>0;z=
  x+=d%8>3?z:-z;v/=2)whil   e-x%W>1&&0<x&&x<8208;F[y,
          8-'#'L>            *B(B[(*-$XG|"E"H=$H:)$'$
  ,n=a;v=B.pop-34;v%3<2?(    z=n%W;     S[s+y=(
  ;y)]):(b+=200/r+=2;M[D[    "=,)_q     >Z"]>>r
  170]*19;b=0;T=(c+(0..8)    .map{Z*51+(0..67).map{i/
          %w{puts            (["Usage:",:ruby,$0,100]
/q/)?$q[/^#{z}+/]+z+z+"R=ev  al#{z*18}$q=#{z*35}%q{#$
1]:[]};n+z+?=+z+(m>1?f[2]:[  m])*(z     +?x+z))
        ,n=0,m;6.ti          mes{|i     |c+=10;
      [i]*684];b=52;t=       T;M[6,(x-i%2)%W+4788]};s=
   {|i|o=[Z*   170]*48*N;    g.map{|c,m|x=m[i]||0;x>0&
ub(N,"\e[E        ");slee    p(0.01)};puts(s)#Unravel#
  "q!;p              0/      0.0;#";

これ実行すると、"NaN"が出力されます。ナンだけに。

$ ruby nan.rb
NaN

もちろん、右半分の「解」も実行可能です。

       p="><"
      puts""+              p||$><<"____\n\s/\s/\n";(
      "\u89D2";;x=(%~      );%#?_Y}_U1jojD(i0(DVA1aq
     .ljust(170)};D=->          e{d=0;e.      bytes{
    ["cGIY&    ?dUptZ            yGj5?=       1+wD5G
  XzIUF<+     <L-MlD            )"+t[2        513,21
 -m.size)+m.reverse];t[x]=    Z;[1,-1        ,-W,W].
&&t[i]>Z&&F[i,[i]+m]}};M=- >d,x{y=x;    m=[];v=0;(m
  *?_jJc?   _v_&z   'NcJ{(Hn{?_O6m       ?}3k7>?Q,
   -(d%4)   %172+   1;~+%~  ##Y.      #E2021
   m]};W=171;B=%{36<-~;%w~    ;puts   :Moo;'
   OAs"3$*2$-2"*5s}.bytes;   r=8;S=   ->*a{s
   v%3*17   0+1)*   w=v/3,  n-(S[s,v%3<1?n+w-z:w*W+z]
   &6,s+B   .pop-   34+v/3 *W])};S[2082,m=8378];c=[Z*
   =2;i%2>0?%q@_R=eval$q=%q{z=?\      s;eval
   *z)if[]==$*.map{|n|puts  (n.       match(
   q}":(m=n.to_i;f=->x{m>1 ?m%x<1?(m/=x;[x]+f[x]):f[x+
   )}}*""           }@[b+= 1]:Z}*""+Z*51}+c)*N;c=340;s
   t=T+""           ;b=0;M [2,x=D["^lv(2A"[i]]+3302+26
  "\e[F"*           47;504            .times
 &o[x]=c            };$><<            s+o.gs
#!;eval        n='eval q.             gsub((
  /[\s          ]|~.*$/)              ,"")#'

実行すると"><"が出力されます。ギリシャ文字カイのイメージ。

$ ruby kai.rb
><

プログラムを横に分解してそれぞれ違う動きをさせるのはわりと難解だと思うので、腕に覚えのあるプログラマは解読してみてください。

ヒント:Ruby「%記法」をうまく使って実現してます。 最初の2行と終わりの2行あたりに注目すると良いです。

おまけのおまけ

これで終わり? まだ分解できますよね。そう、「角」と「刀」と「牛」。

       p="><"
      puts""+
      "\u89D2";;x=(%~
     .ljust(170)};D=->
    ["cGIY&    ?dUptZ
  XzIUF<+     <L-MlD
 -m.size)+m.reverse];t[x]=
&&t[i]>Z&&F[i,[i]+m]}};M=-
  *?_jJc?   _v_&z   'NcJ{(
   -(d%4)   %172+   1;~+%~
   m]};W=171;B=%{36<-~;%w~
   OAs"3$*2$-2"*5s}.bytes;
   v%3*17   0+1)*   w=v/3,
   &6,s+B   .pop-   34+v/3
   =2;i%2>0?%q@_R=eval$q=%
   *z)if[]==$*.map{|n|puts
   q}":(m=n.to_i;f=->x{m>1
   )}}*""           }@[b+=
   t=T+""           ;b=0;M
  "\e[F"*           47;504
 &o[x]=c            };$><<
#!;eval        n='eval q.
  /[\s          ]|~.*$/)

「角」を実行すると角が出ます。

$ ruby tsuno.rb
角

次は「刀」。

 p||$><<"____\n\s/\s/\n";(
 );%#?_Y}_U1jojD(i0(DVA1aq
      e{d=0;e.      bytes{
       yGj5?=       1+wD5G
      )"+t[2        513,21
    Z;[1,-1        ,-W,W].
 >d,x{y=x;    m=[];v=0;(m
Hn{?_O6m       ?}3k7>?Q,
  ##Y.

「刀」を実行すると「刀」のアスキーアートっぽいものが出ます。

$ ruby katana.rb
____
 / /

最後は「牛」。

            #E2021
    ;puts   :Moo;'
   r=8;S=   ->*a{s
  n-(S[s,v%3<1?n+w-z:w*W+z]
 *W])};S[2082,m=8378];c=[Z*
q{z=?\      s;eval
  (n.       match(
 ?m%x<1?(m/=x;[x]+f[x]):f[x+
 1]:Z}*""+Z*51}+c)*N;c=340;s
 [2,x=D["^lv(2A"[i]]+3302+26
            .times
            s+o.gs
            gsub((
            ,"")#'

「牛」を実行すると鳴きます。

$ ruby ushi.rb
Moo

それぞれのプログラムは単純なので、Rubyがわかれば普通に読めると思います。 実行に関係ない文字列がいかにSyntax Errorにならないようにするかがポイントです。

しかし、「解」や「難解」に合体させても動くようにするのは、かなり苦労しました。 その甲斐あって、なかなか香ばしいコードに仕上がったと思うので、ぜひ解読して欲しいです。

ヒント:p~.*$/が分解次第で構文の解釈が変わります。ダブルミーニングなコード。

さらなるおまけ

最後に一瞬表示される "Solved" も実行可能になってます。

   R=eval                  $q=                                   %q{
 z=?\s;eval                %w{                                   put
s(["     Usa               ge:                                   ",:
 ruby                      ,$0                                   ,10
   0]*z)if      []==$*.    map  {|n     |pu    ts(n.ma      tch(/q/)
       ?$q[    /^#   {z}   +/]  +z+     z+"   R=e   val    #{z   *18
}$q     =#{z  *35}   %q{#  $q}   ":(   m=n   .to_i;f=->x  {m>1   ?m%
 x<1?(m/=x;    [x]   +f[   x])    :f[ x+1     ]:[          ]};   n+z
   +?=+z+       (m>1?f[    2]:     [m])*       (z+?x+z)     ))}}*""}

実行すると使い方が表示されます。

$ ruby solved.rb
Usage: ruby solved.rb 100

この通りに実行してみます。

$ ruby solved.rb 100
100 = 2 x 2 x 5 x 5

100が素因数「分解」されました。コマンドライン引数の数字を次々に素因数分解していくプログラムになってます。

$ ruby solved.rb 1 2 3 4 5 6 7 8 9 10
1 = 1
2 = 2
3 = 3
4 = 2 x 2
5 = 5
6 = 2 x 3
7 = 7
8 = 2 x 2 x 2
9 = 3 x 3
10 = 2 x 5

最後のおまけ

もはや全然関係ないおまけですが、solved.rb は引数に "quine" を渡すと、自分自身を表示します。ここにもクワイン

$ ruby solved.rb quine
   R=eval                  $q=                                   %q{
 eval(%w{z=                ?\s                                   ;pu
ts([     "Us               age                                   :",
 :rub                      y,$                                   0,6
   ]*z)if[      ]==$*.m    ap{  |n|     put    s(n.mat      ch(/q/)?
       $q[/    ^#{   z}+   /]+  z+z     +"R   =ev   al#    {z*   18}
$q=     #{z*  35}%   q{#$  q}"   :(m   =n.   to_i;f=->x{  m>1?   m%x
 <1?(m/=x;[    x]+   f[x   ]):    f[x +1]     :[]          };n   +z+
   ?=+z+(       m>1?f[2    ]:[     m])*(       z+?x+z))     )}}*"")}

ささやかなこだわりですが、左側に空白をいれたら出力も同じようにずれるようになってます。 アニメ中のように、画面真ん中あたりに表示されたのをコピペしてもちゃんとクワインになります。

$ cat solved2.rb
            R=eval                  $q=                                   %q{
          eval(%w{z=                ?\s                                   ;pu
         ts([     "Us               age                                   :",
          :rub                      y,$                                   0,6
            ]*z)if[      ]==$*.m    ap{  |n|     put    s(n.mat      ch(/q/)?
                $q[/    ^#{   z}+   /]+  z+z     +"R   =ev   al#    {z*   18}
         $q=     #{z*  35}%   q{#$  q}"   :(m   =n.   to_i;f=->x{  m>1?   m%x
          <1?(m/=x;[    x]+   f[x   ]):    f[x +1]     :[]          };n   +z+
            ?=+z+(       m>1?f[2    ]:[     m])*(       z+?x+z))     )}}*"")}

$ ruby solved2.rb quine
            R=eval                  $q=                                   %q{
          eval(%w{z=                ?\s                                   ;pu
         ts([     "Us               age                                   :",
          :rub                      y,$                                   0,6
            ]*z)if[      ]==$*.m    ap{  |n|     put    s(n.mat      ch(/q/)?
                $q[/    ^#{   z}+   /]+  z+z     +"R   =ev   al#    {z*   18}
         $q=     #{z*  35}%   q{#$  q}"   :(m   =n.   to_i;f=->x{  m>1?   m%x
          <1?(m/=x;[    x]+   f[x   ]):    f[x +1]     :[]          };n   +z+
            ?=+z+(       m>1?f[2    ]:[     m])*(       z+?x+z))     )}}*"")}