LoginSignup
8
4

年賀状でよくみる Quine を自分自身でも作ってみたかったので、作ってみました。

f=()=>{eval(`p='ThisIsAQuine!';o='';t='f='+f+';f();';r='\\u0020';s='\`';t=t.replace(/[\\n]|[
\\s]/g,'').split('\`');u=t[4];t=t[0]+s+t[1]+s+t[2]+s+t[3];c=0;a=[[0,92],[0,92],[0,92],[0,92]
,[0,92],[0,92],[0,8,16,48,51,92],[0,6,18,48,51,92],[0,5,10,15,19,48,51,92],[0,4,8,16,20,92],
[0,3,7,18,21,92],[0,2,6,18,22,92],[0,2,5,19,22,92],[0,2,5,19,23,28,31,39,42,48,51,57,60,63,6
8,81,87,92],[0,1,5,20,23,28,31,39,42,48,51,57,60,61,70,79,89,92],[0,1,4,20,23,28,31,39,42,48
,51,57,63,67,70,78,82,85,89,92],[0,1,4,20,23,28,31,39,42,48,51,57,62,68,71,77,80,87,90,92],[
0,1,4,20        ,23,28,31,39,42,48,51,57,61,68,7   1,76,80,87,91,92],[0,1,4,20,23,28,31,39,4
2,48,5            1,57,60,68,71,76,79,88,91,92],   [0,1,4,20,23,28,31,39,42,48,51,57,60,68,7
1,76,     79,88    ,91,92],[0,1,4,20,23,28,31,39   ,42,48,51,57,60,68,71,76,91,92],[0,1,5,20
,23,    28,31,39    ,42,48,51,57,60,68,71,76,91,92],[0,2,5,12,14,20,23,28,31,39,42,48,51,57,
60,    68,71,76,79   ,92],[0,2,5,12,15,19,22,28,31,39,42,48,51,57,60,68,71,76,79,92],[0,2,6,
12    ,16,19,22,28    ,31,39,42,48,51,57,60,68,71,76,79,92],[0,3,7,13,21,28,31,38,42,48,51,5
7,   60,68,71,76,80   ,89,90,92],[0,4,8,15,21,28,31,37,42,48,51,57,60,68,71,77,80,88,91,92],
[0   ,5,10,15,20,28    ,32,3   6,42,48,   51,57,   60,68,   71,     78,82,86,90,9      2],[0
,    6,22,29,38,39,4   2,48,   51,57,60   ,68,71   ,79,89   ,         92],[0,8,          16,
1   9,23,31,36,39,42   ,48,5   1,57,60,   68,71,   81,87,      92],   [0,20,23    ,92    ],[
0   ,22,23,92],[0,92   ],[0,   92],[0,9   2],[0,   92]];f     or(i=0   ;i<a.l   ength;i   ++
)   {if(i==a.length-   1){fo   r(j=0;j<   a[i].l   ength-    1;j++){   for(k    =a[i][j    ]
;   k<a[i][j+1]-u.le   ngth-   3;k++){o   +=p[k%   p.leng   th];c++;   }}o+=   "*/"+s+u;   b
r   eak;}for(j=0;j<a   [i].l   ength-1;   j++){f   or(k=a   [i][j];k   <a[i]   [j+1];k++   )
{   if(c>=t.length){   if(j%   2){o+=r;   }else{   if(c==   t.length   ){o+=               '
/    ';c++;continue;   }if(c   ==t.leng   th+1){   o+='*'   ;c++;con   tinue               ;
}o   +=p[c%p  .lengt   h];c+   +}}else{   if(j%2   ){o+=r   ;}else{o   +=t[c   ];c++}}}}o+="
\\   n";}con   sole   .log(o   );/*sAQu   ine!Th   isIsAQ   uine!Thi   sIsAQ   uine!ThisIsAQ
ui    ne!Thi    sIs   AQuine   !ThisIsA   Quine!   ThisIs   AQuine!T   hisIs   AQuine!ThisIs
AQu    ine!Th        isIsAQu   ine!Thi    sIsAQu   ine!Th   isIsAQui   ne!Th    isIsAQuin e!
This    IsAQuin      e!ThisI   sAQuin     e!This   IsAQui   ne!ThisI   sAQuin   e!ThisIs   A
Quine     !This     IsAQuine    !Thi      sIsAQu   ine!Th   isIsAQui   ne!This    IsAQ    ui
ne!Thi                sIsAQui         n   e!This   IsAQui   ne!ThisI   sAQuine!          Thi
sIsAQuin        e!T    hisIsAQu     ine   !ThisI   sAQuin   e!ThisIs   AQuine!Thi      sIsAQ
uine!ThisIsAQuine!Th   isIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIs
AQuine!ThisIsAQuine!Th isIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIs
AQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsA
Quine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQ
uine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQu
ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!ThisIsAQuine!T*/`.replace(/([\n]|[\s])+/g,''));};f();

