LoginSignup
10
7

More than 1 year has passed since last update.

Pythonで書いた45行のリバーシプログラム

Posted at

はじめに

どうも、y-tetsuです。

本記事は、オセロプログラム ~7行のC言語で書くコンピュータ対局~に衝撃を受けた筆者が、これをPythonで書き直してみる!!というものです。

上記サイトのC言語のコードはたった7行×79文字なのですが、コンソール上でAIとリバーシ(オセロ)の対局ができる、という一品です。有難いことに、整形したコードも載せていただいています。

ぎっしりと趣向が凝らされたコードを紐解いてみたい、Python文法にもっと馴染んでおきたい、という個人的な思いが相まって、今回このような取り組みをしてみました。

コード行数の少なさだけを目指すなら、リスト内包表記を使って1行で書くようなトリッキーな方法も考えられます。ですが、今回はあくまで元のコードの意図を優先し、かつPythonの基本的(Pythonic)な書き方にもなるべく沿ったものとしたい、という思いがありました。

そこで、今回は以下の条件でコードを書くことにしました。

  • Python 3.9以上
  • ライブラリは未使用
  • flake8に準拠

flake8とはPythonのコードチェッカーツールです。pep8の規約に違反しているなど、Pythonコードの問題点を示してくれます。今回はflake8で問題点が検出されない範囲でコードを書きました。

できたもの

という事で、ささっと今回作ったコードをお見せしておきます。

class ShortReversi:
    def __init__(self):
        self.dirs = [-10, -9, -8, -1, 1, 8, 9, 10]
        self.discs = ' - o x\n'
        self.board = [0 if i % 9 else 3 for i in range(91)]
        self.board[40] = self.board[50] = self.turn = 1
        self.board[41] = self.board[49] = 2

    def _check(self, board, move, flip=False):
        if not (ret := False) and not board[move]:
            for i in range(8):
                count = value = move + self.dirs[i]
                while board[value] == 3 - self.turn:
                    value += self.dirs[i]
                if count != value and board[value] == self.turn:
                    ret = value = move
                    while flip:
                        board[value] = self.turn
                        value += self.dirs[i]
                        if board[value] == self.turn:
                            break
        return ret

    def play(self, com1=False, com2=True):
        end = False
        while not (move := 0):
            for i in range(9, 82):
                if self._check(self.board, i, flip=False) and not move:
                    move = i
                print(self.discs[self.board[i]*2:][:2], end='')
            while move and not (end := False):
                if not com1 and self.turn == 1 or not com2 and self.turn == 2:
                    x, y = [int(i) for i in input().split()]
                    move = x + y * 9
                if self._check(self.board, move, flip=True):
                    break
            else:
                if end:
                    break
                end, _ = True, print('pass')
            self.turn = 3 - self.turn


if __name__ == '__main__':
    ShortReversi().play()

元のコードに用意されていた、maincheck関数の構成はそのままに、盤面などの初期化を加えた、クラスを使った書き方に直しています。

なお、mainplayに名称変更しました。その他も変数名やロジックなど微妙に書き換えてはいますが、概ね元のコードを再現したつもりです。

また、おまけでplayメソッドの引数に応じて、対戦AIを変えられるよう改造もしてみました。

動かし方

以下を実行するとゲームが始まります。

実行コマンド

> python short_reversi.py

※あらかじめPython 3.9以上をインストールしておいてください。

遊び方

先手が人、後手がAIとなっています。(デフォルト時)

自分の手番の時に、以下のとおりコマンドプロンプトにて、打ちたい場所をXY座標で指定してください。(例では"x=5 y=3"として"5 3"を入力、Y方向は上から下です)
input1.png

そうすると、座標の場所に自分のディスク(石)が置かれて、次にAIが手を打ちます。
input2.png

再び自分の手番となり、ゲームが決着するまでこれを繰り返します。

コード説明

プログラム全体はShortReversiクラスとしています。以下で実行しています。

if __name__ == '__main__':
    ShortReversi().play()

