0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

数式で輪を作り、Rubyを流し込む — OUROBOROSのQuineを紐解く②

0
Posted at

自分のコードを輪の形に並べ、実行するたびに次の形を生み出す。周期Quine「OUROBOROS」は、24世代で最初のソースへ戻る作品です。

公開作品はこちらです。

前回は、eval と %w を使って、文字の配置と実行するコードを切り離す仕組みを解説しました。今回は、その文字をどこへ置くのかを見ていきます。

対象は、Rubyソースそのものの輪です。公開作品に表示される光の糸はGLSLで描いており、その描画については次回扱います。

絵の形と、そこへ置く文字を分ける

アスキーアート型のQuineを作るときには、二つの情報を分けて考えると分かりやすくなります。

  • どの場所に文字を置くかという「形」
  • その場所へ順番に置いていく「コード本体」

たとえば、次のような形を用意します。

  #####
 ##   ##
##     ##
 ##   ##
  #####

# の場所へコードを一文字ずつ置き、空白はそのまま残します。形の情報は、文字を置く場所を 1、空白を 0 とした配列でも表現できます。

この方法を詳しく紹介しているのが、「RubyでうどんげQuine(とAA型Quineの作り方講座)」です。この記事では、AAの形を数値化してコード内に持ち、実行時に復元しています。

OUROBOROSも、形と文字を分けて扱います。ただし、形を作る方法には数式を使いました。各位置について「ここは輪の内側か」を計算し、その答えによって文字を置くか空白にするかを決めています。

200列×80行の紙面を用意する

実装では、輪を配置するために200列×80行の領域を使っています。

rows = Array.new(80) do |y|
  Array.new(200) do |x|
    # この位置に置く文字を決める
  end.join.rstrip
end

y は行番号、x は列番号です。左上から右へ進み、一行終わったら次の行へ移ります。

.join で一行の文字列を作り、.rstrip で行末に余った空白を除きます。計算に使う領域は200列ですが、出力する各行の長さは輪の形によって変わります。

この領域の各位置を調べるため、まず座標を変換します。

xx = (x - 99.5) / 98
yy = (y - 39.5) / 38

99.5 と 39.5 は、それぞれ領域の中央です。これを引くと、中央を原点とする座標になります。

さらに横を 98、縦を 38 で割り、領域の端付近が、おおむね -1 から 1 の値になるようにします。

この変換により、列番号や行番号を直接使うよりも、輪の大きさを扱いやすくなります。文字セルの縦横比はフォントによって異なるため、実際に表示される輪の見え方は表示環境にも依存します。

中心からの距離で輪を作る

変換した座標から、中心までの距離を求めます。

rr = Math.sqrt(xx * xx + yy * yy)

これは三平方の定理による距離です。中心では 0、中心から離れるほど大きくなります。

たとえば、次の条件で文字を置くと輪になります。

on = rr < 0.97 && rr > 0.40

外側の半径より内側にあり、内側の半径より外側にある場所だけを選びます。

中心からの距離 配置
0.40 以下 中央の穴なので空白
0.40 より大きく、0.97 より小さい 輪なので文字を置く
0.97 以上 輪の外なので空白

ここまでなら、毎回同じ輪ができます。次に、輪の境界を少し変形させます。

角度によって輪の太さを変える

各位置が中心から見てどちらの方向にあるかを求めます。

ang = Math.atan2(yy, xx)

ang は中心からその位置へ向かう角度です。

OUROBOROSでは、この角度を使って内側と外側の境界を変えています。

outer = 0.97 + 0.025 * Math.cos(3 * ang + a)
inner = 0.40 + 0.035 * Math.sin(2 * ang + a)

on = rr < outer && rr > inner

外側は 0.97 を基準に 0.025、内側は 0.40 を基準に 0.035 の幅で揺れます。

3 * ang は輪を一周する間に三周期、2 * ang は二周期の変化を作ります。内外の境界が異なる周期で変化するため、場所によって輪の厚みが変わります。

輪を大きく崩さず、小さなうねりを付けるための式です。

世代番号を形の変化につなぐ

式に登場した a は、世代番号 k から作ります。

a = k * Math::PI / 12

一世代進むごとに、位相を π / 12、つまり15度分進めます。24世代分では 2π になり、三角関数の一周期に相当します。

