【はじめてのPython】オセロを作る⑥~ついに完成!~

ITスキル

こんにちは。ヒトツメです。
前回までで概ねオセロは動くようになってきました。あとは石が置けない場合の処理と、終局の判定ができれば、オセロは完成です。
また、折角なので使いやすく、UIも少しだけ工夫してみたいと思います。

【はじめてのPython】オセロを作る⑤~GUIベースで動かす~
こんにちは。ヒトツメです。少し間が空いてしまいましたが、前回に引き続き、オセロのGUIを作っていきます。今回は、GUIを使ってオセロを操作するところを実装していきたいと思います。 クリック操作 前回、tkinterを使って、初期配置の状態の
新しいタブで開く)
スポンサーリンク

置ける場所の判定

前回までで、オセロはほぼ動くようになりました。ただ、手番は石を置くごとに白と黒が交互に変更になるようにしか、設定していませんでした。石を置けない場合、手番が再度相手に回ってしまいますが、その場合における処理がされていません。
そこで、石を置けない場合に再度手番を変更する、という処理をする必要があります。

そこで、8×8のマスの中で、どの場所なら石を置けるかを判定する、という操作をしなければなりません。
実際にどの場所なら石を置けるかの判定にあたっては、次のような関数を置くことで、処理をすることとします。

def placable(game, Player):
    able = numpy.zeros((8, 8))
    for rowIter in range(8):
        for clmIter in range(8):
            nextstatus = game.copy()
            nextgame(nextstatus, rowIter, clmIter, Player)
            if nextstatus[rowIter][clmIter] != game[rowIter][clmIter]:
                able[rowIter][clmIter] = Player
    return able

gameの状態をPlayer(白か黒か)を入れることで、8×8の中で、Playerが置ける場所についてPlayerと同じ数値が返却されます。
処理の内容としては、そこまで難しい話ではなく、ableという返却用のarrayを作成し、マスの一つ一つに実際に石を置いてみたときに局面に変化が起きているか、ということを判断して、置ける場合にPlayerと同じ数字を設定しているだけです。

実際これが動いているかを確認すると、次のように、きちんと動いていることが確認できます。

このようにして返却されるableというarrayは、仮に石を置けない場合、その合計は0.0になります。なので、「numpy.sum(able)」が0.0であれば、再度手番を変えるという処理をしてあげれば、良いということになります。
ちなみに、上のテストの場合で、「numpy.sum(able)」を処理すると、きちんと置ける場所が4つだと分かるように、4.0と返却されます。

終局判定も同じ考え方

さて、石を置くことが出来るかという点に関しては、以上の通りなので、gameUIクラスの中に、これを実装してあげれば、問題は解決しそうです。
しかもうれしいことに、さらにその中で、終局判定も実施することが可能です。

というのも、終局は、64個石が置かれている状態ではなく、「双方が置くことが出来なくなった場合」です。64個石が置かれた状態は、双方置けなくなった場合の一例でしかありません。なので、gameUIの中に、次の図のような分岐を作ってあげれば、一挙に問題は解決します。

実際にこのような分岐を実装すると、次のようになります。

    def on_click(self, event):
        self.x = int(event.x/60)
        self.y = int(event.y/60)
        self.nextstatus = self.game.copy()
        nextgame(self.nextstatus, self.y, self.x, self.player)
        if self.nextstatus[self.y][self.x] != self.game[self.y][self.x]:
            self.game = self.nextstatus
            self.player = -1 * self.player
            able = placable(self.game, self.player)
            if numpy.sum(able) == 0.0: #どこにも置けない場合、手番を変更
                self.player = -1 * self.player
                able = placable(self.game, self.player)
            self.on_draw()
            if numpy.sum(able) == 0.0: #どこにも置けない場合、終局
                self.game_end() #終局の処理をgame_end関数に格納

UIをもっとよくする

