BrainF*ckで15パズル

折角ブログをつくったので,部誌で書いた記事を少し改変してここで再掲したいと思います.

プログラミングを学び始めたときに初心者用の言語としてBrainF*ckが紹介されていたので,それで作った15パズルを紹介します.この言語についてはネットに沢山解説記事が上がっているのでそちらを参照ください.


プログラム

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 ※po=xはポインタの位置がx番メモリにあるの意味。最初のメモリは1番メモリとする。
指定されたメモリにパズルの番号+66を入力。改行の箇所には10、空白は0、その他は1
+>+>+>+>++++[>++++<-]+> po=6
[>++++>++++>++++>++++>+>
>++++>++++>++++>++++>+>
>++++>++++>++++>++++>+>
>++++>++++>++++>>+ po=29
<<<<<<<<<<<<<<<<<<<<<<<-]+ po=6
>+++>++++>+++++>++++++
>------>+>+++++++>++++++++>+++++++++>++++++++++
>------>+>+++++++++++>++++++++++++>+++++++++++++>++++++++++++++
>------>+>+++++++++++++++>++++++++++++++++>+++++++++++++++++
>>------>+>+>+>+>+>
ここまで初期設定 po=35

以下ループ
※一旦1を代入しておいてループの部分が飛ばされてしまうのを防ぐ
+[-
  
ここから描画処理
マスの先頭へ
<<<<<<<<<<<<<<<<<<<<<<<<<<<< po=7
全体にー2して出力させることで、空白のメモリをー2=126にし、チルダを出力させる。
--.++>--.++>--.++>--.++>.>>
--.++>--.++>--.++>--.++>.>>
--.++>--.++>--.++>--.++>.>>
--.++>--.++>--.++>--.++>.>> po=31
>>>> po=35


以上。入力へ
,
読み込んだ値を36、38、40、42にコピー 
[->+>>+>>+>>+<<<<<<<]  po=35

入力がAかどうかの判定
 >>++++++++++++[-<-------->]<- 36の値から97を引く、po=36
 >+<[>-<[-]]>[
  Aならここを実行、あとは飛ばす po=37
38、40、42に十分大きな値を入れて負になるのを防ぐ。
  >>+++++[<+++++>-]
 >>+++++[<+++++>-]
 >>+++++[<+++++>-] po=43
<<<<<<<<<< po=33
  [<]  パズルの空白の位置まで移動
  <-[  空白の左隣が1じゃないならここを実行
    [>+<-]>+<]  左隣の値を空白に移動
  +>[<-]>>>[>]   もし空白だったところの左隣が1だったら、現在0になっているので、そのときのみ1を足して元に戻す po=35
 >>-]      37番を0にする 

以上、Aの判定


Dの判定
>>++++++++++[-<---------->]< po=38
>+<[>-<[-]]>[ po=39
>>+++++[<+++++>-] po=41 >>+++++[<+++++>-] po=43 <<<<<<<<<[<]
>----------[
[<+>-]<++++++++++>]
++++++++++<[>----------]>>[>]
>>>>-] if po 38 = d now po=39

Sの判定
>>+++++++++++[-<---------->]<----- po=40 minus115
>+<[>-<[-]]>[ po=41
>+++++<<<<<<<<[<]
>>>>>>-[
[<<<<<<+>>>>>>-]<<<<<<+>>>>>>]
+<<<<<<[>>>>>>-]>[>]
>>>>>>-] if po 40 = s now po=41

Wの判定
>>+++++++++++[-<---------->]<--------- po=42 minus119
>+<[>-<[-]]>[ po=43
<<<<<<<<<[<]
<<<<<<-[
[>>>>>>+<<<<<<-]>>>>>>+<<<<<<]
+>>>>>>[<<<<<<-]>[>]
>>>>>>>>-] if po 42 = w now po=43

<<<<<<<< po=35
出入力へループ
+]


概要

A~Nはそれぞれ1~15に対応し、?が空白となっております。操作はBrainFckの入力パネルにw(半角)を入力することで空白に対応する部分が上に、a,s,dも同様にそれぞれ左、下、右に動きます。
BrainF
ck製である以上、以下の欠陥があります。

  • シャッフル機能がないです。なんか適当に文字入力してもらうのも考えましたが、面d(ry
  • インタプリタによっては、入力してもまた入力ウィンドウがでる無限ループに入ります。

メモリの割り振り

使用したメモリの数は43です。下に主な用途を書きますが、この言語の特性上このほかにメモリをいろいろ使いまわします.例えば35、36はパズルの描画のために「63」を一時的に作るために使われます。

メモリの位置 主な用途 備考
1~34 パズルのマスに対応 なぜ34マスもあるかは後述
35 キーの入力値を一時格納 すぐに36,38,40,42にコピー
36,37 Aが入力された時の判定 以下、42,43までそれぞれD,S,Wに対応

初期設定

まず、マスの状況を保存する部分をはじめに15パズルの揃った状況に設定します。ここで、本来の16マスよりも大幅に多い34個のメモリを使う理由ですが、下のように4×4のマスを6×6に拡張したためです(いわゆる番兵の設置)。

このようにメモリを拡張することで、実際に用いる真ん中の16個のメモリのある場所からの上下左右が常に±1、6になります。このとき、パズルに対応する16個のメモリのうち、数字に対応する部分はその値+1を、空白の部分は0を格納します。周囲のメモリは1を入力しておきます。空白に対応する部分を0にすることで、‘’]''の命令で0か正かしか判定できないこの言語で後で参照しやすくします。


入力キーの判定、移動処理

この後、入力待ちの状態にし、35番メモリにそれを8ビットの整数で代入させ、それを4箇所にコピーします(コピー元は消えちゃいます)。入力が予想される値はa(97),d(100),s(115),w(119) {括弧内は文字コードの10進数値}で、まず文字コードの値が若い順に処理します。コピーした値のうち1つから97を引き、0になるかを判定します。(この判定の書き方はこちらをパクらせていただきました。)もし0ならさっきコピーした残りの値に十分大きな値を足しておいて(別の値の判定で97より大きい値を引いて負にさせないため)、パズルの移動にかかります。といっても1~34のメモリの中で0になっている値とその左隣の値を交換するだけなので単純です。ただし、空白の左隣がパズルの4×4マスの外(つまり値が1)なら、その移動は実行しません。

この処理をd,s,wでも行います。


描画

移動処理が終われば描画するだけですが、これはマスの状態が記録されてるメモリに63を足して出力する(Aの文字コードの値は65)だけなので大したことないです。35、36番メモリ(このとき、値は常に0です)に63をあらかじめ作っておき、一気に該当のメモリに足したあと順に出力します。このとき、パズルの4*4のマスの右の外にある列に対応するメモリの値は10にして改行を出力させます。

あとはメモリの値を元に戻してループさせるだけです。


まとめ

今思うとよくこんな面倒なものを作ったなあと思いました。この時期にNP性の証明とか書かせてたらチューリングマシンの動作をきちんと書いてたかもしれません。