完成形は GitHub にもあげています。

Quine とは

Quine とは、自己言及プログラムと呼ばれるもので、プログラムを実行するとプログラム自体のソースコードを出力するものです。

つまり先ほどのコードを実行すると、出力として先ほどのコードが出力されるということです。

定義については Wikipedia に譲ろうと思います。

Quine の条件を満たしながら上記のコードのように、文字列を出力できるような処理を組み入れることでアスキーアートを作れます。

動機

年賀状で Quine を作っていた人を見かけて、自分でも作ってみたくなったので作りました。
しかし、肝心の元にした方を忘れてしまいました……。思い当たるワードで調べても出てこず、ちゃんと読み込んでおくんだったなあという一抹の後悔があります。確か白地にパステル調の年賀状を作っていらっしゃたはずです。

とりあえず、「Quine」という文字列の AA を出力する Quine を作るのが目標です。

作り方

用いる言語は悩みましたが、 参考にした記事では JavaScript を用いていたのでそれに準じました。

私はこちらの記事を参考に作成しました。

こちらの記事ではコードをトークン化し、シンタックスハイライトを効かせて、カラフルにしています。

私はこちらの記事の、トークン化前の検証で言及されている、テンプレート文字列を利用して Quine を作成しました。

テンプレート文字列を利用した Quine 作成

おおよそは参考にさせていただいた記事で言及されているので、私が個人的に詰まった部分に対して言及します。

基本形は以下の形です。これを発展させます。

f = function() {
    eval(`console.log('f = ' + f + ';\\nf();')`);
};
f();

AA を作るための情報を生成

まず、「Quine」という文字列を AA にして、文字を置く場所と置かない場所を判別できるようにしたいです。

こちらのサイトを利用して、 AA を作成しました。

AA として生成するための設定としては以下のものを行いました。

  • サイズは 25 pt 指定
  • 文字設定を変更
    • 背景文字を「_」、前景文字を「#」

Quine だとこの設定であれば、 92 x 36 文字のサイズで以下のような AA が生成されます。

これを元に、置く場所と置かない場所の情報を生成します。

____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
________########________________________________###_________________________________________
______############______________________________###_________________________________________
_____#####_____####_____________________________###_________________________________________
____####________####________________________________________________________________________
___####___________###_______________________________________________________________________
__####____________####______________________________________________________________________
__###______________###______________________________________________________________________
__###______________####_____###________###______###______###___#####_____________######_____
_####_______________###_____###________###______###______###_#########_________##########___
_###________________###_____###________###______###______######____###________####___####___
_###________________###_____###________###______###______#####______###______###_______###__
_###________________###_____###________###______###______####_______###_____####_______####_
_###________________###_____###________###______###______###________###_____###_________###_
_###________________###_____###________###______###______###________###_____###_________###_
_###________________###_____###________###______###______###________###_____###############_
_####_______________###_____###________###______###______###________###_____###############_
__###_______##______###_____###________###______###______###________###_____###_____________
__###_______###____###______###________###______###______###________###_____###_____________
__####______####___###______###________###______###______###________###_____###_____________
___####______########_______###_______####______###______###________###_____####_________#__
____####_______######_______###______#####______###______###________###______###________###_
_____#####_____#####________####____######______###______###________###_______####____####__
______################_______#########_###______###______###________###________##########___
________########___####________#####___###______###______###________###__________######_____
____________________###_____________________________________________________________________
______________________#_____________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________
____________________________________________________________________________________________

上の AA からどのようにその情報を生成するために、文字種の切り替わり位置を記録して、配列で管理することにしました。

これからは文字種の切り替わり位置を、切替位置と呼びます。

具体的には以下のコードを実装しました。

str = ` /* AA をここに貼る*/`;

// 改行区切りで分ける
str = str.split('\n');

