pythonでオセロを作ってみた

# オセロは初心者の壁
pythonの基礎をなんとなく理解できてきました（ライブラリはnumpyやpandasを少し使えるくらい）。自分でアプリを一から作ったほうが成長できると聞いたので、ここは新規に何か作ってみたいなと思いました。
そこで、最も初心者向けであろうオセロ（リバーシ）を作ろうと思いました。全てのゲームの基礎になるとかならないとか。挟んでひっくり返すという非常にシンプルなゲームゆえにとっつきやすいはず...
しかし、いざ作ってみると初心者はオセロすらまともに作るのに四苦八苦しました。

追記）2019/4/26
端っこの方でうまく作動しなかったのを修正しました。
AIでの作動を考慮し、石を置けない場合自動的にパスするシステムを組み込みました。
それに伴い新しく関数を追加しました
出力レイアウトを少し変えました。

追記）2019/4/27
連続で並んでいる石を取ることができなかったバグを修正しました。
分かりやすいように入力を0~7ではなく1~8にしました。

##注意
ネットに転がってるものを利用して作ってみたものです。似たようなコードは調べれば出てきます。
初心者が作っているので、かなり粗があると思います。
実行環境はgoogleドライブのcolaboratoryです。jupyterと似たような感じ。
ローカルで実行する際は２つのコードを１つにまとめるか、classの部分をimportすれば動く、のか？
ちょっと詳しくは分からないです。

# オセロを作るために
作ろうと思ってすぐ作れるようなスキルはないため、以下の手順で考えることにしました。

1. アプリを作るためにオセロがどのような手順で進められるかを確認する。
2. とりあえず、座標を入力したら石をおいてひっくり返してくれるプログラムを組んでみる。
3. きちんとオセロらしく作動するように調整する。

それぞれどんなことをしたのか、記述していこうと思います。
## 1. アプリを作るのにどのような要素があるのか
まず、オセロはどういう感じで動くのか考えてみました。

イメージとしては

１ターンの挙動 : 石を置きたい座標を指定 → 石を置く → 挟んだ石をひっくり返す

これを繰り返す感じ。

特殊な要素として、
・パス
・石がおけないためゲーム終了
の２つが挙げられます。
これを踏まえてプログラムを組んでみます。

## 2. 石を置いてひっくり返すコードを組んでみる

下記のコードはまず
入力座標に石を置けるか : ３つの条件
　　1. 入力座標が盤内であるかどうか　→　①
　　2. 入力座標に石が置かれていないか  →　②
　　3. 入力座標に石をおいた時にひっくり返せる石が１つでもあるかどうか  → ③　
の３つを考えます。
曲者は３番のひっくり返せるかどうかの判断
今回はnumpyの２次元配列を用いてます。
八方向を表すために、x方向、y方向に対して(-1,0,1)の３つをdx, dyでとりました。
ex)右上であれば方向ベクトルは(dx, dy) = (1, 1)
　 下方向であれば(dx, dy) = (0, -1)

座標(x,y)に対して(dx,dy)方向を調べます。
まず(dx, dy) = (0, 0)はいらないので除外。　　→　⑴
(x+dx, y+dy)が自分の石、もしくは空白であれば石はひっくり返せないのでFalseを返します。　→ ⑵
(x+dx, y+dy)に相手の石があればその方向をさらに調べます。  → ⑶
調べた先に自分の石があればTrue、何もなければFalseを返します。  → ⑷-True,⑷-False
これで1,2,3全てを満たしていれば石をおけることがわかりました。

あとは条件を満たした座標に石を置いてひっくり返します。  →　④

これらの全てを組み合わせれば挟んだ石をひっくり返すコードの完成です。
＊ターンチェンジはまだ使ってないけれど一応書いてあるだけです。

追記）2019/4/26
話が被りますが、盤面のどこかに石を置けるか、それともどこにも置けないかを出力する関数(❶)を追加しました。
これは次のclassで使うのでそちらに移した方が良かったかもしれません。
詳しくは下へ。

初期設定として
白 : white = 1
黒 : black = -1
空白 : blank = 0
盤面の大きさ : tablesize = 8

