メインコンテンツへスキップ

画像でプログラムを描く!Pietでアイコンを作ってみた

目次

こんにちは、tamate(@tamate39) です。

今回は、画像がプログラムになるという、ちょっと変わったプログラミング言語「Piet」を使って、このサイトのアイコン(ファビコン)を作っていきます。

今回の目標
#

  • 「t」の字が真ん中にあり、Piet として実行したら @tamate39 と出力するアイコンを作る。

プログラミング言語「Piet」
#

まずは以下の画像をご覧ください。

Thomas Schoch, CC BY-SA 3.0, via Wikimedia Commons

カラフルな四角形と黒い線で構成されている絵ですが、プログラミングと関係ない絵を突然出しているわけではありません。実はこの画像自体がプログラムのコードになっています。

上記の画像は「Piet」と表示するプログラムです。

もう一つ例を出しておきましょう。以下の画像は Pietで記述したHello worldです。

Thomas Schoch, CC BY-SA 3.0, via Wikimedia Commons

このように、Piet は画像でコードを「描く」という非常に珍しいプログラミング言語です。

Piet の言語仕様
#

Piet の仕組みについて軽く解説します。

色の変化と命令
#

Piet は、色の変化でプログラムの処理を表現します。プログラムは左上から始まり、色のブロックを辿りながら実行されます。

最小の四角形をコーデル、同じ色のコーデルが集まったものをブロックといいます。Piet では、1ブロックあたり1命令になります。

Thomas Schoch, CC BY-SA 3.0, via Wikimedia Commons

Piet で使える色は、明度3段階と色相6種類の18色と、白・黒の合計20色です。これらを並べていき、色の変化を利用して命令を実行していきます。

具体的には、以下のような変化で命令が決まります。

明度変化なし 明度変化 1 明度変化 2
色相変化なし なし push pop
色相変化 1 add subtract multiply
色相変化 2 divide mod not
色相変化 3 greater pointer switch
色相変化 4 duplicate roll in (number)
色相変化 5 in (char) out (number) out (char)

push, pop はスタックに入れたりスタックから出したりする操作になります。duplicaterollもスタックに関連している操作で、duplicateはスタックの一番上の要素の複製、rollはスタック内の要素の順番を入れ替える操作です。

add, subtract, multiply, divide, mod は四則演算と除算の余り、not, greater は否定と比較、in, out は入出力になります。ここあたりはC言語などにも似たような命令がありますね。

pointer, switch は Piet ならではの命令です。先ほど色のブロックをたどりながら実行されると述べましたが、そのブロックの辿り方を変更する命令です。方向転換や、特定の場合での行き先の指定などができます。

白と黒のブロックは特殊なブロックで、白は何もせず通過、黒は壁の役割を持ちます。

スタック
#

先ほど出てきたスタックについて軽く触れてみます。

スタックは、データを積み重ねて管理する仕組みのことです。最後に入れたデータが最初に取り出されるという特徴があります。

Vectorization: Alhadis, CC0, via Wikimedia Commons

Piet では、このスタックに数値を積んだり(push)、取り出したり(pop)、計算したりしながらプログラムを実行していきます。

具体例を見てみましょう。

  1. push 3: スタックに3を積む → [3]
  2. push 5: スタックに5を積む → [3, 5]
  3. add: スタックから2つ取り出して足し算し、結果を積む → [8]
  4. out (number): スタックから取り出して数値として出力 → 「8」が出力される

このように、スタックを使って数値を管理し、計算を行っていくのが Piet の基本的な動作です。

pushの際に使う数値(push 33など)は、前のブロックのコーデルの数になります。

実践(テキスト)
#

仕様の解説の部分が少し長くなってしまいましたが、実際にプログラムを書いていきます。

print("@tamate39")と書いただけで出力されるほど Piet は甘くありません。文字コードに対応する数字を作って、それを文字として出力するという操作を、文字数分行う必要があります。

いきなりピクセルを書いていくと絶対に途中でわからなくなるので、まずはテキストで設計を書いていきます。

