こんにちは、tamate(@tamate39) です。
今回は、画像がプログラムになるという、ちょっと変わったプログラミング言語「Piet」を使って、このサイトのアイコン(ファビコン)を作っていきます。
今回の目標 #
- 「t」の字が真ん中にあり、Piet として実行したら
@tamate39と出力するアイコンを作る。
プログラミング言語「Piet」 #
まずは以下の画像をご覧ください。
カラフルな四角形と黒い線で構成されている絵ですが、プログラミングと関係ない絵を突然出しているわけではありません。実はこの画像自体がプログラムのコードになっています。
上記の画像は「Piet」と表示するプログラムです。
もう一つ例を出しておきましょう。以下の画像は Pietで記述したHello worldです。
このように、Piet は画像でコードを「描く」という非常に珍しいプログラミング言語です。
Piet の言語仕様 #
Piet の仕組みについて軽く解説します。
色の変化と命令 #
Piet は、色の変化でプログラムの処理を表現します。プログラムは左上から始まり、色のブロックを辿りながら実行されます。
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 はスタックに入れたりスタックから出したりする操作になります。duplicateとrollもスタックに関連している操作で、duplicateはスタックの一番上の要素の複製、rollはスタック内の要素の順番を入れ替える操作です。
add, subtract, multiply, divide, mod は四則演算と除算の余り、not, greater は否定と比較、in, out は入出力になります。ここあたりはC言語などにも似たような命令がありますね。
pointer, switch は Piet ならではの命令です。先ほど色のブロックをたどりながら実行されると述べましたが、そのブロックの辿り方を変更する命令です。方向転換や、特定の場合での行き先の指定などができます。
スタック #
先ほど出てきたスタックについて軽く触れてみます。
スタックは、データを積み重ねて管理する仕組みのことです。最後に入れたデータが最初に取り出されるという特徴があります。
Piet では、このスタックに数値を積んだり(push)、取り出したり(pop)、計算したりしながらプログラムを実行していきます。
具体例を見てみましょう。
push 3: スタックに3を積む →[3]push 5: スタックに5を積む →[3, 5]add: スタックから2つ取り出して足し算し、結果を積む →[8]out (number): スタックから取り出して数値として出力 → 「8」が出力される
このように、スタックを使って数値を管理し、計算を行っていくのが Piet の基本的な動作です。
pushの際に使う数値(push 3の3など)は、前のブロックのコーデルの数になります。
実践(テキスト) #
仕様の解説の部分が少し長くなってしまいましたが、実際にプログラムを書いていきます。
print("@tamate39")と書いただけで出力されるほど Piet は甘くありません。文字コードに対応する数字を作って、それを文字として出力するという操作を、文字数分行う必要があります。
いきなりピクセルを書いていくと絶対に途中でわからなくなるので、まずはテキストで設計を書いていきます。
まずは@tamate39の各文字の文字コードを調べます。半角英数字はASCIIコードになるので、ASCIIコード表で調べます。@tamate39は64, 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 というソフトを使いました。
上の画像はプログラムを書いている途中のものです。@まで出力できます。また、@の次の文字を作りやすくなるように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】 - ニコニコ動画
この動画がなければPietと出会ってなかったかも
-
Wikipediaの言語仕様は日本語でわかりやすいです
-
DM’s Esoteric Programming Languages - Piet
詳しい言語仕様(英語)
-
使用したエディタ
おまけ #
低水準な言語をもっと見てみたいあなたにおすすめの動画 #
-
アルカディアのゲームを作ろう(提案).mp1 - ニコニコ動画
低水準も低水準、機械語でゲームを作っている動画です。ちょっと何言ってるか分からない(困惑)という感じですが、雰囲気が面白くて好きです。
今回のコード(テキスト) #
| 命令 | スタック | 補足 |
|---|---|---|
| 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] |
戦略: tの116を64+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] |
戦略: aの97を64+(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] |
戦略: mの109は97+(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] |
戦略: eの105を116-(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] |
戦略: 3の51を(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を出力。次の戦略: 9の57を51+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を出力 |