// 文字が変わったかどうかを見るためのフラグ
flag = '_';
for(let i in str){
    // 行内の切り替え位置を保存する配列
    let arr = [0];
    for(let j in str[i]){
        // 文字種が異なるならば
        if(str[i][j] != flag){
            // 配列に切り替え位置を挿入
            arr.push(parseInt(j));
            // flag を更新
            flag = str[i][j];
        }
    }
    // 行末の位置を追加
    arr.push(str[i].length);
    // 出力部分
    for(let j in arr){
        // 先頭
        if (arr[j] == 0){
            // 配列として出力させるために、 '[' と合わせて出力
            process.stdout.write(`[${arr[j]},`);
            continue;
        }
        if (arr[j] == str[i].length){
            // 行内終わりの数字であれば ']' と改行を合わせて出力
            // 最後の行であれば、カンマが要らないため、その処理も行う
            process.stdout.write(`${arr[j]}]`+ (i!=str.length-1 ? ',' : '') + `\n`);
            continue;
        }
        // それ以外はカンマ区切り
        process.stdout.write(`${arr[j]},`);
    }
}


これによって出力結果として以下のものが得られます。

[0,92],
[0,92],
[0,92],
[0,92],
[0,92],
[0,92],
[0,8,16,48,51,92],
[0,6,18,48,51,92],
[0,5,10,15,19,48,51,92],
[0,4,8,16,20,92],
[0,3,7,18,21,92],
[0,2,6,18,22,92],
[0,2,5,19,22,92],
[0,2,5,19,23,28,31,39,42,48,51,57,60,63,68,81,87,92],
[0,1,5,20,23,28,31,39,42,48,51,57,60,61,70,79,89,92],
[0,1,4,20,23,28,31,39,42,48,51,57,63,67,70,78,82,85,89,92],
[0,1,4,20,23,28,31,39,42,48,51,57,62,68,71,77,80,87,90,92],
[0,1,4,20,23,28,31,39,42,48,51,57,61,68,71,76,80,87,91,92],
[0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,79,88,91,92],
[0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,79,88,91,92],
[0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,91,92],
[0,1,5,20,23,28,31,39,42,48,51,57,60,68,71,76,91,92],
[0,2,5,12,14,20,23,28,31,39,42,48,51,57,60,68,71,76,79,92],
[0,2,5,12,15,19,22,28,31,39,42,48,51,57,60,68,71,76,79,92],
[0,2,6,12,16,19,22,28,31,39,42,48,51,57,60,68,71,76,79,92],
[0,3,7,13,21,28,31,38,42,48,51,57,60,68,71,76,80,89,90,92],
[0,4,8,15,21,28,31,37,42,48,51,57,60,68,71,77,80,88,91,92],
[0,5,10,15,20,28,32,36,42,48,51,57,60,68,71,78,82,86,90,92],
[0,6,22,29,38,39,42,48,51,57,60,68,71,79,89,92],
[0,8,16,19,23,31,36,39,42,48,51,57,60,68,71,81,87,92],
[0,20,23,92],
[0,22,23,92],
[0,92],
[0,92],
[0,92],
[0,92]

処理として、偶数番目の数で文字を配置しないことに、奇数番目の数で文字を配置することにしています。
その都合を受けて、最初と最後の数字は処理をしやすいように 0 と行の長さの数値で固定しています。
今回の場合、横幅が 92 文字ですので [0,92] としています。

AA の形にコードを配置する処理

AA を作るための情報を生成できたので、次はこれを用いてコードを AA の形に整える処理です。

処理部分のコードは以下のとおりです。

// 字数埋めに使う文字列
p='ThisIsAQuine!';
// 出力を保存するための変数
o='';
// 関数を文字列として取得し代入
t='f='+f+';f();';
// 空白を表す文字コードを代入
r='\\u0020';
// バッククォートをエスケープして使いやすくしている
s='\`';
// バッククォートで分割
t=t.replace(/[\\n]|[\\s]/g,'').split('\`');
// 関数の最後の部分を変数 u に代入
u=t[4];
t=t[0]+s+t[1]+s+t[2]+s+t[3];
c=0;

a = [
    [0,92],
    /* 省略 */
    [0,92]
];

