Pythonプログラミングで

2048を作る

Stage 4 盤面を動かす

4-2 4 通りの場合わけをして盤面を動かす

さて、ここからは実際のコーディングに入ります。4-1 で考えたフローチャートと rootfocused の移動にのっとって、上下左右全ての方向についてそれぞれ場合わけし、盤面の挙動をコーディングします。

ただ、今回はモノホンのコードがかなり複雑なので解説用コードを別に作りました。まずは解説用コードでイメージを掴んだあと、コーディング本番に入ります。

~~~~~~~~~~~~~~~

それでは 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 とかが入れられると、その時点でエラーを出します。

別の引数 logger はメソッドの中で使うロガーです。

                        
    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 になるな

ということで、引数が入れられなかった場合は self.logger をメソッドロガーに使おうということになります。

~~~~~~~~~~~~~~~

次の ever_moved という変数は「数字が動いたか」を bool 型で記録しておくものです。

                        
    def board_move(self, direction: list, *, logger=None):
        logger = logger or self.logger
        ever_moved = False
                        
                    

例えばこんな場合を考えてみましょう。

UNAVAILABLE
from https://glebbahmutov.com/2048/

この状態から盤面を上に動かそうと思っても、全ての数字が上に上がれませんから無理ですね。こんなとき「盤面を動かせなかった」ということで FAILED をリターンしたいんですよ。

そこでこの ever_moved を使うわけです。どこかしら数字が動いたらこの値を True に入れ替えて、ever_moved が True なら最後に「動かせる」ということで SUCCEEDED をリターンする、False なら FAILED をリターンする、そんな形を考えています。

~~~~~~~~~~~~~~~

ここからは上下左右各方向についてどういう動きをさせるかみていきましょう。

1-2 で考えた rootfocused という二つのマスの位置関係を思い出してみてください。

focused の数字を root に移すってやつやろ

そうですね。まずは上に盤面を動かす場合を考えます。direction == UP の場合です。

                        
        # up
        if direction == UP:
            logger.debug('direction is UP')
            root = [0, 0]
            ...
                        
                    

rootfocused の取り方はこうするんでした。

これに沿うように rootfocused の位置を変更していきます。

リストの 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 を調節する必要がありますね。rowcol で分けた 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 はインデックスはみ出さないかみなくてええの?

focusedroot の真下にありますから、columnroot を変えるまで変えません。

~~~~~~~~~~~~~~~

while ループの中ではとりあえず rootfocused の数を比較して

  • 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 を動かします。盤面を上に動かすとき focusedrow を一つ下に持っていくので、こうですね。

                        
                        focused[ROW] += 1
                        
                    

~~~~~~~~~~~~~~~

同じように下・左右とやっていきましょう。先に下に盤面を動かす場合を見ていきます。column はどうでもいいのですが、row は下から上がるように見ていかないといけません。

UNAVAILABLE

したがって 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 文の挙動には気をつけなければなりませんね。左へ動かすときは rootfocused はこうします。

UNAVAILABLE

つまり先ほどまでの row の代わりに focusedcolumn を動かさなければいけないんです。それも 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
                        
                    

~~~~~~~~~~~~~~~

右に動かす時の rootfocused の動きも欲しいんですか?仕方ないですね。

UNAVAILABLE

ツンデレかよめんどくせぇ

これに基づいてコーディングするとこうなりますね。

                        
        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 方向を全て一つにまとめようと思います。そのため、ここでのデバッグは特に神経注いでください。

え〜、めんどくさ

こんなに具体的なコードでバグが発生するようだと、次に抽象化したら一気に

どこバグってんだ??

と訳わからんくなりますよ。

NEXT 4-3 場合わけを統合したメソッドを実装する