Update 2023.12.23 2018.11.09

8パズル,15パズル:tkinterアニメーションで見せる
最短経路探索を双方向A*(A-Star,エースター)アルゴリズムで解く
Python3(3.10)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」


実際に動く Python ソースコードがあります。ドラッグして開発環境にコピペすればそのまま動きます。

◆◆8パズル◆◆
3×3 の枠に 1 から 8 のピースをあてはめて,1つの空ピースに四方の隣接ピースをスライドして,左上から 1 から順にピースを並べる。
1 から 6 まで揃っても 7 と 8 が入れ替わっていると順に並び替えることができないことが証明されている。

◆◆15パズル◆◆
4×4 の枠に 1 から 15 のピースをあてはめて,1つの空ピースに四方の隣接ピースをスライドして,左上から 1 から順にピースを並べる。
1 から 13 まで揃っても 14 と 15 が入れ替わっていると順に並び替えることができないことが証明されている。

(2023-12-23)Python3.10で動作確認済み,コードを追加「time.clock = time.time」,旧いモジュールが使えなくなったので新しいモジュールを変換して旧いプログラムと互換とする。

手数はイニシャル盤面を数えないことした。


fifteen_puzzle.pyをrunさせたところ,

51手で,19.8秒でした(普通のA*(A-Star)の約半分です。ただしA*は53手でした)

eight_puzzle.pyをrunさせたところ,

31手で,0.04秒でした

(CPU : Intel Core i7-1260P 2.10GHz)


【重要な注意】「PyCharm」では,fifteen_puzzle_animation.py②だけをrunします。fifteen_puzzle.py①は②にimportされます。①の自己テスト(if __name__=='__main__':)は無視され,①のdefとclassが②に呼ばれます。

「Jupyter Notebook」では,①を上に②を下に並べます。②のimport文はコメントアウトされていて,①と②が順にrunします。①の自己テストもrunして①のdefとclassもrunしてその定義が記憶され②に引き継がれます。

このように「Jupyter Notebook」と「PyCharm」とでは,ファイルの参照関係が違うので注意が必要です。



◆◆双方向A*(A-Star,エースター)アルゴリズム◆◆

経路探索アルゴリズムは主なものだけでも10種類ほどあるそうですが,その中で最良優先探索(best-first search)が特に探索回数が少なくて済むアルゴリズムらしい。最良優先探索はいくつか種類があるらしいが最も有名なのが A*(A-Star,エースター)アルゴリズムである。次に有名なダイクストラアルゴリズムは A*アルゴリズムに含まれています。

双方向A*アルゴリズムは A*アルゴリズムをさらに改良したものです。ソースコードは巻末に掲載してあります。ソースファイルは左上隅からダウンロードできます。

A*アルゴリズムをPythonで実装するとなぜこうなるのかを詳細に知りたい方は次のサイトを参照してください。8パズルの例です。ヒープを自作しているのでヒープの中身まで理解できます。
http://www.nct9.ne.jp/m_hiroi/light/pyalgo28.html
『Algorithms with Python - ヒューリスティック探索』(2023-12-21 修正)

このWebのプログラムは上のサイトのプログラムと考え方は同じですが実装がかなり違います。私のプログラムはもちろん私のオリジナルですが,上のサイトのプログラムの1行ごとにコメントを入れながら理解したからこそ自分のプログラムが書けたと思います。感謝しています。

本サイトの別記事でも書きましたが,私が書いた A*アルゴリズムの15パズルは探索回数が何百万回を超えメモリ不足のエラーになりました。そこである工夫をしてとにかく解を出すようにしたところ解は53手でした。時間は数分程度かかります。

上のサイトの双方向A*アルゴリズムの8パズルを私が15パズルに改造して初期盤面を同じにして比べてみました。解は57手です(著作権が設定されているので紹介できないのが残念)。

このWebで紹介する私が書いた双方向A*アルゴリズムの15パズルの解は51手でした(たぶん最短解でしょう)。時間は1分弱です。上のサイトのものより時間がかなり長いですが,A*アルゴリズムや双方向A*アルゴリズムのクセとして最短解に近くなるほど時間はずっと長くなります。それでも双方向A*アルゴリズムの方が A*アルゴリズムに比べて10倍程度速くなっていると思われます。