最後に、UIを良くして、例えばどちらがいくつ石を手に入れているか、といった情報や、今手番がどちらにあるか、といった情報も出していきたいと思います。また、終局の場合の表示も実装して、完成となります。
この辺りは、on_draw関数の中で石の配置状況以外の情報を表示できるようにするのと、game_end関数の中に最後の処理を格納するだけです。

    def on_draw(self):
        for row in range(8):
            for clm in range(8):
                if self.game[row][clm] == 1:
                    self.c.create_oval(clm * 60 + 5, row * 60 + 5, (clm + 1) * 60 - 5, (row + 1) * 60 - 5, width = 0.0, fill = '#FFFFFF')
                if self.game[row][clm] == -1:
                    self.c.create_oval(clm * 60 + 5, row * 60 + 5, (clm + 1) * 60 - 5, (row + 1) * 60 - 5, width = 0.0, fill = '#000000')
        self.c.create_rectangle(500, 20, 620, 60, width = 0.0, fill = '#ffffaa')
        self.c.create_rectangle(500, 80, 620, 120, width = 0.0, fill = '#ffffaa')
        lbl_white = tk.Label(text='白:'+str(numpy.count_nonzero(self.game == 1.0)) + '個', bg='#ffffaa', font=("メイリオ", "16", "bold")) 
        lbl_white.place(x=510, y=25)
        lbl_black = tk.Label(text='黒:'+str(numpy.count_nonzero(self.game == -1.0)) + '個', bg='#ffffaa', font=("メイリオ", "16", "bold")) 
        lbl_black.place(x=510, y=85)
        lbl = tk.Label(text='手番', font=("メイリオ", "16", "bold")) 
        lbl.place(x=530, y=140) 
        self.c.create_rectangle(510, 180, 610, 280, width = 0.0, fill = '#006400')
        if self.player== 1:
            self.c.create_oval(530, 200, 590, 260, width = 0.0, fill = '#FFFFFF')
        if self.player== -1:
            self.c.create_oval(530, 200, 590, 260, width = 0.0, fill = '#000000')
    def game_end(self):
        if numpy.count_nonzero(self.game == 1.0) > numpy.count_nonzero(self.game == -1.0):
            lbl_end = tk.Label(text='白の勝ち', font=("メイリオ", "16", "bold")) 
        elif numpy.count_nonzero(self.game == 1.0) < numpy.count_nonzero(self.game == -1.0):
            lbl_end = tk.Label(text='黒の勝ち', font=("メイリオ", "16", "bold")) 
        else:
            lbl_end = tk.Label(text='引き分け', font=("メイリオ", "16", "bold")) 
        lbl_end.place(x=500, y=300) 

これを実装すると、次のようになります。

さいごに

以上で、オセロの完成です。
実に6回にわたって解説をしてきましたが、これで、numpyの配列の基本的な操作や、tkinterを使ったGUIの作成などが実際の使い方と共に身に付いたと思います。
このように、実際に何かを作りながらやり方を学んでいくのは、非常に効果的です。オセロ以外にも、ご自身の好きなボードゲームを実装して色々試してもらえると良いかと思います。

最後にコードをすべて掲載すると、次の通りになります。

import numpy
import tkinter as tk

game = numpy.zeros((8, 8))
game[3][3] =  1
game[4][4] =  1
game[3][4] = -1
game[4][3] = -1
def nextgame(game, row, clm, Player):
    if game[row][clm] == 0:
        for d_iter in range(9):
            dc = int(d_iter / 3) - 1
            dr = d_iter % 3 - 1
            length = 0
            while True:
                length += 1
                if row + dr * length < 0 or row + dr * length > 7 \
                or clm + dc * length < 0 or clm + dc * length > 7:
                    break
                buf = game[row + dr * length][clm + dc * length]
                if length == 1 and buf != -1 * Player:
                    break
                if length > 1 and buf == 0:
                    break
                if length > 1 and buf == Player:
                    for iter in range(length):
                        game[row + dr * iter][clm + dc * iter] = Player
def placable(game, Player):
    able = numpy.zeros((8, 8))
    for rowIter in range(8):
        for clmIter in range(8):
            nextstatus = game.copy()
            nextgame(nextstatus, rowIter, clmIter, Player)
            if nextstatus[rowIter][clmIter] != game[rowIter][clmIter]:
                able[rowIter][clmIter] = Player
    return able

