ステーキの戯れ言

私のブログです。アドベントカレンダーを埋めるために作りました。

Python3とTkinter&canvasでマインスイーパーを作る その2

前回

uragou.hatenablog.com

こんなの作ってました。
f:id:uragou:20190705153638p:plain

canvasのモジュールについて
canvas = tkinter.Canvas(...)したということで、canvas.〜〜として記述します。
コードは一番下

オブジェクトを作成する

四角形は
self.canvas.create_rectangle(0,0,self.x * 40 + 20, self.y * 40 + 20, fill = "white")
create_rectangleは左上の座標と右下の座標を最初の4つの引数に、fillにカラーコードなどを入れれば、その図形を作成してくれる。
上のは、canvasの範囲いっぱいに白の四角形の図形を置く。(背景が白になる)

文字も同じように座標を引数に渡すが、テキストの場合は表示したい場所の中心の座標が必要。
作りたいオブジェクトごとに座標の指定の仕方があるので注意。

大量のオブジェクトを管理する。

オブジェクト生成時に実は各関数にて返り値があり、その図形のkeyが帰ってきている。
ただ、自分のわかりやすい規則でオブジェクトを管理したい場合はオブジェクト生成時にtagをつけることができる。
(create_rectangle(x1,y1,x2,y2,tag = str ,...)

タグとして文字列を作っておけば、生成順番関係なく各オブジェクトにアクセスしやすい。 タグでできることは

  • 任意のタグ付きオブジェクトの状態変更
  • 任意のタグ付きオブジェクトへの関数登録
  • 任意のタグ付きオブジェクトの削除
  • etc

オブジェクトの重なりの管理

基本的にオブジェクトは先に作成したものが下に来るようになる。
(環境依存でないことを祈るばかり)
ゲームが終わったときや、万が一を考え生成時に重なりが変わるようにcanvas.tag_raiseを使用している。
canvas.tag_raise(tag)
タグで指定されたオブジェクトは重なりが上に来る。
反対に重なりを下にする関数もあるが今回は使わなかった。

オブジェクトの削除

canvas.delete(tag) 指定したタグのついたオブジェクトを削除する。
オブジェクトを全削除する場合は
canvas.delete("all")

オブジェクトへのイベント

canvas.tag_bind(tag,event,function)
オブジェクトのタグ、発火条件、作動する関数オブジェクトを指定する。
登録できる条件などは逐一探せばいい。今回使ったのは

  • canvas.tag_bind(Mtag , "<Button-1>" , self.Bopen)(左クリック)
  • canvas.tag_bind(Mtag , "<Button-3>" , self.Flag)(右クリック)
  • canvas.tag_bind(Mtag , "<Enter>" , self.Enter)(マウスカーソルがオブジェクトを指した時)
  • canvas.tag_bind(Mtag , "<Leave>" , self.Leave)(マウスカーソルがオブジェクトから離れた時)

それぞれ、ブロック削除・フラグ設置・ブロック強調などをする関数を実行する。

ゲームオーバー時など、オブジェクトについた設定したイベントを削除する場合は
canvas.tag_unbind(tag , event)

イベント実行時

イベントにて実行された関数には引数としてイベントが起こったときの情報が渡される。(今回は特には使っていない)
複数のオブジェクトに同一の関数を登録していると、イベント発生時にどのタグのついたオブジェクトから呼ばれたのかは引数にはないが、関数実行時に関数内では、イベント呼び出し元のオブジェクトに current というタグがつく。
これにより「クリックされたオブジェクトを削除」などの操作を実現できる。

イベントでの色の変更

canvas.itemconfigure(tag,...)
引数のタグ以降には変更したい要素(fillとか)を書くと、該当オブジェクトの色などを変更できる。
今回のマインスイーパーの場合は、ブロックの色によって動作が変わるが(フラグの設置・削除など)オブジェクトの色を判定したい場合、色々やってみた結果…

for lop in self.canvas.itemconfig("current"):
            print(lop)

するとオプションなどが表示される。
print(self.canvas.itemconfig("current")["fill"])すると
('fill', '', '', '', '#d3d3d3')のような結果が返ってきたため
if canvas.itemconfig("current")["fill"][4] == color:
でオブジェクトからイベント実行時に、オブジェクトの色を判定して処理を変えることができるようになった。

プログラム内でのタイマー作成

コードを見てもらえば分かるが、timeモジュールを使用していない。 canvasにはcanvas.after(time,function) 第一引数には時間(ms)、第二引数に関数を指定すると
指定時間後にその関数を実行してくれる。

基本的なタイマー

import tkinter

def Timer():
        global time

        print(time)
        time += 1
        
        canvas.after(1000,Timer)

a = tkinter.Tk()
canvas = tkinter.Canvas(master=a)
canvas.pack()

time = 0
Timer()

a.mainloop()

もちろんこれでは止まらないが、条件によりループを止めたり、外部からtime変数を変更するなどすればストップ・リセット機能を作成可能。

感想など

オブジェクト管理・イベントの登録・タイマーの設定、これに加えてマインスイーパーのプログラムを組み合わせることにより完成しました。
割とやりたいことが実現する機能があったのでとても良き

コード

# coding: utf-8 
import tkinter
import random

class Field():
    def __init__(self,x,y,mine):
        
        self.x = x
        self.y = y
        self.start_mine = mine

        #ゲームが終わったかどうか
        self.game_finish = False

        self.nomal_color = "#808080"
        self.flag_color = "#87cefa"
        self.enter_color = "#d3d3d3"

        self.main = tkinter.Tk()
        self.main.title(u"マインスイーパー")
        #self.main.geometry( str(self.x * 40 + 20) + "x" +  str(self.y * 40 + 170) )
        
        self.main.grid_columnconfigure((0,1,2),weight=1)

        self.message_mine = tkinter.IntVar()
        tkinter.Label(master=self.main,font=("",20),bd=1,fg = "red",textvariable= self.message_mine).pack(pady = 0)
        
        self.message_time = tkinter.StringVar()
        tkinter.Label(master=self.main,font=("",20),textvariable= self.message_time).pack(pady = 0)

        tkinter.Button(master=self.main,text=u"リセット",command=self.Reset).pack(pady = 0)
        
        self.message_game = tkinter.StringVar()
        tkinter.Label(master=self.main,font=("",20),textvariable= self.message_game).pack(pady = 0)

        self.canvas = tkinter.Canvas(master=self.main,width = self.x * 40 + 20, height = self.y * 40 + 20)
        self.canvas.pack()

        self.Game_Init()

        self.canvas.after(1000,self.Loop)

    def Bopen(self,ev):
        if self.canvas.itemconfig("current")["fill"][4] == self.flag_color:
            return

        map_data = self.canvas.gettags("current")[0].split("-")
        self.active_field -= 1
        self.canvas.delete("current")

        if self.Mine_map[ int( map_data[1] ) ][ int( map_data[2] ) ] == 0:
            self.Copen( int( map_data[1] ) , int( map_data[2] ) )
        elif self.Mine_map[ int( map_data[1] ) ][ int( map_data[2] ) ] == 9:
            self.Gameover("bomb")

        if self.active_field - self.start_mine == 0:
            self.Gameover("clear")


    def Copen(self,y,x):
        if x > 0:
            Next_map = self.canvas.gettags( "field-" + str(y) + "-" + str(x-1) )

            if not( Next_map == () ) and not(self.Mine_map[y][x-1] == 9):
                self.active_field -= 1
                self.canvas.delete(Next_map)
                if self.Mine_map[y][x-1] == 0:
                    self.Copen(y,x-1)

        if x < self.x - 1:
            Next_map = self.canvas.gettags( "field-" + str(y) + "-" + str(x+1) )

            if not( Next_map == () ) and not(self.Mine_map[y][x+1] == 9):
                self.active_field -= 1
                self.canvas.delete(Next_map)
                if self.Mine_map[y][x+1] == 0:
                    self.Copen(y,x+1)
        
        if y > 0:
            Next_map = self.canvas.gettags( "field-" + str(y-1) + "-" + str(x) )

            if not( Next_map == () ) and not(self.Mine_map[y-1][x] == 9):
                self.active_field -= 1
                self.canvas.delete(Next_map)
                if self.Mine_map[y-1][x] == 0:
                    self.Copen(y-1,x)
        
        if y < self.y - 1:
            Next_map = self.canvas.gettags( "field-" + str(y+1) + "-" + str(x) )

            if not( Next_map == () ) and not(self.Mine_map[y+1][x] == 9):
                self.active_field -= 1
                self.canvas.delete(Next_map)
                if self.Mine_map[y+1][x] == 0:
                    self.Copen(y+1,x)

        

    def Enter(self,ev):
        if self.canvas.itemconfig("current")["fill"][4] == self.nomal_color:
            self.canvas.itemconfigure("current" , fill = self.enter_color)
        

    def Fadd(self,y,x):

        if y > 0:
            self.Mine_map[y-1][x] = self.Mine_map[y-1][x] + 1 if not(self.Mine_map[y-1][x] == 9) else self.Mine_map[y-1][x] 
            
        if y < self.y - 1:
            self.Mine_map[y+1][x] = self.Mine_map[y+1][x] + 1 if not(self.Mine_map[y+1][x] == 9) else self.Mine_map[y+1][x]

        if x > 0:
            self.Mine_map[y][x-1] = self.Mine_map[y][x-1] + 1 if not(self.Mine_map[y][x-1] == 9) else self.Mine_map[y][x-1]

        if x < self.x - 1:
            self.Mine_map[y][x+1] = self.Mine_map[y][x+1] + 1 if not(self.Mine_map[y][x+1] == 9) else self.Mine_map[y][x+1]

        if x > 0 and y > 0:
            self.Mine_map[y-1][x-1] = self.Mine_map[y-1][x-1] + 1 if not(self.Mine_map[y-1][x-1]== 9) else self.Mine_map[y-1][x-1]
        
        if x > 0 and y < self.y - 1:
            self.Mine_map[y+1][x-1] = self.Mine_map[y+1][x-1] + 1 if not(self.Mine_map[y+1][x-1] == 9) else self.Mine_map[y+1][x-1]

        if y > 0 and x < self.x - 1:
            self.Mine_map[y-1][x+1] = self.Mine_map[y-1][x+1] + 1 if not(self.Mine_map[y-1][x+1] == 9) else self.Mine_map[y-1][x+1]

        if x < self.x - 1 and y < self.y - 1:
            self.Mine_map[y+1][x+1] = self.Mine_map[y+1][x+1] + 1 if not(self.Mine_map[y+1][x+1] == 9) else self.Mine_map[y+1][x+1]

    def Field_init(self):

        self.Field_tag = []        

        for lop in range(self.y):
            Tbuf = []

            for lop2 in range(self.x):
                Mtag = "field-" + str(lop) + "-" + str(lop2)
                Tbuf.append( Mtag )
                self.canvas.create_rectangle(lop2 * 40 + 10, lop * 40 + 10, lop2 * 40 + 50, lop * 40 + 50,tag = Mtag , fill = self.nomal_color , outline = "white" , width = 2)
                self.canvas.tag_bind(Mtag , "<Button-1>" , self.Bopen)
                self.canvas.tag_bind(Mtag , "<Button-3>" , self.Flag)
                self.canvas.tag_bind(Mtag , "<Enter>" , self.Enter)
                self.canvas.tag_bind(Mtag , "<Leave>" , self.Leave)

            self.Field_tag.append(Tbuf)

    def Flag(self,ev):
        if self.canvas.itemconfig("current")["fill"][4] == self.enter_color:
            self.canvas.itemconfigure("current" , fill = self.flag_color)

            mine = self.message_mine.get()
            self.message_mine.set( mine - 1 )
        else:
            self.canvas.itemconfigure("current" , fill = self.enter_color)

            mine = self.message_mine.get()
            self.message_mine.set( mine + 1 )

    def Game_Init(self):
        
        self.canvas.create_rectangle(0,0,self.x * 40 + 20, self.y * 40 + 20, fill = "white")
        self.message_game.set(u"")
        self.message_mine.set(self.start_mine)

        self.active_field = self.x * self.y

        self.Mine_set()
        self.Map_load()
        self.Field_init()

        for lop in range(self.y):
            for lop2 in range(self.x):
                self.canvas.tag_raise("field-" + str(lop) + "-" + str(lop2))
                #チート
                #self.canvas.tag_raise("map-" + str(lop) + "-" + str(lop2))

        if self.game_finish == True:
            self.game_finish = False
            self.canvas.after(1000,self.Loop)
            
        self.message_time.set(u"time 0 : 00")
        self.time = 0

        

    def Gameover(self,res):
        
        if res == "clear":
            self.message_game.set(u"ゲームクリア")
        elif res == "bomb":
            self.message_game.set(u"ボムりました")
        
        for lop in range(self.y):
            for lop2 in range(self.x):
                Mtag = "field-" + str(lop) + "-" + str(lop2)

                if not(self.canvas.gettags( Mtag ) == () ):

                    if self.Mine_map[lop][lop2] == 9:

                        if self.canvas.itemconfig(Mtag)["fill"][4] == self.flag_color:
                            self.canvas.tag_raise("map-" + str(lop) + "-" + str(lop2))
                        else:
                            self.canvas.delete(Mtag)

                    self.canvas.tag_unbind(Mtag , "<Button-1>")
                    self.canvas.tag_unbind(Mtag , "<Button-3>")
                    self.canvas.tag_unbind(Mtag , "<Enter>")
                    self.canvas.tag_unbind(Mtag , "<Leave>")
        self.game_finish = True

    def Leave(self,ev):
        if self.canvas.itemconfig("current")["fill"][4] == self.enter_color:
            self.canvas.itemconfigure("current" , fill = self.nomal_color)

    def Loop(self):
        if self.game_finish:
            return

        self.time += 1
        message = u"time " + str( int( (self.time - (self.time % 60) ) / 60 ) ) + " : "
        if self.time % 60 < 10:
            message += u"0" + str(self.time % 60)
        else:
            message +=  str( self.time % 60)
        self.message_time.set( message )

        self.canvas.after(1000,self.Loop)

    def Map_load(self):
        num_color = ["","black","midnightblue","blue","darkturquoise","darkgreen","green","orange","blueviolet","red"]

        for lop in range(len(self.Mine_map)):

            for lop2 in range(len(self.Mine_map[0])):
                Mtag = "map-" + str(lop) + "-" + str(lop2)
                
                if self.Mine_map[lop][lop2] == 9:
                    self.canvas.create_text(lop2 * 40 + 30 , lop * 40 + 30,tag = Mtag ,font = ("",20),text = "B" , fill = num_color[self.Mine_map[lop][lop2]])
                elif self.Mine_map[lop][lop2] > 0:
                    self.canvas.create_text(lop2 * 40 + 30 , lop * 40 + 30,tag = Mtag ,font = ("",20),text = str(self.Mine_map[lop][lop2]) , fill = num_color[self.Mine_map[lop][lop2]])
                

    def Mine_set(self):
        self.Mine_map = []
        Mine_place = sorted( random.sample( range(self.x * self.y) , self.start_mine ) ) 
        cnt = 0

        for lop in range(self.y):
            buf = []
            for lop2 in range(self.x):
                if len(Mine_place) > 0 and Mine_place[0] == cnt :
                    del Mine_place[0]
                    buf.append(9)
                else:
                    buf.append(0)
                cnt += 1
            self.Mine_map.append(buf)
             
        for lop in range(self.y):
            for lop2 in range(self.x):
                if self.Mine_map[lop][lop2] == 9:
                    self.Fadd(lop,lop2)

    def Reset(self):
        self.canvas.delete("all")
        
        self.Game_Init()



def set_func():

    if size_x.get().isdecimal() == False or size_y.get().isdecimal() == False or mine_num.get().isdecimal() == False:
        message_err.set(u"文字や負数を入れるな")
        return
    x = int(size_x.get())
    y = int(size_y.get())
    mine = int(mine_num.get())

    if x < 4 or y < 4 or mine < 1:
        message_err.set(u"少ない")

    #マス数*0.8 まで許容
    elif x > 20 or y > 20 or x * y  < int( mine / 0.8 ) :
        message_err.set(u"多い")

    else:
        config.destroy()
        
        game = Field(x,y,mine)
        game.main.mainloop()

config = tkinter.Tk()
config.title("config")

tkinter.Label(master=config,text=u"サイズ").grid(row = 0,column = 0)

size_x = tkinter.Entry(master=config)
size_x.insert(tkinter.END,u"12")
size_x.grid(row = 0,column = 1)

tkinter.Label(master=config,text="×").grid(row = 0,column = 2)

size_y = tkinter.Entry(master=config)
size_y.insert(tkinter.END,u"8")
size_y.grid(row = 0,column = 3)

tkinter.Label(master=config,text=u"地雷数").grid(row = 1,column = 0)

mine_num = tkinter.Entry(master=config)
mine_num.insert(tkinter.END,u"10")
mine_num.grid(row = 1,column = 1)

tkinter.Button(master=config,text=u"スタート",command=set_func).grid(row=2,columnspan=3)

message_err = tkinter.StringVar()
tkinter.Label(master=config,textvariable=message_err).grid(row = 3,columnspan=3)

config.mainloop()

このコードは最新でない可能性もあるので、github

github.com