いずれにしても本サイトの別記事の A*アルゴリズムの8パズル,15パズルも参照してください。

◆◆盤面の局面の記録はクラスで実現◆◆

A*アルゴリズムのプログラムは本サイトの別記事のマイクロマウスの迷路探索とほとんど同じです。双方向A*アルゴリズムのプログラムで変えたのは open-list と close-list を採用しています。といってもフラグで識別しているだけです。

マイクロマウスの迷路探索と大きく違うのは盤面の局面の記録はクラスにしました(16行目)。初期盤面,ゴール盤面,探索盤面をインスタンス化しています(47, 48, 106行目)。クラスがインスタンス化されると,コンストラクタで盤面の状態が記録されます(18~29行目)。

つまりオブジェクト指向プログラミングしているわけです。狭義のオブジェクトとインスタンスは完全に同じ意味です。米国人には,オブジェクトは文系用語,インスタンスは理系用語に聞こえるというわけでインスタンスの方を使います。

インスタンス化するとその記録してあるすべての属性が1つの変数で呼び出せます。つまり面倒くさい配列から卒業できるわけです。

◆◆評価関数◆◆

A*アルゴリズムの評価関数の定義はこのアルゴリズムの最大の特徴である。ソースの25行目のクラスのコンストラクタにあります。それは,
  self.cost = self.distance + self.heuristic
である。

distance は今の探索盤面までの手数であり,クラスのインスタンス化のときに親 node つまり1手前の盤面の distance に1を加えていてそれが引数によって自動的に記録されます。

初期盤面とゴール盤面の distance はそれぞれのインスタンス化のときに固定値を記録しています。

heuristic は127行目の関数 calc_heuristic()であり,今の探索盤面からゴール盤面までの手数の予測値である。 インスタンス化されたときコンストラクタで distance と heuristic が上の式で足されて cost 予測値になります。

heuristic は今の探索盤面からゴール盤面までの手数の予測値であるが,理論上,必ず実際の手数よりも小さくなければならない。heuristic はこのルールを遵守すればどんな関数でもかまわない。ルールを遵守しなければ最適解(つまり,最良,最短)が得られないことが証明されているそうだ。

127行目の関数 calc_heuristic()では,ゴール盤面と一致しないピースの数と個々のピースがゴール盤面へ移動するときのマンハッタン距離の和を heuristic としている。どこかのWebにあったものを採用している。

debug すると判るのだが,same を除いて manhattan だけにしたものが最短解をだす。same を加えると速くなるのだが最短解ではない。 まさに上の理論通りであり heuristic が最短の手数を超えていると考えられる。

最短の手数の解を得るには heuristic の式を慎重に選び実際の手数を超えてはならない。逆に heuristic を大きくすれば最短解は得られないけれどかなり速くなる。きちんとゴールには到達するので役には立つ。

実はダイクストラアルゴリズムはこの heuristic を 0 にするつまり削除するだけで実装できるのである。

◆◆ヒープ◆◆

アルゴリズムと関係ないがプログラミング技法としてのヒープを理解しないとプログラムが読めない。

ヒープは使う側からみると,1次元の配列みたいなスタックである。ただ1番上にはいつもデータのうち最小値がいる。つまりいつもソートしているので特別な構造をしているわけである。しかしこの特別な構造をしらなくてもヒープは使うことができる。知っておかなければならないのは,最上位は最小値ということだけである。

特別なデータ構造とソートアルゴリズムを知る必要はまったくないが,なぜそんなことができるのかを知ることはおもしろい。

これはツリーというデータ構造である。
ツリーが10段あれば最下段は1024コのデータが並ぶ。
ある特別なアルゴリズムで枝を渡っていくとする。
ツリーからデータを探すのに最大10回の操作ですむ。
データを加えてツリーのしかるべき位置におさまるのに
最大10回の操作ですむ。
1次元配列で1000コの要素があれば1つのデータを探すのに平均500回の操作が必要である。データを1つ加えてそれを正しい位置までソートするのにもまた平均500回の操作が必要である。ヒープではそれが10回の操作ですむ。

くどいようだがヒープは使う側からみると,1次元の配列みたいなスタックである。中身をみても上のツリーのようにはなっていない。あくまでも配列のソートの考え方だけのことである。

