【はじめてのPython】オセロを作る⑤~GUIベースで動かす~

Python 3

こんにちは。ヒトツメです。
少し間が空いてしまいましたが、前回に引き続き、オセロのGUIを作っていきます。
今回は、GUIを使ってオセロを操作するところを実装していきたいと思います。

スポンサーリンク

クリック操作

前回、tkinterを使って、初期配置の状態の石をGUIに設置するところまで完了しました。次は実際にクリック操作で石を配置できるようにする必要があります。
そこで、tkinterのbindという関数を利用します。

bind関数は、かなりざっくり言ってしまえば、特定の操作(クリックやキー操作など)がなされた場合に、どういう処理をするかを設定することが出来るものです。
試しに次のようなコードを入れ、tkinterの画面を押すと、下の図のように、指定した操作がなされます。

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 = 480, height = 480, highlightthickness = 0)
        self.c.bind('<Button-1>', self.on_click)
        self.c.pack()
    def on_click(self, event):
        print('click')

f = gameUI(game)
f.pack()
f.mainloop()
tkinterの画面を押した分、「click」と表示される

このとき、bind関数は、on_clickの引数としている「event」の部分に様々な情報を持っており、例えば、tkinterのウィンドウのどの場所が押されたかも把握することが出来ます。縦軸と横軸の位置を、それぞれ「event.y」と「event.x」に持っているので、on_click関数の中の記述を次のようにすることで、どのマスが押されたかが把握可能となります。

    def on_click(self, event):
        self.x = int(event.x/60)
        self.y = int(event.y/60)

次の局面を作って描画する

あとは、以前までに作成している、nextgame関数を使って、gameを次の局面に持っていき、描画するだけです。
ただ、ここで一つ問題があります。というのも、今までnextgameは、次の局面をarrayで表現するための関数でした。ここでは、次に白の石の手番なのか、黒の石の手番なのかが、わからず、playerという引数にいちいち設定してあげる必要がありました。

そこで、gameUIのクラスの初期化の時に、playerを「1」とし、クリックされるごとに、これを-1と1に変更してあげる必要があります。
また、前回まで、石の配置は初期化の中に入れていましたが、クリックするごとに石を配置するのであれば、この操作は外に出して、配置が必要な度に関数で呼び出した方が全体の記述が完結になります。

    def on_click(self, event):
        self.x = int(event.x/60)
        self.y = int(event.y/60)
        nextgame(self.game, self.y, self.x, self.player)
        self.player = -1 * self.player

        #このon_drawの中に、石の配置の指示を入れる
        self.on_draw()
    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')

これらを実装すると、実際にクリック操作だけでオセロを動かすことが出来るようになります。

置けない場所に置いた場合

ただ、この実装、実際に動かしてみると分かるのですが、置けない場所に置いてもPlayerが変わってしまい、何度かクリックすることで、ありえない操作ができてしまいます。
そこで、置ける場所に置いた場合にのみplayerを変更するといった処理に変える必要があります。

このとき、考え方としては、nextgame関数の中にplayerを変更するという処理を入れてしまって、nextgameを呼び出すごとに、石がひっくり返ればplayerを変更するという考え方がありえます。ただ、今回はnextgame関数で処理されたgameの状態と、もともとのgameの状態を見比べて、違いがある場合にplayerを変更するという処理にしようと思います。
理由としては、石を置いた場合以外でも、手番が交代になる可能性があるからです。相手が石を置くことが出来ない場合、連続して石を置くことが出来ますが、これは実質的に、相手方の手番の際に石が置けない場合、もう一度手番が変わるという動作です。この場合の手番の変更をnextgame関数に入れ込むのはかなり大変なので、gameUI側で手番の変更を設定した方が楽だと考えられるということです。

そこで、on_clickを次のように書き換えます。

    def on_click(self, event):
        self.x = int(event.x/60)
        self.y = int(event.y/60)
        print(self.y, self.x)
        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
            self.on_draw()

考え方としては、gameをコピーしてnextgame関数を呼び出し、配置しようとした場所が変更されている場合にのみ、手番の変更と配置の描写を行うというものです。
ちなみに、この時self.nextstatus = self.gameとしてしまうと、うまく動きません。これは、Pythonの特徴なのですが、このような記述をしてしまうと、nextstatusとgameは、違う名前の同じものになってしまい、nextgame関数の処理をしたとき、gameにもその処理が適用されてしまうためです。

さいごに

いままでの実装を記述すると、次のようになります。

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

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 = 480, 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
            self.on_draw()
    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')

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

次回は、先ほども出てきた、置く場所がなくて手番が変更になる場合、また、終局の場合を実装しようと思います。それでオセロは完成となる予定です。

コメント

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