まずは@tamate39の各文字の文字コードを調べます。半角英数字はASCIIコードになるので、ASCIIコード表で調べます。@tamate3964, 116, 97, 109, 97, 116, 101, 51, 57となるようです。

これらの数字を作り、出力していきます。

まずは@と出力してみましょう。スタックに64を作って出力すれば@が出力されます。64マスのコーデルを使ったブロックを作ってpushすれば64がプッシュできるのですが、それでは面白味に欠けるので、今回は使うコーデルの数をできるだけ少なくしていきたいです。

というわけで以下が@を出力する手順の一例です。これなら9マスで@を出力できます。

push 4: [4]
dup:    [4,4]
dup:    [4,4,4]
multi:  [4,16]
multi:  [64]
out(c): [] (@を出力)

@に対応する 64 は、 \( 4^3 = 64 \) ですのでまだ作りやすい方ですが、数字によっては足し算や引き算が必要になってきます。

さらに、コード量を減らすためにあらかじめ数字をコピーしておくとか、スタック内の順番を入れ替えて文字を保持しておくとか、そういうことを考え始めるとどんどん複雑になってきます。

このコード量を減らす工夫をどれだけ上手くやるかが Piet の面白いポイントでもあるのですが、記事の本文中に書いてしまうと大変なことになってしまいますので、こだわったポイントを実際のコードとともに記事の最後に載せておきますね。

実践(Piet)
#

さて、いよいよ設計をもとにして Piet で描いていきます。今回は Pietron というソフトを使いました。

Pietron の画面

上の画像はプログラムを書いている途中のものです。@まで出力できます。また、@の次の文字を作りやすくなるように64をコピーしています。

今回は薄ピンク色から始めましたが、プログラムに関係あるのは色の変化のみなので、どの色から始めてもプログラム的な意味は変わりません。

以下に、命令とそれを表現する色の変化を載せておきました。

命令 スタック 色の変化 実際の色 補足
push 4 [4] 色彩+0, 明度+1 pushの数はブロックのコーデルの数
dup [4, 4] 色彩+4, 明度+0
dup [4, 4, 4] 色彩+4, 明度+0
multi [4, 16] 色彩+1, 明度+2
multi [64] 色彩+1, 明度+2
dup [64, 64] 色彩+4, 明度+0
out(c) [64] 色彩+5, 明度+2 @を出力

このようにプログラムを描いていきます。なかなか果てしない作業です。

Pietで描く際の注意点
#

方向転換
#

Piet では、プログラムの実行が画面の端に到達すると自動的に方向を変えてくれます。しかし、画面の外周以外の場所で折り返す場合は、方向転換用のコードを自分で書く必要があります。この方向転換には概ね2マス必要なので、最初にキャンバスのサイズを決めるときから考慮しておく必要があります。

ブロックの色の干渉
#

隣り合うコーデルの色が同じ色になってしまうと、同じブロックだと解釈されるので命令が変わってしまいます。そのため、手前のブロックを引き伸ばしたり、別の処理に変えたりするなどして、違うブロックの隣り合うコーデルが同じ色にならないように注意する必要があります。

色が被ることへの対策として、あらかじめ置き換え可能な命令を想定しておくと良いと思います。例えば、tに対応した116を作るのにもいろいろな方法があります。64+64-(3*4)で作っていて色が被った場合は64+64-(4*3)((64/2)-3)*4で計算すれば色の被りを回避できるかもしれません。このようなことを設計段階から考えておくと良いと思います。

プログラムの終了
#

プログラムは白いブロック上で無限ループになった場合や、黒いブロックに囲まれて脱出ができなくなった場合に終了します。コードを書き終わったつもりでいても、プログラムが終了するような配置にしないと勝手に先に進んで意図しない命令が実行され続けるため注意しましょう。

完成
#

試行錯誤の末、アイコンが完成しました!今後はサイトのファビコンとしてこのアイコンを使っていきます。

感想
#