◆◆双方向A*(エースター)アルゴリズムの予備知識◆◆

探索盤面の状態や局面のことを node といい,今探索している盤面を親 node,次の盤面を子 node という。次の盤面とは空ピースの位置に隣接ピースが入った盤面のことであり,親と子の node は下に向かって枝分かれしていくツリーとして書かれる。

すべての子 node 生成した親 node を close といい,生成してない子 node が残っている親 node を open という。

これは航空路や物流などのグラフ理論で地点を node というのとニュアンスが違うので注意してほしい。8パズルや15パズル,または詰め将棋や詰め碁でもよいがある盤状態・局面を最短経路探索では node という。つまり 1手先はツリーが枝分かれして新しい node となる。

探索では open-node を待ち行列にに登録して,適当な順序で取り出してその親 node から子 node を展開することを繰り返す。待ち行列を open-list と呼ぶこともある。 close-node も close-list に登録しておく。実際のプログラムでは,両方とも同じ待ち行列に入れてフラグで識別しているだけである。

ここまではさまざまな探索において共通の一般論である。

A*アルゴリズムの特徴の1つは待ち行列に priority queue (優先度付き待ち行列)を採用すること,もう1つは優先度を決める評価関数である。

A*アルゴリズムは初期画面からゴール画面へと探索していくのに対して,双方向A*アルゴリズムでは,初期画面からゴール画面へ,ゴール画面から初期画面へと双方向に探索していくのである。

そこで評価関数は,ゴール画面に向かうツリーでは初期盤面から現盤面までのコスト,初期画面に向かうツリーではゴール盤面から現盤面までのコストである。コストは現盤面までの最短経路のコスト(手数)と目標盤面までのコスト(手数)予測値の和である。

探索済みの盤面については open か close か識別されてコストと共に待ち行列に登録されている。A*アルゴリズムを実現するために待ち行列は前項で述べたヒープを採用している(heapq の heappush, heappop を使う)。

◆◆双方向A*(エースター)アルゴリズムの詳細◆◆

待ち行列から探索済み盤面 node を取り出し探索を繰り返すところを詳しくみてみよう。

58行目の while 文は待ち行列がある限り探索を続ける。 while 文を抜けたら探索は失敗である。このプログラムでは while 文を抜けたところに return文を書いてないので戻り値なしのエラーとなる。

ループの最初に待ち行列の中の最小コストの盤面(最初のタプルの要素[1])を取り出す(60,61行目)。それが close-node だったら何もしないでループの先頭に戻る。それが open-node だったらピースのない位置へスライドできるピースの座標を選び出す(66~68行目)。

ピースのない位置へスライドできるピースがある分だけ試行を繰り返す(70行目)。

スライド試行ループの先頭で次の盤面を構築する(72~76行目)。

その盤面が訪問済み(探索済みの中に同一画面がある)ならば探索方向が異なるかチェックする(82行目)。探索方向が異なれば解を発見したことになる。将来描画プログラムに解を渡すことを想定して解の配列をつくる(84~94行目)。そしてこのアルゴリズム関数を呼んだメインプログラムに解を戻り値として渡す(95行目)。

その盤面が訪問済み(探索済みの中に同一画面がある)でありかつ試行盤面の方がコストが小さいならば open-list に移し属性の再設定をする必要がある。しかし,その盤面 node を待ち行列から取り出すことはヒープの性格からみてかなり困難であるといえる。そこで既に待ち行列に入っている node を強制的にクローズして(101行目),全く新しくインスタンス化することにした(106行目)。

試行盤面が訪問済みでないときはその盤面をインスタンス化するだけである(106行目)。属性はコンストラクタで自動的に登録される。そしてそのインスタンスを訪問済みリストと待ち行列に登録する(109, 111行目)。

スライド試行が終了して子 node の展開が完了しているので親 node を close にする(113行目)。そして新しい待ち行列ができているので while文を繰り返す。

115~117行目はdebugしているときに探索回数が膨大になっても強制的に停止させるためにある。debugが終了したらコメントアウトすればよい。

◆◆8パズル◆◆

書き換えたのは,179~182行目の4行だけである。実は8パズルと15パズルの両方に使えるように慎重に作られている

しかし,この15パズルはこのままでは最短解をださなかった。