// 二次元配列 a 内の各配列を見る
for(i=0;i<a.length;i++){
    // 終端処理
    if(i==a.length-1){
        // 必要な分だけコメントで字数埋め
        for(j=0;j<a[i].length-1;j++){
            for(k=a[i][j];k<a[i][j+1]-u.length-3;k++){
                o+=p[k%p.length];
                c++;
            }
        }
        // コメントアウト終了、必要な記述を追加
        o+="*/"+s+u;
        break;
    }
    // 二次元配列 a 内の配列内要素を見る
    for(j=0;j<a[i].length-1;j++){
        // 文字を置くか空白を置くかの処理
        for(k=a[i][j];k<a[i][j+1];k++){
            // 処理として出力に追加すべき文字数を超えていればコメントで字数埋め
            if(c>=t.length){
                // コメントアウトの開始
                if(c==t.length){
                    o+='/';
                    c++;
                    continue;
                }
                // 同上
                if(c==t.length+1){
                    o+='*';
                    c++;
                    continue;
                }
                // 添字の偶奇で置く文字を変更
                if(j%2){
                    // 偶数ならば空白
                    o+=r;
                }else{
                    // 奇数ならば文字
                    o+=p[c%p.length];
                    c++
                }
            }else{
                // 処理として記述すべき文字を追加し終わっていない場合
                if(j%2){
                    o+=r;
                }else{
                    o+=t[c];
                    c++
                }
            }
        }
    }
    // 末尾の改行
    o+="\\n";
}

Quine を作る都合上、スペースは取っ払ってしまうため、明示的な変数宣言を let a を行っていません。

詰まった部分だけをかいつまんで説明することとし、基本的な説明は上記のコードに添えたコメントに代えることとします。

詰まった部分

AA の形に整える処理

参考にさせていただいた記事では、 AA を作るための情報をどのように管理するかは示されていましたが、 AA 整形についてはトークン化に焦点を当てた解説でしたのでテンプレート文字列を用いた方法として実装するのに手間取りました。

AA を作るための情報をどのように利用したかというと、 2 次元配列に上の配列を入れて管理し、 for ループの範囲をそれに準じて行いました。

変数 a を 2 次元配列として、具体的には以下のような for ループを書きました。

