Stage 4 盤面を動かす
4-2 4 通りの場合わけをして盤面を動かす
さて、ここからは実際のコーディングに入ります。4-1 で考えたフローチャートと root や focused の移動にのっとって、上下左右全ての方向についてそれぞれ場合わけし、盤面の挙動をコーディングします。
ただ、今回はモノホンのコードがかなり複雑なので解説用コードを別に作りました。まずは解説用コードでイメージを掴んだあと、コーディング本番に入ります。
~~~~~~~~~~~~~~~
それでは move.py のファイルをご覧ください。
import board
from config import *
class Move(board.Board):
def board_move(self, direction: list, *, logger=None):
logger = logger or self.logger
ever_moved = False
# up
if direction == UP:
logger.debug('direction is UP')
root = [0, 0]
for root[COL] in range(self.size):
for root[ROW] in range(self.size):
focused = [root[ROW] + 1, root[COL]]
while self.isInBoard(focused[ROW]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root]==board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[ROW] += 1
elif direction == DOWN:
logger.debug('direction is DOWN')
root = [0, 0]
for root[COL] in range(self.size):
for root[ROW] in range(self.size - 1, -1, -1):
focused = [root[ROW] - 1, root[COL]]
while self.isInBoard(focused[ROW]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root]==board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[ROW] -= 1
elif direction == LEFT:
logger.debug('direction is LEFT')
root = [0, 0]
for root[ROW] in range(self.size):
for root[COL] in range(self.size):
focused = [root[ROW], root[COL]+1]
while self.isInBoard(focused[COL]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root]==board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[COL] += 1
elif direction == RIGHT:
logger.debug('direction is RIGHT')
root = [0, 0]
for root[ROW] in range(self.size):
for root[COL] in range(self.size - 1, -1, -1):
focused = [root[ROW], root[COL] - 1]
while self.isInBoard(focused[COL]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root]==board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[COL] -= 1
else:
logger.error('INPUT DIRECTION is INVALID VALUE')
return FAILED
if ever_moved == True:
return SUCCEEDED
else:
# if no number was moved, it is game over
logger.debug('ever_moved == False')
return FAILED
量多いな...
これがいやだから多少複雑でも短いコードを本番で使います。
~~~~~~~~~~~~~~~
まずはいろいろ import しましょうか。
import board
from config import *
本文では Move クラスという形で board.py の Board クラスを継承しています。この中で board_move というここだけのメソッドを定義します。
class Move(board.Board):
def board_move(self, direction: list, *, logger=None):
...
クラスの引数にクラス入れて継承やったな
そうです。その通りです。
~~~~~~~~~~~~~~~
まずはメソッドの引数からみていきましょうか。
def board_move(self, direction: list, *, logger=None):
direction は方向を表すリストです。2-2 で決めた UP とか LEFT などの方向を入れていきます。
direction の後に ": list" ってつけているのはなんでなん?
引数の型をリストに指定するためです。int とかが入れられると、その時点でエラーを出します。
def board_move(self, direction: list, *, logger=None):
このメソッドの中でのログは全て logger に記録されます。
でもデフォルト None でロガーなんてないじゃん
ではその解説に入りましょうか。
~~~~~~~~~~~~~~~
メソッドの中身のコードは最初こんなんになってますよね。
def board_move(self, direction: list, *, logger=None):
logger = logger or self.logger
ever_moved = False
太字で強調した logger = の部分でロガーの設定を行なっています。
もし引数に何かロガーが入れられたら、言うまでもなく引数のロガーを使いますよね。
そらそうや、使えって言ってんだからな
では引数に何も入れられなかった時、logger or self.logger
の値はどうなるでしょうか。
logger がデフォルトで None なんだから self.logger になるな
~~~~~~~~~~~~~~~
次の ever_moved という変数は「数字が動いたか」を bool 型で記録しておくものです。
def board_move(self, direction: list, *, logger=None):
logger = logger or self.logger
ever_moved = False
例えばこんな場合を考えてみましょう。
この状態から盤面を上に動かそうと思っても、全ての数字が上に上がれませんから無理ですね。こんなとき「盤面を動かせなかった」ということで FAILED をリターンしたいんですよ。
そこでこの ever_moved を使うわけです。どこかしら数字が動いたらこの値を True に入れ替えて、ever_moved が True なら最後に「動かせる」ということで SUCCEEDED をリターンする、False なら FAILED をリターンする、そんな形を考えています。
~~~~~~~~~~~~~~~
ここからは上下左右各方向についてどういう動きをさせるかみていきましょう。
1-2 で考えた root と focused という二つのマスの位置関係を思い出してみてください。
focused の数字を root に移すってやつやろ
そうですね。まずは上に盤面を動かす場合を考えます。direction == UP の場合です。
# up
if direction == UP:
logger.debug('direction is UP')
root = [0, 0]
...
これに沿うように root と focused の位置を変更していきます。
リストの root を先に宣言しておきます。
# up
if direction == UP:
logger.debug('direction is UP')
root = [0, 0]
for root[COL] in range(self.size):
for root[ROW] in range(self.size):
ここでの [0, 0] という値には特に意味はなく、ただ要素数が 2 のリストを用意したにすぎません。どうせこのあと for ループで初期位置を指定しますから。
~~~~~~~~~~~~~~~
4-1 でお見せしたフローチャートを再掲します。
これによればまず最初に root を調節する必要がありますね。row と col で分けた 2 重の for ループでしっかり回しておきます。
logger.debug('direction is UP')
root = [0, 0]
for root[COL] in range(self.size):
for root[ROW] in range(self.size):
focused = [root[ROW] + 1, root[COL]]
while self.isInBoard(focused[ROW]):
次は focused の位置を決めていきます。
これをみていただければお分かりでしょうが、focused の初期位置は root の真下ですね。
for root[COL] in range(self.size):
for root[ROW] in range(self.size):
focused = [root[ROW] + 1, root[COL]]
while self.isInBoard(focused[ROW]):
# in case root == EMPTY
直下の while で focused を回していきますよ。とりあえず focused の位置が盤面をはみでたら board のインデックスがエラーを鳴らしてきますので、3-3 で実装した isInBoard でループ続行を判断します。
これ focused[ROW] しかみてないけど、
column はインデックスはみ出さないかみなくてええの?
~~~~~~~~~~~~~~~
while ループの中ではとりあえず root と focused の数を比較して
0 == root == focused
0 == root != focused
0 != root == focused
0 != root != focused == 0
0 != root != focused != 0
の場合わけをします。
まずは root == 0 != focused の場合ですね。
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
数を動かしているので ever_moved は True にしてください。また focused を EMPTY にすることをお忘れなく。
続いて root == focused != 0 の場合です。
# in case board[root] == board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
やはりこの場合も ever_moved は True ですね。そしてフローチャートの通りに数を移動させたら root を移動させますので、focused を回す while ループを break してください。
ここまでで focused != 0 なのに取り扱っていないものは root != focused ですから、こちらも break してあげましょう。
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
ここまできたら focused を動かします。盤面を上に動かすとき focused は row を一つ下に持っていくので、こうですね。
focused[ROW] += 1
~~~~~~~~~~~~~~~
同じように下・左右とやっていきましょう。先に下に盤面を動かす場合を見ていきます。column はどうでもいいのですが、row は下から上がるように見ていかないといけません。
したがって root[ROW] に関する for ループの range の引数が変わります。
実際にコードをみてみましょう。UP の場合と変更があるところを太字にしてあります。
elif direction == DOWN:
logger.debug('direction is DOWN')
root = [0, 0]
for root[COL] in range(self.size):
for root[ROW] in range(self.size - 1, -1, -1):
focused = [root[ROW] - 1, root[COL]]
while self.isInBoard(focused[ROW]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root] == board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[ROW] -= 1
row を回す for 文では range(self.size - 1, -1, -1)
とする必要があります。インデックスが 0 から self.size - 1 の範囲であることに注意してください。
focused を動かすときは row を下から上にあげるので -1 してください。
これ以外はコピペで構いません。
~~~~~~~~~~~~~~~
左に動かす場合、右に動かす場合もそれぞれ for 文の挙動には気をつけなければなりませんね。左へ動かすときは root と focused はこうします。
つまり先ほどまでの row の代わりに focused の column を動かさなければいけないんです。それも column の値が増える方向にですよ。右に行くほど column の値が増えるように print メソッドで実装しているんですから。忘れた方は 3-4 でもう一度確認してください。
ではコードをみてみましょう。UP と比較して変わったところを太字にします。
elif direction == LEFT:
logger.debug('direction is LEFT')
root = [0, 0]
for root[ROW] in range(self.size):
for root[COL] in range(self.size):
focused = [root[ROW], root[COL]+1]
while self.isInBoard(focused[COL]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root] == board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[COL] += 1
~~~~~~~~~~~~~~~
右に動かす時の root と focused の動きも欲しいんですか?仕方ないですね。
ツンデレかよめんどくせぇ
これに基づいてコーディングするとこうなりますね。
elif direction == RIGHT:
logger.debug('direction is RIGHT')
root = [0, 0]
for root[ROW] in range(self.size):
for root[COL] in range(self.size - 1, -1, -1):
focused = [root[ROW], root[COL] - 1]
while self.isInBoard(focused[COL]):
# in case root == EMPTY
if self.board[root[ROW]][root[COL]] == EMPTY != self.board[focused[ROW]][focused[COL]]:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] = self.board[focused[ROW]][focused[COL]]
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
# in case board[root]==board[focused] (!= 0)
elif self.board[root[ROW]][root[COL]] == self.board[focused[ROW]][focused[COL]] != 0:
logger.info('{}, {} <- {}'.format(root[ROW], root[COL], self.board[focused[ROW]][focused[COL]]))
self.board[root[ROW]][root[COL]] *= 2
self.board[focused[ROW]][focused[COL]] = EMPTY
ever_moved = True
break
elif self.board[focused[ROW]][focused[COL]] != EMPTY:
break
focused[COL] -= 1
~~~~~~~~~~~~~~~
上下左右以外の値が direction に与えられている場合はどうやって動かせばいいかわかりませんから「失敗」FAILED をリターンします。
else:
logger.error('INPUT DIRECTION is INVALID VALUE')
return FAILED
他のすべての場合でもリターンしましょう。ここで、ever_moved を宣言した理由を思い出してください。動かせる数字がどこにもないかもしれなかったんですよね。ですから一つでも数字が動いていれば True を、数字が一切動いていなければ「失敗」False をリターンします。
if ever_moved == True:
return SUCCEEDED
else:
# if no number was moved, it is game over
logger.debug('ever_moved == False')
return FAILED
失敗したときはログで「ever_moved が False だから」と書いてあげるのがいいでしょう。
~~~~~~~~~~~~~~~
さて、ここまでで一通りコーディングはおわりました。バグがないか一応チェックしておきましょう。クラスの外、if __name__ == ...
というところがありますよね。
if __name__ == "__main__":
test_board = Move()
test_board.print()
test_board.board_move(UP)
test_board.insert()
test_board.print()
test_board.board_move(DOWN)
test_board.insert()
test_board.print()
test_board.board_move(LEFT)
test_board.insert()
test_board.print()
test_board.board_move(RIGHT)
test_board.insert()
test_board.print()
ここでは実際のゲームと同じように
- 動かす
- 入れる
- 見せる
のスリーステップをすべての方向で実装しています。このようにして動かしてみて、
あれ?なんかちげーな
となったら今すぐにデバッグへ直行してください。
今回のコード、コピペ部分があまりにも多いですよね。これ、コードとしてはあまりよろしくないので、上下左右 4 方向を全て一つにまとめようと思います。そのため、ここでのデバッグは特に神経注いでください。
え〜、めんどくさ
こんなに具体的なコードでバグが発生するようだと、次に抽象化したら一気に
どこバグってんだ??
と訳わからんくなりますよ。