__init__

初期化処理は以下の通りです。クラスをインスタンス化した際に、リバーシを始める準備をします。

    def __init__(self):
        self.dirs = [-10, -9, -8, -1, 1, 8, 9, 10]
        self.discs = ' - o x\n'
        self.board = [0 if i % 9 else 3 for i in range(91)]
        self.board[40] = self.board[50] = self.turn = 1
        self.board[41] = self.board[49] = 2

ディスクの表現

ディスクはself.discsの文字列として保持しています。

記号 意味
- 空きマス
o 先手のディスク
x 後手のディスク

盤面の表現

盤面は91マスの1次元のリストです。格納された値が表現するものは、以下の通りです。

意味
0 空きマス
1 先手のディスク
2 後手のディスク
3

2次元のイメージに当てはめると、9列×10行+1の盤面となっていて、初期化後の値は下図のようになります。

board.png

緑色のエリアはディスクが置ける部分で、灰色は探索を打ち切るための"壁"となっています。白色のエリアは空きマスとしていますが、実際にはディスクが置かれる事はなく、こちらも"壁"となっています。細かい部分で灰色の壁と白色の壁では、盤面表示の際に改行出力に対応している、という違いがあります。

当初は、参考にしたコードと同じく9*10=90マスのリストで作ろうとしたのですが、うまくいきませんでした。

ディスクが置けるエリアの一番右下隅に置こうとすると、右斜め下方向への探索の際、リストの領域外アクセスとなりPythonに怒られてしまいました…。そのため、1マス足して91マスに改変しています。

方向の表現

self.dirsにて盤面の上下左右斜め8方向の1マス進んだ時の移動量を表しています。

self.dirs = [-10, -9, -8, -1, 1, 8, 9, 10]

directions.png

手番の表現

self.dirsにて現在の手番を表します。盤面の表現と同じく、1が先手で2が後手を表します。初期値を1の先手としています。

self.board[40] = self.board[50] = self.turn = 1

_check

_checkメソッドは手が打てるかどうかの判定と、ディスクをひっくり返す処理の二役を担っています。

    def _check(self, board, move, flip=False):
        if not (ret := False) and not board[move]:
            for i in range(8):
                count = value = move + self.dirs[i]
                while board[value] == 3 - self.turn:
                    value += self.dirs[i]
                if count != value and board[value] == self.turn:
                    ret = value = move
                    while flip:
                        board[value] = self.turn
                        value += self.dirs[i]
                        if board[value] == self.turn:
                            break
        return ret

引数に盤面(board)と手(move)、ひっくり返すかどうか(flip)を指定します。

戻り値(ret)は手が置ける場合にTrueそうでない場合にFalseを返します。

まず指定した場所が空きマスかどうかを判定し、空いていれば8方向をループで順に探索していきます。

        if not (ret := False) and not board[move]:
            for i in range(8):

セイウチ演算子(:=)の部分は、戻り値の初期化(Falseを設定)を、空きマス判定と同時に行うための条件式です。(ここは行数を稼ぐために、やや無理した?感じに見えるかもしれません…)

            for i in range(8):
                count = value = move + self.dirs[i]
                while board[value] == 3 - self.turn:
                    value += self.dirs[i]
                if count != value and board[value] == self.turn:
                    ret = value = move

各方向の探索時は、以下の条件を満たす場合に手が置けると判定します。

  • 相手のディスクが連続している
  • 相手ディスクの連続の最後は、自分のディスクとなっている
                    while flip:
                        board[value] = self.turn
                        value += self.dirs[i]
                        if board[value] == self.turn:
                            break

flip引数にTrueが指定されていれば、ついでにディスクをひっくり返します。

play

playメソッドはゲームのメインループ処理を行います。

    def play(self, com1=False, com2=True):
        end = False
        while not (move := 0):
            for i in range(9, 82):
                if self._check(self.board, i, flip=False) and not move:
                    move = i
                print(self.discs[self.board[i]*2:][:2], end='')
            while move and not (end := False):
                if not com1 and self.turn == 1 or not com2 and self.turn == 2:
                    x, y = [int(i) for i in input().split()]
                    move = x + y * 9
                if self._check(self.board, move, flip=True):
                    break
            else:
                if end:
                    break
                end, _ = True, print('pass')
            self.turn = 3 - self.turn