for(i=0;i<a.length;i++){
    for(j=0;j<a[i].length-1;j++){
        for(k=a[i][j];k<a[i][j+1];k++){

変数 i に関わるループでは行ごとの処理を、変数 j に関わるループでは行内の文字が変わる場所として記録されているものを列挙する処理を行っています。
そして変数 k に関わるループでは、行内の文字が変わる部分の範囲に対して処理を行います。

具体的な処理手順を以下に示します。

まず、変数 a が以下のように定義されているとします。
0 と 10 はそれぞれ必ず最初と最後に入るものとします。

a= [ [ 0, 4, 5, 10 ], [ 0, 2, 5, 10 ] ]

先ほどのコードではまず、 [ 0, 4, 5, 10 ] という配列に対して処理が行われます。
変数 j に関わる部分では、 j の偶奇を利用して、文字を置くか、空白を置くかを切り替えます。
そして変数 k に関わる部分では、「0 から 3 まで」「4 から 5 まで」「5 から 10 まで」といった形で処理が走り、実際に文字を置いたり空白を置いたりしています。

テンプレート文字列を用いることによる制約

AA を作る処理は全てテンプレート文字列中に書かねばなりません。
テンプレート文字列中では、バッククォートやバックスラッシュがエスケープ文字として意味を持ったものとして解釈されてしまいますので、そこの処理に気を配るのは大変でした。

特に、バッククォートは曲者で、テンプレート文字列中にそのまま含めると意図しない動作となることが多々ありました。
そのため、変数に代入して扱うことで意図した動作を引き出しやすくしました。

その他にも何度も出力を見ながらどこをエスケープさせなければいけないのかを把握しつつ実装を行いました。

詳らかに言及できたら良かったのですが、 1 ヶ月ほどの期間で期間で緩く作っていたので忘れてしまいました……。

正規表現を用いた置換もあまり経験がなかったため勉強になりました。

調べてみると特殊文字として空白やタブを表す文字が用意されていてありがたく感じました。

以上で Quine を AA の形に整えられました。

コード全体

AA の形に出力される前のコードは以下のものとなります。
これを実行することで冒頭の Quine AA になります。

f=()=>{eval(`
p='ThisIsAQuine!';
o='';
t='f='+f+';f();';
r='\\u0020';
s='\`';
t=t.replace(/[\\n]|[\\s]/g,'').split('\`');
u=t[4];
t=t[0]+s+t[1]+s+t[2]+s+t[3];
c=0;
a=[
    [0,92],
    [0,92],
    [0,92],
    [0,92],
    [0,92],
    [0,92],
    [0,8,16,48,51,92],
    [0,6,18,48,51,92],
    [0,5,10,15,19,48,51,92],
    [0,4,8,16,20,92],
    [0,3,7,18,21,92],
    [0,2,6,18,22,92],
    [0,2,5,19,22,92],
    [0,2,5,19,23,28,31,39,42,48,51,57,60,63,68,81,87,92],
    [0,1,5,20,23,28,31,39,42,48,51,57,60,61,70,79,89,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,63,67,70,78,82,85,89,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,62,68,71,77,80,87,90,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,61,68,71,76,80,87,91,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,79,88,91,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,79,88,91,92],
    [0,1,4,20,23,28,31,39,42,48,51,57,60,68,71,76,91,92],
    [0,1,5,20,23,28,31,39,42,48,51,57,60,68,71,76,91,92],
    [0,2,5,12,14,20,23,28,31,39,42,48,51,57,60,68,71,76,79,92],
    [0,2,5,12,15,19,22,28,31,39,42,48,51,57,60,68,71,76,79,92],
    [0,2,6,12,16,19,22,28,31,39,42,48,51,57,60,68,71,76,79,92],
    [0,3,7,13,21,28,31,38,42,48,51,57,60,68,71,76,80,89,90,92],
    [0,4,8,15,21,28,31,37,42,48,51,57,60,68,71,77,80,88,91,92],
    [0,5,10,15,20,28,32,36,42,48,51,57,60,68,71,78,82,86,90,92],
    [0,6,22,29,38,39,42,48,51,57,60,68,71,79,89,92],
    [0,8,16,19,23,31,36,39,42,48,51,57,60,68,71,81,87,92],
    [0,20,23,92],
    [0,22,23,92],
    [0,92],
    [0,92],
    [0,92],
    [0,92]
];
for(i=0;i<a.length;i++){
    if(i==a.length-1){
        for(j=0;j<a[i].length-1;j++){
            for(k=a[i][j];k<a[i][j+1]-u.length-3;k++){
                o+=p[k%p.length];
                c++;
            }
        }
        o+="*/"+s+u;
        break;
    }
    for(j=0;j<a[i].length-1;j++){
        for(k=a[i][j];k<a[i][j+1];k++){
            if(c>=t.length){
                if(j%2){
                    o+=r;
                }else{
                    if(c==t.length){
                        o+='/';
                        c++;
                        continue;
                    }
                    if(c==t.length+1){
                        o+='*';
                        c++;
                        continue;
                    }
                    o+=p[c%p.length];
                    c++
                }
            }else{
                if(j%2){
                    o+=r;
                }else{
                    o+=t[c];
                    c++
                }
            }
        }
    }
    o+="\\n";
}
console.log(o);
`.replace(/([\n]|[\s])+/g,''));};f();

終わりに

先駆者様の情報がかなりありましたので、悩んだ部分はありましたがスムーズに実装できました。
詳細にまとめてくださっていて大変ありがたかったです。

やはりシンタックスハイライトを効かせるやり方の方が見栄えとして良いですが、 Quine を作れたのでとても満足です。

ここまでお読みいただきありがとうございました!

皆さんも年賀状に Quine をぜひ作ってみませんか?

おまけ

シンタックスハイライトが効かないのならば、ターミナル上で色をつけてみるのはどうだろうかと思いました。

以下、 2 つのサイトを参考に色付けと配色を行い、完成したものが以下の画像です。

スクリーンショット 2023-12-31 15.53.04.png

色の混ぜ方は改善の余地があるかと思いますが、とりあえずは色がついたので良しとしています。

やり方としては、 \x1b[38;2;86;166;209m という記述を表示したい文字列に混ぜると色がつけられるというものです。

例えば、

console.log('\x1b[38;2;86;166;209m 色がついたよ!');

とすると、水色で「色がついたよ!」と表示されます。

詳細なやり方は参考にした記事を参照してみてください。

なお、元記事と異なる部分は \033\x1b にしている部分です。
VSCode で警告が出ていたので、 8 進数表記から 16 進数表記にしました。

これでハガキに印刷した時に、カラフルな年賀状が作れるはずです!

また印刷できたら追記しようと思います。

8
4
2

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
8
4