class gameUI(tk.Frame):
    def __init__(self, game, master=None):
        tk.Frame.__init__(self, master)
        self.master.title('othello')
        self.c = tk.Canvas(self, width = 640, height = 480, highlightthickness = 0)
        self.c.bind('<Button-1>', self.on_click)
        self.player = 1
        self.c.pack()
        #盤の色を指定
        self.c.create_rectangle(0, 0, 480, 480, width = 0.0, fill = '#006400')
        #升目を表示
        for rowIter in range(8):
            self.c.create_line(0, rowIter * 60, 480, rowIter * 60, width = 1.0, fill = '#FFFFFF')
        for clmIter in range(8):
            self.c.create_line(clmIter * 60, 0, clmIter * 60, 480, width = 1.0, fill = '#FFFFFF')
        #石を配置
        self.game = game
        self.on_draw()
    def on_click(self, event):
        self.x = int(event.x/60)
        self.y = int(event.y/60)
        self.nextstatus = self.game.copy()
        nextgame(self.nextstatus, self.y, self.x, self.player)
        if self.nextstatus[self.y][self.x] != self.game[self.y][self.x]:
            self.game = self.nextstatus
            self.player = -1 * self.player
            able = placable(self.game, self.player)
            if numpy.sum(able) == 0.0: #どこにも置けない場合、手番を変更
                self.player = -1 * self.player
                able = placable(self.game, self.player)
            self.on_draw()
            if numpy.sum(able) == 0.0: #どこにも置けない場合、終局
                self.game_end() 
    def on_draw(self):
        for row in range(8):
            for clm in range(8):
                if self.game[row][clm] == 1:
                    self.c.create_oval(clm * 60 + 5, row * 60 + 5, (clm + 1) * 60 - 5, (row + 1) * 60 - 5, width = 0.0, fill = '#FFFFFF')
                if self.game[row][clm] == -1:
                    self.c.create_oval(clm * 60 + 5, row * 60 + 5, (clm + 1) * 60 - 5, (row + 1) * 60 - 5, width = 0.0, fill = '#000000')
        self.c.create_rectangle(500, 20, 620, 60, width = 0.0, fill = '#ffffaa')
        self.c.create_rectangle(500, 80, 620, 120, width = 0.0, fill = '#ffffaa')
        lbl_white = tk.Label(text='白:'+str(numpy.count_nonzero(self.game == 1.0)) + '個', bg='#ffffaa', font=("メイリオ", "16", "bold")) 
        lbl_white.place(x=510, y=25)
        lbl_black = tk.Label(text='黒:'+str(numpy.count_nonzero(self.game == -1.0)) + '個', bg='#ffffaa', font=("メイリオ", "16", "bold")) 
        lbl_black.place(x=510, y=85)
        lbl = tk.Label(text='手番', font=("メイリオ", "16", "bold")) 
        lbl.place(x=530, y=140) 
        self.c.create_rectangle(510, 180, 610, 280, width = 0.0, fill = '#006400')
        if self.player== 1:
            self.c.create_oval(530, 200, 590, 260, width = 0.0, fill = '#FFFFFF')
        if self.player== -1:
            self.c.create_oval(530, 200, 590, 260, width = 0.0, fill = '#000000')
    def game_end(self):
        if numpy.count_nonzero(self.game == 1.0) > numpy.count_nonzero(self.game == -1.0):
            lbl_end = tk.Label(text='白の勝ち', font=("メイリオ", "16", "bold")) 
        elif numpy.count_nonzero(self.game == 1.0) < numpy.count_nonzero(self.game == -1.0):
            lbl_end = tk.Label(text='黒の勝ち', font=("メイリオ", "16", "bold")) 
        else:
            lbl_end = tk.Label(text='引き分け', font=("メイリオ", "16", "bold")) 
        lbl_end.place(x=500, y=300) 

f = gameUI(game)
f.pack()
f.mainloop()

コメント

タイトルとURLをコピーしました