```python:osero.py
import numpy as np

white = 1
black = -1
blank = 0
tablesize = 8

class Board(object):
    # 初期設定
    def __init__(self):
        self.cell = np.zeros((tablesize,tablesize))
        self.cell = self.cell.astype(int)
        self.cell[3][3] = self.cell[4][4] = 1
        self.cell[3][4] = self.cell[4][3] = -1
        self.current = black
        self.pass_count = 0
        self.turn = 1
        
    def turnchange(self):  #  ＊
        self.current*= -1
    def stonenumber(self):
        return self.stones
    
    def rangecheck(self,x,y):  # ①　盤内かどうか　
        if x == None: x = -1
        if y == None: y = -1
        if x < 0 or tablesize <=x  or y < 0 or tablesize <= y:
            return False
        return True
    
    def check_can_reverse(self,x,y):  # 置けるかどうか
        if not self.rangecheck(x,y):   # →　①へ
            return False
        elif not self.cell[x][y] == blank:   #  ②
            return False
        elif not self.can_reverse_stone(x,y):   # →　③へ
            return False
        else: return True
    
    def can_reverse_one(self,x,y,dx,dy):  # ⑶、⑷　　(dx,dy)方向に敵石があり、その先に自石があるかどうか

        if not self.rangecheck(x+dx,y+dy):
            return False
            length = 0
            if not self.cell[x + dx][y + dy] == -self.current: #  ⑶
                return False # (dx,dy)方向が敵石じゃない時False
            else:    
                while self.cell[x+dx][y+dy] == -self.current: 
                    x +=dx
                    y +=dy
                    length += 1
                    if self.cell[x+dx][y+dy] == self.current:  # ⑷-True
                        return length
                    elif not self.cell[x+dx][y+dy] == -self.current:
                        continue
                    else: return False
                else: return False  # ⑷-False
                
    def can_reverse_stone(self,x,y):  # 　③入力座標ではひっくり返せる石はあるか
        for dx in range(-1,2):
            for dy in range(-1,2):
                if dx == dy == 0: continue   # ⑴
                elif not self.rangecheck(x+dx,y+dy): continue  # ①（調べた範囲が番外だったらエラーが起こるため）
                elif not self.can_reverse_one(x,y,dx,dy):  # →　⑶、⑷の処理へ
                    continue
                else: return True
       
    def reverse_stone(self,x,y): #  ④ 座標に石を置いて石をひっくり返す
            for dx in (-1,0,1):
                for dy in (-1,0,1):
                    length = self.can_reverse_one(x,y,dx,dy)
                    if length == None: length = 0
                    if length > 0:
                        for l in range(length):
                            k = l+1
                            self.cell[x + dx*k][y + dy*k] *= -1
            
    def display(self):  # 盤面の状況を表示
        print('==='*10)   #  *(下の文を参照）
        for y in range(tablesize):
            for x in range(tablesize):
                if self.cell[x][y] == white:
                    print('W', end = '  ')
                elif self.cell[x][y] == black:
                    print('B', end = '  ')
                else:
                    print('*', end = '  ')
            print('\n', end = '')
            
    def put_stone(self,x,y):  # 一回のターン内の行動 
        if self.check_can_reverse(x,y):   # 入力座標に石を置ける
            self.pass_count = 0
            self.cell[x][y] = self.current
            self.reverse_stone(x,y)
            self.turnchange()
            return True
        else:  # 入力座標に石を置けない
            return False
        
    def check_put_place(self):  # ❶盤面上に石が置ける場所があるか　次のクラスの時に使用
        for i in range(tablesize):
            for j in range(tablesize):
                if self.check_can_reverse(i,j): # (i,j)座標に置いて石が置けたら成立
                    return True
                else:continue
        return False

if __name__ == '__main__':
    board = Board()
    board.display()
    board.put_stone(3,2)
    board.display()
    board.put_stone(2,2)
    board.display()
```
実行すると以下のようになりました。

```python
----------------------------------------
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  W  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
----------------------------------------
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  B  *  *  *  *  
*  *  *  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
----------------------------------------
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  W  B  *  *  *  *  
*  *  *  W  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
```
うん、いい感じ。

追記）2019/4/26
レイアウトを少し変更したので区切る横のラインは---　→　===のように変わります。


## 3. 今度はゲームが終わるまで動かすようにする

今度は１ターンの挙動をもう少し細かく作ります。
必要なのは
　1. 座標を入力するシステム  → ①
　2. 石を置き、挟んだ石をひっくり返す  ← 上で作ったやつ
　3. パスするシステム     → ②
　4. ゲームセット（スコア付き）  → ③
　5. 入力した座標に石をおけない場合再入力する  → ④

2の部分はパスすると相手のターンに移行するのと、二回連続でパスしたらゲーム終了、という設定にしました。  →　②' + 上記の＊部分
また、ゲームが終わった時に勝敗を表示するために、両石の数、空白の数をカウントするシステムも作成しました。  → ⑤
また、あまり意味はないのですが、初期設定として各石と空白の数を設けました。
これらが書けたらあとは組み合わせるだけ。

追記）2019/4/26
置くところがない時に自動的にパスを行う関数（→❶）を導入しました。
出力のレイアウトを少しだけ変更しました。
面倒なので座標が欲しければご自分で。
→2019/4/27 入力を0~7から1~8にしておきました。少しは分かりやすくなると思います。

