はじめに
どうも、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()
元のコードに用意されていた、mainとcheck関数の構成はそのままに、盤面などの初期化を加えた、クラスを使った書き方に直しています。
なお、mainはplayに名称変更しました。その他も変数名やロジックなど微妙に書き換えてはいますが、概ね元のコードを再現したつもりです。
また、おまけでplayメソッドの引数に応じて、対戦AIを変えられるよう改造もしてみました。
動かし方
以下を実行するとゲームが始まります。
実行コマンド
> python short_reversi.py
※あらかじめPython 3.9以上をインストールしておいてください。
遊び方
先手が人、後手がAIとなっています。(デフォルト時)
自分の手番の時に、以下のとおりコマンドプロンプトにて、打ちたい場所をXY座標で指定してください。(例では"x=5 y=3"として"5 3"を入力、Y方向は上から下です)

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

再び自分の手番となり、ゲームが決着するまでこれを繰り返します。
コード説明
プログラム全体は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の盤面となっていて、初期化後の値は下図のようになります。
緑色のエリアはディスクが置ける部分で、灰色は探索を打ち切るための"壁"となっています。白色のエリアは空きマスとしていますが、実際にはディスクが置かれる事はなく、こちらも"壁"となっています。細かい部分で灰色の壁と白色の壁では、盤面表示の際に改行出力に対応している、という違いがあります。
当初は、参考にしたコードと同じく9*10=90マスのリストで作ろうとしたのですが、うまくいきませんでした。
ディスクが置けるエリアの一番右下隅に置こうとすると、右斜め下方向への探索の際、リストの領域外アクセスとなりPythonに怒られてしまいました…。そのため、1マス足して91マスに改変しています。
方向の表現
self.dirsにて盤面の上下左右斜め8方向の1マス進んだ時の移動量を表しています。
self.dirs = [-10, -9, -8, -1, 1, 8, 9, 10]
手番の表現
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文字に収まるリバーシプログラムにもチャレンジしてみたいと思います。(できるかどうかもわかりませんが)
今回の記事で紹介させていただいたコードは、以下でも公開しています。もし良かったら見てみて下さい。
それでは皆様、よきリバーシ・ライフを!