アルゴリズムを変えられなければ,変えられるのはただ1つ heuristic だけである。このプログラムでは heuristic は今の探索盤面のピース1つ1つが目標盤面へ移動するときのマンハッタン距離である。これが理論的に実際の手数より小さくないと最短解が得られないことが証明されている。

つまり最短解を出す方法は heuristic を小さくするほかないので,148行目を書き換えた。

逆に heuristic を大きくすれば速くなります。最短解でなくてもゴールには到達するので役には立ちます。 heuristic を大きくしておけば debug が簡単になります。

◆◆Tkinterによるアニメーション◆◆

描画プログラムはTkinterによるアニメーションを使っている。少々長いプログラムだが構造がしっかりしているので難しいところはない。

クラス PuzzleBoard が11行目で Tkinter.Canvasを継承している。18行目のコンストラクタも21行目で親コンストラクタを継承していて盤面を描いている。

この Canvas でピースの移動を描くので,コンストラクタの24行目で探索プログラムから解を受け取る。このクラスで重要な関数は,ピースの番号を描く64行目の関数 refresh() である。関数 refresh() はもう1つのクラスからアニメーションをつくるため繰り返し呼ばれている。

実際の描画は36行目の関数 numbering()でおこなっている。ピースの図を 4×4 にレイアウトして,番号はテキストで描いている。番号の付いたピースの図があるわけではない。最後に空のピースの図を0番に置く。というわけで図は2種類である。

もう1つのクラス PuzzleFrame が74行目で Tkinter.Frameを継承している。Canvas の枠でありタイトルなどを設定する。

この Frame で「start」ボタンを設定して,そのボタンから関数 show_next() を呼び出す。そしてこの関数からマウスを描画する関数 refresh() を再帰的に呼び出す。

107行目からのプログラムはこのモジュールが起動されたとき自動的に走るプログラムであり,クラスで Tkinter を使っているときはこの決まり文句になる。

少々長いので複雑にみえるが,いろいろ応用しても決まった構造は変えないので理解しやすいと考える。

アニメーションのコマ送り間隔は,9行目で設定できる。1以上を設定すること。単位はミリ秒であるが「1」に設定しても描画のためかそれほど速くない。1秒(設定値は1000)程度であると試行が人間の思考のように見える。

◆◆ソースコード◆◆

このWebの左上隅からダウンロードできます。

ソースファイルは4つです。図が2つです。
・eight_puzzle.py;8パズルの解法;探索プログラム
・fifteen_puzzle.py;15パズルの解法;探索プログラム
・fifteen_puzzle_animation.py;描画プログラム:fifteen_puzzle.py の解を読みに行きます
・fifteen_puzzle_sol.py;描画プログラムに解を渡すダミー(debug用)
・piece0.gif
・piece15.gif

「fifteen_puzzle.py」が描画プログラム「fifteen_puzzle_animation.py」に解を渡します。解を渡すまで1分ほどかかります。
「fifteen_puzzle.py」148行目の係数を大きくすることを推奨します。最短解でなくなりますがゴールにはきちんと到達します。係数を「10」にするとストレスなく描画が始まります。
解のアニメーションだけ見たいときは,「fifteen_puzzle_animation.py」の import するファイル名を解だけの「fifteen_puzzle_sol.py」に書き換えてください。

以上