```python:playgame.py
import sys

class Game(Board):
    def __init__(self):
        self.white_count = 2
        self.black_scount = 2
        self.blank_count = 60
        
    # パスをする関数。一度パスするとpass_countが１増える
    # pass_count　== 2になるとゲームセット関数に飛ぶ
    def pass_system(self):  #  ②
        board.pass_count += 1
        board.turnchange()
        if board.pass_count ==2:  # 連続二回パスしたので③のゲームを終わらせる関数へ移動
            self.gameset()
        return True
    
    def gameset(self): #  ③ ゲーム終了、石の数をカウントし勝敗を表示
        print('game set')
        self.count_system()
        print('white : ', self.white_count)
        print('black : ', self.black_count)
        if self.white_count > self.black_count:
            print('white WIN !!')
        if self.white_count < self.black_count:
            print('Black WIN !!')
        if self.white_count == self.black_count:
            print('Draw')
        sys.exit()
            
        
    def count_system(self):  #  ⑤ 石のカウントシステム
        self.white_count = np.sum(board.cell == white)
        self.black_count = np.sum(board.cell == black)
        self.blank_count = np.sum(board.cell == blank)
            
    def input_point(self):    #  ① 座標を入力
        print('石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。')
        x = input('x>>')
        y = input('y>>')
        try:
            x = int(x)-1
            y = int(y)-1
        except:
            self.input_point()
        return x, y
            
        
    def one_turn_play(self):  # ①〜⑤と❶をまとめる（❶に関しては上に記述）　
        if board.check_put_place():  #  ❶ 盤面に石が置ける場所があるかどうか
            (x,y) = self.input_point()   #  ① 座標を入力
            board.put_stone(x,y)  # Boardクラスで作ったやつ。石をおいてひっ繰り返してTrueを返すか、何もせずFalseを返す
            if not board.put_stone(x,y):
                    if (x,y) == (8,8):       #  ② パスするとき
                        self.pass_system()
                    elif (x,y) == (-1,-1):   #  ③ ゲームをやめる時
                        self.gameset()
                    #石をおけない時は もう一度同じことをする
                    while False:
                        self.one_turn_play()     
        else:self.pass_system()
            
        
    # 最後まで続くようにしてみる   
    def gameplay(self):
        while self.blank_count >0:
            board.display()            #　盤面を出力
            print('-----'*10)
            self.turn += 1
            print('turn : ', self.turn, end = '  ')
            if board.current == -1:
                print(', turn black')
            elif board.current == 1:
                print(', turn white')
            self.one_turn_play()   # ターンでの行動
            self.count_system()   # 石とblankの数を出す
            print('white : ', self.white_count, ', black : ', self.black_count, ', blank : ', self.blank_count)
            print('pass_count : ',board.pass_count)
        self.gameset()
        
if __name__ == '__main__':
    board = Board()
    game = Game()
    game.gameplay()
```

実行してみる

```
==============================
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  W  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  1  , turn black
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>3
y>>2
white :  1 , black :  4 , blank :  59
pass_count :  0
==============================
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  B  *  *  *  *  
*  *  *  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  2  , turn white
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>2
y>>2
white :  3 , black :  3 , blank :  58
pass_count :  0
==============================
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  W  B  *  *  *  *  
*  *  *  W  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  3  , turn black
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>2
y>>3
white :  2 , black :  5 , blank :  57
pass_count :  0
==============================
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  W  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  4  , turn white
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>8
y>>8
white :  2 , black :  5 , blank :  57
pass_count :  1
==============================
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  W  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  5  , turn black
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>2
y>>1
white :  1 , black :  7 , blank :  56
pass_count :  0
==============================
*  *  *  *  *  *  *  *  
*  *  B  *  *  *  *  *  
*  *  B  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  6  , turn white
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>8
y>>8
white :  1 , black :  7 , blank :  56
pass_count :  1
==============================
*  *  *  *  *  *  *  *  
*  *  B  *  *  *  *  *  
*  *  B  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  W  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  7  , turn black
石を置く座標を(1~8で)入力してください。(x,y)=(9,9)でpass、(0,0)で終了します。
x>>5
y>>4
white :  0 , black :  9 , blank :  55
pass_count :  0
==============================
*  *  *  *  *  *  *  *  
*  *  B  *  *  *  *  *  
*  *  B  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  B  B  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  8  , turn white
white :  0 , black :  9 , blank :  55
pass_count :  1
==============================
*  *  *  *  *  *  *  *  
*  *  B  *  *  *  *  *  
*  *  B  B  *  *  *  *  
*  *  B  B  B  *  *  *  
*  *  *  B  B  B  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
*  *  *  *  *  *  *  *  
--------------------------------------------------
turn :  9  , turn black
game set
white :  0
black :  9
Black WIN !!
An exception has occurred, use %tb to see the full traceback.
```

これでオセロを遊べそうですね(ぼっち(セルフ)専用)。
blank = 0 になるまでは検証していませんので悪しからず。

#　感想と今後の展望
個人的には難しかったけど、初心者としては成長したと感じました。
正直これを作る前はclassやtry, exceptなどはいまいち分からなかったのですが、多少は使えるようになったかなと...
今後はAIを導入してぼっちでも対戦できるようにしたいと思っています。
また、オセロから発展させてチェスや将棋なども作って行きたいと思っています。

プログラムが動かないなど何か問題があれば教えてください。
以上です。参考になれば幸いです。