Piet を使ってみた感想ですが、モダンな言語を使っていると起こらないバグが頻出するのが面白かったです。Piet では方向転換の処理を作らないと一周した後に同じ場所を通ってまた命令を実行していくのですが、これは現在使われているほとんどの言語では起こらないような挙動ですよね。

手前のコードの変更がそれ以降のコード全てに影響することや、コード量を減らすことを優先して可読性が落ちることも新鮮でした。昔の開発環境ではこのようなこともあったと考えると面白いです。

私はC言語よりもコンピュータに近い言語は触ったことがなかったのですが、あえて低水準な言語を触ってみることでプログラミング言語の進化を身を以て感じることができると思いました。

PythonやJava、C#などからプログラミングに入った方、低水準な言語の世界をちょっと覗いてみてはいかがでしょうか。

あと「遊び」としてのプログラミングは面白いから君も Piet で自分のアイコンを作ろう

参考
#

おまけ
#

低水準な言語をもっと見てみたいあなたにおすすめの動画
#

今回のコード(テキスト)
#

命令 スタック 補足
push 4 [4] 戦略: @の64は4*4*4で作る
dup [4, 4]
dup [4, 4, 4]
multi [4, 16]
multi [64]
dup [64, 64] tを作りやすいようにあらかじめ複製
out(c) [64] @を出力
dup [64, 64] 戦略: t11664+64-(64/5)で作る
dup [64, 64, 64]
dup [64, 64, 64, 64]
push 5 [64, 64, 64, 64, 5]
div [64, 64, 64, 12] 64÷5=12…4
sub [64, 64, 52] 64-12=52
add [64, 116] 64+52=116
dup [64, 116, 116] あとでもう一度tが出てくるので複製しておく
out(c) [64, 116] tを出力
push 2 [64, 116, 2] 2つ目のtをスタックの底で保持するためのrollの準備
push 1 [64, 116, 2, 1] rollの準備
roll [116, 64] スタックを回転して116を底へ
dup [116, 64, 64] 戦略: a9764+(64/2)+1で作る
push 2 [116, 64, 64, 2]
div [116, 64, 32] 64÷2=32
add [116, 96] 64+32=96
push 1 [116, 96, 1]
add [116, 97] 96+1=97
dup [116, 97, 97]
dup [116, 97, 97, 97] 2つ目のaをあらかじめ作る
out(c) [116, 97, 97] aを出力
push 1 [116, 97, 97, 1] 方向転換の準備
point [116, 97, 97] 方向転換
push 4 [116, 97, 97, 4] 戦略: m10997+(4*3)で作る
push 3 [116, 97, 97, 4, 3]
push 1 [116, 97, 97, 4, 3, 1] 方向転換の準備
point [116, 97, 97, 4, 3] 方向転換
multi [116, 97, 97, 12] 4×3=12
add [116, 97, 109] 97+12=109
out(c) [116, 97] mを出力
out(c) [116] 事前に作っておいたaを出力
dup [116, 116]
out(c) [116] 事前に作っておいたtを出力
push 1 [116, 1] 方向転換の準備
point [116] 方向転換
push 5 [116, 5] 戦略: e105116-(5*3)で作る
push 3 [116, 5, 3]
multi [116, 15] 5×3=15
sub [101] 116-15=101
dup [101, 101]
push 1 [101, 101, 1] 方向転換の準備
point [101, 101] 方向転換
out(c) [101] eを出力
push 2 [101, 2] 戦略: 351(101/2)+1で作る
div [50] 101÷2=50…1
push 1 [50, 1]
add [51] 50+1=51
push 1 [51, 1] 方向転換の準備
point [51] 方向転換
dup [51, 51]
out(c) [51] 3を出力。次の戦略: 95751+6で作る
push 1 [51, 1] 6を作るスペースが無いので無駄な処理をする
push 1 [51, 1, 1] 無駄な処理2
roll [51] 無駄な処理3
push 1 [51, 1] 方向転換の準備
point [51] 方向転換
push 6 [51, 6] 黒いコーデルに当てて方向転換させる
add [57] 51+6=57
out(c) [] 9を出力