【fifteen_puzzle.py】
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
15-puzzle path search by bidirectional A* algorithim using open&close list
"""
from heapq import heappush, heappop
from random import shuffle
import time
time.clock = time.time

OPEN = 0
CLOSE = 1
toGOAL = 0
toINIT = 1


class sBoard():

    def __init__(self, board_list, distance, parent, dir, node = OPEN):
        self._array = board_list
        self.distance = distance
        if dir == toGOAL:
            self.heuristic = calc_heuristic(board_list, goal_board)
        else:
            self.heuristic = calc_heuristic(board_list, init_board)
        self.cost = self.distance + self.heuristic
        self.parent = parent
        self.dir = dir
        self.node = node
        self.hashvalue = hash(tuple(self._array))

    def _getsBoard(self):
        return self._array

    def __hash__(self):
        return self.hashvalue

    def __eq__(self,other):
        return self._array == other._array

    def __lt__(self, other):
        return self._array < other._array

def astar():
    queue = []              # 待ち行列
    visited = {}            # 訪問済みnode;過去の盤面
    # インスタンス化
    start = sBoard(init_board, 0, None, toGOAL)
    end = sBoard(goal_board, 0, None, toINIT)
    # 訪問リストに登録
    visited[tuple(init_board)] = start
    visited[tuple(goal_board)] = end
    # 待ち行列にコストとインスタンスを登録
    heappush(queue, (start.cost, start))
    heappush(queue, (end.cost, end))

    No = 0                                              # debug
    # ゴールに到達するまで新しい盤面を探索する
    while len(queue) > 0:
        # 待ち行列からコスト最小の探索済みnode(盤面)を取り出す
        now_tuple = heappop(queue)
        now_board = now_tuple[1]                    # インスタンス
        # close-nodeならばループ先頭へ戻る
        if now_board.node == CLOSE: continue
        No += 1                                     # debug
        # ピースのない位置へ入ることのできる隣接座標
        index = now_board._array.index(0)
        x, y = XY_coord(index)
        coord_next_OK = coord_next(x, y)
        # 次のnode(盤面)を探索;ピースのない位置へスライドを試行
        for coord in coord_next_OK:
            # 次の盤面をつくる
            next_board = now_board._array[:]
            next_index = coord[0] * No_XY + coord[1]
            next_board[index], next_board[next_index] = \
                    next_board[next_index], next_board[index]
            key = tuple(next_board)
            # 同一盤面があるか(訪問済みか)確認する
            if key in visited:
                # 同一盤面がある
                visited_board = visited[key]
                # 両者の探索方向を確認する
                if now_board.dir != visited_board.dir:
                    # ゴールを発見
                    if now_board.dir == toGOAL:
                        # print 'toGOAL'                  # debug
                        sol1 =  get_sol(now_board)
                        sol2 = get_sol(visited_board)
                        sol1.reverse()
                    else:
                        # print 'toINIT'                  # debug
                        sol1 = get_sol(visited_board)
                        sol1.reverse()
                        sol2 = get_sol(now_board)
                    sol = sol1 + sol2
                    return sol, No

                # 同一盤面がある(訪問済み)がゴールではない
                if visited_board.cost > now_board.cost + 1:
                    # 訪問済みで今回のコストのほうが小さいならば更新する
                    # 以前のnodeをcloseにして新しいnodeをつくる
                    visited_board.node = CLOSE
                else:
                    continue
            # 未訪問ならばor訪問済みで今回のコストのほうが小さいならば
            # インスタンス化
            new_board = sBoard(next_board, now_board.distance+1, \
                                    now_board, now_board.dir)
            # 訪問済みリストに登録
            visited[key] = new_board
            # 待ち行列に登録
            heappush(queue, (new_board.cost, new_board))
        # 子ノードは展開済みなので親ノードをcloseする
        now_board.node = CLOSE
        #"""debug
        if No > 1000000:
            end = now_board
            break
        #"""

def get_sol(var):
    sol = []
    while var is not None:
        sol = sol + [var._array]
        var = var.parent
    return sol

def calc_heuristic(list, list2):
    """現局面からゴールまでのコスト予測値
    """
    now_board = list
    std_board = list2
    same = 0
    manhattan = 0
    for var in now_board:
        """ゴール盤面と一致しないピースの数"""
        x, y = XY_coord(var)
        if std_board.index(var) != now_board.index(var):
            same += 1
        """ゴール盤面へ移動するときのマンハッタン距離"""
        pos = std_board.index(var)
        std_board_x, std_board_y = XY_coord(pos)
        x, y = XY_coord(now_board.index(var))
        manhattan += abs(x-std_board_x) + abs(y-std_board_y)
    # print same, manhattan                   # debug
    # heuristic = same + manhattan            # debug
    heuristic = manhattan                   # debug
    # heuristic = same                        # debug
    return 0.79 * heuristic

def coord_next(x, y):
    """ピースのない位置へスライドできる隣接マスのXY座標のリストを返す
    """
    coord_next_OK = [[x, y]]
    # right
    if(x+1 < No_XY):
        coord_next_OK.append([x+1, y])
    # left
    if(x-1 >= 0):
        coord_next_OK.append([x-1, y])
    # down
    if(y-1 >= 0):
        coord_next_OK.append([x, y-1])
    # up
    if(y+1 < No_XY):
        coord_next_OK.append([x, y+1])

    return coord_next_OK