ただし、この式は輪全体を一世代ごとに15度回転させるものではありません。外側には 3 * ang、内側には 2 * ang があるため、境界のうねりがそれぞれ異なる動きをします。

実際の世代番号は、次の式で 0 から 23 を循環します。

(n + 1) % 24

23の次では、番号そのものが0に戻ります。最初と同じ入力で形を計算し直すため、浮動小数点の計算で「一周した角度が厳密に元へ戻るか」に頼る必要もありません。

輪の中へコードを流し込む

形が決まったら、コード本体を一文字ずつ配置します。

実装では、配置する文字を次の式で用意しています。

v = ($s.split(35.chr)[0] + 35.chr).chars

35.chr は # です。

今回の本体では、最初の # が実行する処理の終わりに置かれています。その手前を取り出し、# を一つ付け直して、文字の配列にします。

この切り出しは、今回の本体の構造を前提とした処理です。文字列の中などに # を含む任意のRubyコードへ、そのまま使える方法ではありません。

各位置へ置く文字は、次の式で決めます。

on ? (v.shift || 35.chr) : 32.chr

32.chr は空白です。

  • 輪の内側なら、v.shift で次の一文字を取り出す。
  • 本体を使い切ったら、# を置く。
  • 輪の外側なら、空白を置く。

文字は紙面を左上から順番に走査して配置します。輪の円周に沿って並べる処理ではありませんが、文字を置く場所を輪の内側に限定することで、ソース全体が輪に見えるようになります。

余った場所と、足りない場所

輪の中には、コード本体の文字数より多くの配置場所があります。余った場所を埋めるのが # です。

前回紹介した外枠は、次の形でした。

eval$s=%w!
  …輪の形に並べたコード…
!*""

%w と連結によって配置用の空白や改行を取り除くと、実行する本体の末尾に # があり、その後ろに埋め草の # が続く文字列になります。eval では、末尾の # 以降がコメントとして扱われます。

次の世代を作る際には、先ほどの split で埋め草を取り除きます。形によって埋め草の数が変わっても、再配置する本体は同じです。

逆に、輪が小さすぎてコードを置き切れなかった場合は、エラーにします。

raise "capacity" unless v.empty?

配置が終わった時点で v に文字が残っていれば、本体の一部が出力されていません。途中で切れたRubyを次の世代へ渡す前に、ここで止めます。

外枠を付ければ、次の世代になる

最後に、輪の形に並べた本体へ世代番号と実行用の外枠を付けます。

実装中の文字コードによる組み立てを、読みやすく書き直すと次の形です。

"n=0x%02x;eval$s=%%w!\n" % k +
  rows.join("\n") +
  "\n!*\"\"\n"

%02x は世代番号を二桁の16進数にします。%% は書式文字列の中で、文字としての % を出力するための記法です。

この式が返すのは、次のようなRubyソースです。

n=0x01;eval$s=%w!
  …第1世代の輪に並べたコード…
!*""

配置する関数自身もコード本体に含まれています。そのため、出力されたRubyも同じ手順で次の世代を生成できます。

24枚の完成済みソースを切り替える必要はありません。各世代が持つ本体と世代番号から、その都度、次のソースを作ります。

コードと形が一緒に循環する

今回の輪を作る処理は、次の順序で動きます。

  1. 世代番号から、境界を変形させる位相を求める。
  2. 各位置の距離と角度から、輪の内側かどうかを判定する。
  3. 輪の内側へコード本体を順番に配置する。
  4. 余った場所を # で埋め、世代番号と外枠を付ける。

同じ本体でも、世代番号によって配置が変わります。そして番号が0へ戻ると、同じ形と同じソースが再生成されます。

Quineの自己再生成と、数式による造形をつなぐ役割を担っているのが、この配置処理です。

次回:描いたコードを読み取って実行する

ここまでで、Rubyが自分のコードを輪の形に並べ直し、次の世代を生み出す仕組みを見てきました。

公開作品では、さらにそのRubyをGLSLへ埋め込み、GPUでコードシートを描きます。そして、描画したピクセルからRubyを復元して実行します。

次回は、コードの各バイトをシェーダーへ渡す方法と、空白や改行までピクセルから読み取れる仕組みを解説します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?