end変数にてゲーム終了の判定を行います。手を打つたびにFalseとしていますが、一度パスが発生するとTrueを設定します。そして次の手番もパスの場合は、breakで最初のwhileループを抜けてゲーム終了となります。

以下の、メインループの記載はmoveを0で初期化しつつ無限ループさせることを意図して書いています。

        while not (move := 0):
            for i in range(9, 82):
                if self._check(self.board, i, flip=False) and not move:
                    move = i
                print(self.discs[self.board[i]*2:][:2], end='')

最初のfor文は盤面全体を探索し、打てる場所(move)の取得(ただし、最初に見つかった場所のみ)と、盤面のコンソールへの表示を行っています。

                print(self.discs[self.board[i]*2:][:2], end='')

上記のコンソールの表示のロジックは、self.discsの文字列に配列スライスを2回適用して盤面に対応する文字を出力しています。

一回目のスライス[self.board[i]*2:]で、盤面の値の位置から最後までを取ります。次に、二回目のスライス[:2]で先頭から2文字分に整形しています。

盤面の状態に対する、コンソールへの標準出力の対応は以下となります。

盤面の値 出力
0 " -"
1 " o"
2 " x"
3 "\n"

なお、上記のとおり改行の出力も盤面の値で管理しているため、print文で改行されないようend=''を指定しています。

打てる手が見つかった場合は、その手をAIが打つか、人が指定した手を打つかで、処理を切り替えています。

                if not com1 and self.turn == 1 or not com2 and self.turn == 2:
                    x, y = [int(i) for i in input().split()]
                    move = x + y * 9

人が手を打つ場合は、input関数で標準入力からXY座標の2つの値を取得し、int型に直します。そしてその値を1次元のリストのインデックスに変換し、打つべき手としています。(人が入力した手が、ディスクを返せない場所の時は、再度入力を求める挙動となります)

手を打った後は、手番を交代して次のループへ移ります。

            self.turn = 3 - self.turn

上記により、先手(1)の場合は後手(2)に、後手(2)の場合は先手(1)に切り替わります。

playメソッドの引数

playメソッドの引数を変更すると、AIまたは人のプレイヤーを自由に変更できます。

引数 用途
com1 True ⇒ 先手はAI、False ⇒ 先手は人
com2 True ⇒ 後手はAI、False ⇒ 後手は人

例えば、AI同士対戦させる場合は、プログラムを以下に変更して下さい。

if __name__ == '__main__':
    ShortReversi().play(True, True)

以上、作ったコードの説明でした。ここまで長々と読んでいただき、誠にありがとうございました。

おわりに

元の7行のコードには遠く及びませんでしたが、無理に処理を1行に詰め過ぎないようなるべく気を付け、ようやく45行に収まったという感じでした。

セイウチ演算子(:=)を使って、変数の初期化をしつつループを回す部分は、あまりやらないだろうなと思いつつもコードを短くしたくて、やや罪悪感を感じていたりします…。他の箇所についても「こっちの方がPythonicだよ」といったところがあれば、ぜひ教えていただけると大変嬉しく思います。

今回の取り組みで、元のC言語のコードをPythonで同じように書こうとするとどうなるのか?と考えていると、パズルを解いているような感覚で、想像以上に夢中になれました。他にも、標準ライブラリやnumpyなどを活用するとどうなるか?と考えるのも面白そうです。

機会があれば、Pythonで7*79文字に収まるリバーシプログラムにもチャレンジしてみたいと思います。(できるかどうかもわかりませんが)

今回の記事で紹介させていただいたコードは、以下でも公開しています。もし良かったら見てみて下さい。

それでは皆様、よきリバーシ・ライフを!

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7