def XY_coord(index):
    """盤面配列のインデックスからXY座標を返す
    """
    x = index // No_XY
    y = index % No_XY
    return x, y

def main():
    global init_board, goal_board, goal_board2, No_XY

    No_XY = 4                                                               # 15
    goal_board = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0]     # 15
    goal_board2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 14, 0]    # 15
    init_board = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12]     # 15
    # shuffle(init_board)     # init_boardをシャッフルする
    sol, No = astar()
    return sol, No

if __name__ == '__main__':
    s = time.clock()
    sol, No = main()
    e = time.clock()
    print(sol)
    print(len(sol), No, "%.3f" % (e - s))

【fifteen_puzzle_animation.py】
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
8 queens animation
"""
import tkinter as Tk
import fifteen_puzzle as NP

Timer = 500 # ミリ秒;アニメ更新間隔(1以上を設定)

class PuzzleBoard(Tk.Canvas):
    """set initial value"""
    cell_size = 80
    margin = 5
    p_images = []
    p_figure = []

    def __init__(self, master):
        """set initial value"""
        cwidth = 4 * self.cell_size
        Tk.Canvas.__init__(self, master, relief=Tk.RAISED, bd=4, bg='white',
                            width=cwidth, height=cwidth)
        """read solution"""
        p_sol, No_sol = NP.main()
        self.p_sol = p_sol
        self.len_sol = len(p_sol)
        print(self.p_sol)                    # debug
        print(self.len_sol)                  # debug
        # 図を登録
        self.p_images.append(Tk.PhotoImage(file="piece0.gif"))
        self.p_images.append(Tk.PhotoImage(file="piece15.gif"))
        # 初期盤面を描く
        sol = self.p_sol[0]
        self.numbering(sol)

    def numbering(self, sol):
        """ピース番号を付す
        """
        for row in range(4):
            for col in range(4):
                rr = self.cell_size*(row+0.5) + self.margin
                cc = self.cell_size*(col+0.5) + self.margin
                self.p_figure.append(self.create_image
                             (cc, rr, image=self.p_images[1], tags="piece15"))
                nn = row * 4 + col
                chr = str(sol[nn])
                self.create_text(
                        cc, rr, text = chr, justify=Tk.CENTER, fill="#ff00ff",
                        font = ("Times","36","bold"), tag = chr)
        pos0 = sol.index(0)
        row, col = self.XY_coord(pos0)
        rr = self.cell_size*(row+0.5) + self.margin
        cc = self.cell_size*(col+0.5) + self.margin
        self.p_figure.append(self.create_image
                (cc, rr, image=self.p_images[0], tags="piece0"))

    def XY_coord(self, index):
        """盤面配列のインデックスから座標を返す
        """
        r = index // 4
        c = index % 4
        return r, c

    def refresh(self, num_sol):
        """ピース空位置を動かす
        """
        sol = self.p_sol[num_sol]
        self.numbering(sol)
        if num_sol == self.len_sol - 1:
            return True
        else:
            return False

class PuzzleFrame(Tk.Frame):

    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.master.title("15-Puzzle")
        # title
        l_title = Tk.Label(self, text='15 Puzzle',
                           font=('Times', '24', ('italic', 'bold')),
                           fg='#191970', bg='#E8B77D', width=12)
        l_title.pack(padx=10, pady=10)
        # board
        self.f_board = PuzzleBoard(self)
        self.f_board.pack(padx=10, pady=10)
        # buttons and a counter
        self.f_footer = Tk.Frame(self)
        self.f_footer.pack()
        self.a_button = Tk.Button(self.f_footer, text="start",
                        font=("Times", 14), command = self.show_next)
        self.a_button.pack(side=Tk.LEFT, padx=5,pady=5)
        """set initial value"""
        self.No_step = 0

    def show_next(self):
        """アニメーションのコマ送り
        """
        sol = self.f_board.refresh(self.No_step)
        if sol:
            """解発見"""
            return
        self.No_step += 1
        self.after(Timer, self.show_next)


if __name__ == "__main__":
    app = PuzzleFrame()
    app.pack()
    app.mainloop()

トップページに戻る
inserted by FC2 system