2018年12月22日土曜日

WxPythonのテトリスがあったのでコードを読んでみた。№4ブロック移動回転






参考サイト

テトリス
http://wxpython.at-ninja.jp/thetetrisgame.html

だいぶ複雑なコードなので順に読み解いていきます。
最初はウィンドウなどの表示のみのコード
№2ブロックも表示
№3ブロック落下
今回 №4ブロック回転移動をします。
#********* のところは前回から付け加えた部分。

WxPythonのカーソルイベント

WxPythonではカーソルキーイベントが拾えなさそう。以下サイトを参考にして、以下のようにコードを変えてもカーソルキーイベントがひろえませんでした。

self.Bind(wx.EVT_CHAR_HOOK, self.hotkey)として

wxPython(Phoenix) キーボード操作は、wx.EVT_CHAR_HOOKを使うのが良さう
http://ikapblg.blog.fc2.com/blog-entry-90.html
def hotkey(self, event):
code = event.GetKeyCode()
if code == wx.WXK_LEFT:  # or whatever...
print "hotkey WXK_LEFT"
else:
event.Skip()

結局以下サイトを参考にカーソルイベントを拾うコードを書いてみました。
Pythonでキーイベント取得(Windows)
http://shade777.blog54.fc2.com/blog-entry-579.html
E:\MyBackups\goolgedrive\myprg_main\python_my_prg\wxpython\thread_keyevent.py

import threading
import ctypes
import time


LEFT = 0x25
RIGHT = 0x27
DOWN = 0x28
UP   = 0x26
#下のようにかいたら思うような結果ならない
#直接16進数で各必要がある
#UP   = hex(38)

def getkey(key):
    return(bool(ctypes.windll.user32.GetAsyncKeyState(key)&0x8000))

def metaGetkey():
    while True:
        time.sleep(1)
        print "metaGetkey"
        if getkey(LEFT):
            print "LEFT"
        if getkey(RIGHT):
            print "RIGHT"
        if getkey(DOWN):
            print "DOWN"
        if getkey(UP):
            print "UP"

metaGetkey()

コード

E:\MyBackups\goolgedrive\myprg_main\python_my_prg\wxpython\game_tetoris_idou.p

# coding: UTF-8

#参考サイト
#http://wxpython.at-ninja.jp/thetetrisgame.html
#テトリス

#WxPython カーソルキーイベントを検知しないようなので
#別のキーイベントを検知するコードを入れる
#Pythonでキーイベント取得(Windows)
#http://shade777.blog54.fc2.com/blog-entry-579.html

#Shapeクラスでcoordsを決定してブロックの形状を決定します。
#それをOnPaintで実際の座標に変換して、drawSquareでブロックを
#描きます
#coordsでの形状表示が、OnPaintで実際の座標に変換される時
#y軸方向の表示が上下逆になります。

# game_tetoris_draw_kakidasi.py によってブロック表示だけを実現
# 今回、それを落下させてみる
#横10 ブロックの座標で x = 0~9 
#縦22 ブロックの座標で y = 0~21
#で表現されている

#curX curY はブロックの基準となるブロック座標
#curX の初期値は6 ブロック形状座標0のセルがここにくる
#curY の初期値は 下の計算式とブロック形状によって
#yが21となるよう curYが決められる。
#どのようなブロック形状座標であろうと、ブロックの初期描画で
#描画表示ウィンドウの最上部と隙間なく、描画される

#curPiece はShapeのcoordsで与えられる、0を基準とした
#ブロックの形状を表すブロックのセルの位置座標
#(ブロック形状座標と呼ぼう)を示す
#x(i) y(i) はおのおの それの x y成分をしめす。
#x = self.curX + self.curPiece.x(i)
#y = self.curY - self.curPiece.y(i)
#x y はブロックのセル各々のブロック座標となる

import threading
import ctypes
import time
import wx
import random

"""  キーイベント取得     """
LEFT = 0x25
RIGHT = 0x27
DOWN = 0x28
UP   = 0x26
#下のようにかいたら思うような結果ならない
#直接16進数で書く必要がある
#UP   = hex(38)
#キーのコード表はここにある
#https://hhhappyyyymoto.blogspot.com/2017/04/3getasynckeystate.html?zx=23e7184c28a6c3f8

keycode = 0

class MyApp(wx.App):
    def __init__(self): 
        #エラーが出たらファイルに書き出す
        wx.App.__init__(self, redirect=True, filename="game_tetoris_log.txt")
        #wx.App.__init__(self, redirect=True )


class Tetris(wx.Frame):
    def __init__(self, parent, id, title):
        print "Tetris __init__",
        wx.Frame.__init__(self, parent, id, title, size=(180, 380))
        #ステータスバー作成
        self.statusbar = self.CreateStatusBar()
        self.statusbar.SetStatusText('0')
        #boardはBoardクラスにも使われているからややこしい
        self.board = Board(self)
        self.board.SetFocus()
        #初期値設定 タイマースタート
        self.board.start()
        self.Centre()
        self.Show(True)


class Board(wx.Panel):
    BoardWidth  = 10
    BoardHeight = 22
    Speed       = 300
    ID_TIMER    = 1

    def __init__(self, parent):
        print "",
        print "Board __init__",
        wx.Panel.__init__(self, parent)
        #イベントをidで区別しているようだ
        # evet_id.py 参照
        self.timer              = wx.Timer(self, Board.ID_TIMER)
        self.curPiece           = Shape()#現在のブロックのオブジェクト
        self.nextPiece          = Shape()#次のブロックのオブジェクト

        #現在落下中のブロックを描画するときは、ミノを self.curX
        #と self.curY の位置に描画します。 そして、座標テーブ
        #ルを参照し、4つの正方形全てを描画します。
        self.curX               = 0
        self.curY               = 0
        self.numLinesRemoved    = 0
        self.board              = []#ブロックのミノの位置

        self.isStarted = False
        #self.isPaused  = False

        #再描画の時OnPaintを呼び出す
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_TIMER, self.OnTimer, id=Board.ID_TIMER)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)

        self.clearBoard()

        #簡略化のため元コードは関数をつかっていたが、ここでは定数化した
        #ブロックのセル一個の幅、高さ
        self.squareWidth = 17
        self.squareHeight = 15
        self.boardTop = 3

    #ブロック座標にどんなブロックのセルがはいっているかを示してい
    #る?
    def shapeAt(self, x, y):                            #****************
        print "shapeAt"
        return self.board[(y * Board.BoardWidth) + x]

    #boardのリストを変えているのだが、何をしているのか不明
    def setShapeAt(self, x, y, shape):                   #*************** 
        print "setShapeAt"
        #boardは要素数22*10 初期値全て0
        #以下の一行 BoardHeight ならわかるのだが
        self.board[(y * Board.BoardWidth) + x] = shape
        print "setShapeAt board", self.board

    def start(self):
        print "start",
        #if self.isPaused:#isPaused:なんかのフラグ 初期値False
        #    return


        self.isStarted          = True  #isPausedがTrueならisStartedをTrueに
        #self.isWaitingAfterLine = False #初期値False
        #self.numLinesRemoved    = 0
    
        #__init__の中にもあるし二度手間では?
        self.clearBoard()
        self.newPiece()
        #300ミリ秒ごとにOnTimer()が実行される
        self.timer.Start(Board.Speed)                        

    def clearBoard(self):
        print
        print "clearBoard"
        for i in range(Board.BoardHeight * Board.BoardWidth):
            self.board.append(Tetrominoes.NoShape)# ()のなかは0
        #boardがの要素が全部0になる


    def OnPaint(self, event):
        print "OnPaint",
        dc = wx.PaintDC(self)

        #次の段階では現在落下中のを描画します。
        #curPiece.shapeは初期値で0
        #curPieceは現在のブロックのオブジェクト
        #Shape()によってpieceShapeが0(Tetrominoes.NoShape)となる
        #しかしsetRandomShape()によって、pieceShapeがランダムに1~7のどれかになる
        if self.curPiece.shape() != Tetrominoes.NoShape:
            #ブロックは4つのセルでできているからrange(4)
            for i in range(4):
                #x(i) coords[index][0]を返す
                #curXの初期値は 6
                #xの衝突について考える
                x = self.curX + self.curPiece.x(i)
                y = self.curY - self.curPiece.y(i)
                #def drawSquare(self, dc, x, y, shape):
                self.drawSquare(dc, 0 + x * self.squareWidth,
                                self.boardTop + (Board.BoardHeight - y - 1) * self.squareHeight)

                #縦に22個のブロックのセルが表示できる
                #どんな形のブロックだろうと最初はy=21
                #どれかのセルがy=0となればそのセルは底辺についた事になる
                #coordsで表現されたブロックは上のコードにより上下逆に表示される
                #例えば coords = [[0, -1], [0, 0], [1, 0], [1, 1]] とすると
                #     ■                [1, 1]
                #  ■■        [0, 0]  [1, 0] 
                #  ■        [0, -1]

                #coords[x,-1]の時newPiece()の処理→上の処理後以下となる
                #つまりちょうどyの最低値(-1)で (Board.BoardHeight - y - 1)が
                #うまく 0 となるよう作られている

                #self.curY                    20
                #self.curPiece.y(i) y        -1
                #y                            21
                #Board.BoardHeight - y - 1   0

                #例えばcoords = [[0, 1], [0, 0], [1, 0], [1, 1]] とすると
                #  ■■        [0, 1]  [1, 1]
                #  ■■        [0, 0]  [1, 0] 

                #coords[x,0]の時newPiece()の処理→上の処理後以下となる
                #つまりちょうどyの最低値(0)で うまく0となるよう作られている
                #self.curY                    21
                #self.curPiece.y(i) y        0
                #y                            21
                #Board.BoardHeight - y - 1    0
                #つまりどのような形状であれ、最初のブロックの表示時ウィンドウ
                #の最上部にピッタリと隙間なく表示される(boardTopの隙間があるが)

    def OnKeyDown(self, event):                              #************
        print "OnKeyDown"
        #startでisStarted=TrueになるしcurPiece.shape()=0という状態は
        #初期状態だし ifが成り立つのはありえないのでは?
        if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
            event.Skip()
            return

        keycode = event.GetKeyCode()
        #ord アスキーコードを取得する
        if keycode == ord('P') or keycode == ord('p'):
            self.pause()
            return
        #ゲーム進行一時停止    
        if self.isPaused:
            return
        else:
            event.Skip()

    
    def getkey(self, key):                   #***************
        return(bool(ctypes.windll.user32.GetAsyncKeyState(key)&0x8000))
    
    def metaGetkey(self):                     #**************
        print "metaGetkey"
        global keycode
        keycode = 0
        if self.getkey(LEFT):
            keycode = wx.WXK_LEFT
            self.OnKeyDown_cursor()
            print "LEFT"
        if self.getkey(RIGHT):
            keycode = wx.WXK_RIGHT
            self.OnKeyDown_cursor()
            print "RIGHT"
        if self.getkey(DOWN):
            keycode = wx.WXK_UP
            self.OnKeyDown_cursor()
            print "DOWN"
        if self.getkey(UP):
            keycode = wx.WXK_DOWN
            self.OnKeyDown_cursor()
            print "UP"
        else:
            return

    
    def OnKeyDown_cursor(self):                       #**********
        print "OnKeyDown_cursor"
        global keycode
        print "keycode",keycode
        #OnKeyDown() メソッドで、押下されたキーを調べます。
        #左矢印キーを押すと、ミノを左に 動かそうと します。
        #なぜ「動かそうとする」なのかというと、ミノは移動でき
        #ないかもしれないからです。

        if keycode == wx.WXK_LEFT:
            print "elif keycode == wx.WXK_LEFT"
            self.tryMove(self.curPiece, self.curX - 1, self.curY)
        elif keycode == wx.WXK_RIGHT:
            self.tryMove(self.curPiece, self.curX + 1, self.curY)
        elif keycode == wx.WXK_DOWN:
            self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
        elif keycode == wx.WXK_UP:
            self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
        elif keycode == wx.WXK_SPACE:
            self.dropDown()
        elif keycode == ord('D') or keycode == ord('d'):
            self.oneLineDown()
        else:
            pass

    #OnTimer() メソッドでは、以前のミノが底に落下しきった後
    #に新しいミノを生成したり、落下中のミノを1行落としたりし
    #ます。
    #oneLineDown→tryMove(Refresh)→OnPaint→drawSquareで再描画されます
    #OnTimerの先にRefreshがあるため、タイマーのタイミングで再描画されます
    def OnTimer(self, event):                            
        print
        print "Board OnTimer"
        self.metaGetkey()

        if event.GetId() == Board.ID_TIMER:
            #if self.isWaitingAfterLine:
            #    self.isWaitingAfterLine = False
            #    self.newPiece()
            #else:
                #self.oneLineDown()
            self.oneLineDown()
        else:
            event.Skip()


    #newpiece() メソッドはランダムに新しいミノを生成します。
    #ミノを初期位置に配置できなければ、ゲームオーバーです。
    def newPiece(self): 
        print "newPiece" 
        #現在のブロックのオブジェクトに次のブロックのオブジェクトを入れる
        self.curPiece = self.nextPiece

        #ブロック位置の初期化?
        #minYの動作は以下のとおり
        #self.coords [[0, -1], [0, 0], [1, 0], [1, 1]]とすると
        #minYは、coordsの中で一番ちいさなyを返す
        #例えばminYが0とすると、curYは21となる

        #実際の表示は
        # coordsとは上下が反転して表示される
        #  ■        curY=20        [0, -1]
        #  ■■        curY=21        [0, 0]  [1, 0]
        #       ■        curY=22        [1, 1]
        
        #self.curY = Board.BoardHeight - 1 、ブロックの上の部分が
        #切れて見えなくなる事がある
        #   _
        #   ■■ [0, 0][1, 0]
        #      ■
        #

        #self.curY = Board.BoardHeight とするとさらに切れてしまう
        #   __ [0, 0][1, 0]
        #     ■  [0, 1]

        #以上から、curYはブロックに対して最適な表示位置を示すパラメータと思われる

        self.curX = Board.BoardWidth / 2 + 1   #6となる
        self.curY = Board.BoardHeight - 1 + self.curPiece.minY()


    def oneLineDown(self):                                    
        print "oneLineDown"
        #tryMove=False ブロックが底に行っていたり他のブロックに接触していたら
        #self.curY - 1 によって落下する
        #ただし底辺に達したらtryMoveによってそれ以上下にいかない
        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
            #ブロック接触の処理
            #ブロックのどれかのセルが底辺に接触したら
            self.pieceDropped()
            pass

    def pieceDropped(self):                                
        print "pieceDropped"
        for i in range(4):
            x = self.curX + self.curPiece.x(i)
            y = self.curY - self.curPiece.y(i)
            #curPiece.shape() 
            #ブロックの形状を示す番号を返す
            #ブロックのセルの座標x,yに対応するboardのリストの要素を
            #今のブロックの形状を示す数値に変更する
            self.setShapeAt(x, y, self.curPiece.shape())

        #self.removeFullLines()

        #if not self.isWaitingAfterLine:
        #    self.newPiece()


    #ミノが底に当たると、 removeFullLines() メソッドを呼びま
    #す。 揃った行を見つけて、その行を削除します。 現在揃って
    #いる行を取り除き、その上の行全てを1行下げることでこれを
    #実現します。 削除される行の順番を逆にしていることに注意
    #してください。 そうでもしなければ、正しく動かないでしょ
    #うからね。 今回は単純な重力を使用しています。 つまり、ミ
    #ノは空の隙間の上に浮かぶこともありうるということです。
    def removeFullLines(self):                      #***************
        print "Board removeFullLines"
        numFullLines = 0
        statusbar = self.GetParent().statusbar
        rowsToRemove = []
        #BoardWidth  = 10  BoardHeight = 22

        for i in range(Board.BoardHeight):
            n = 0
            for j in range(Board.BoardWidth):
                #ブロック座標にどんな形状のブロックのセルがはいっているか
                #調べる
                if not self.shapeAt(j, i) == Tetrominoes.NoShape:
                    #なにかしらのブロックのセルがあれば n+1
                    n = n + 1
            #ある一列のブロック座標にブロックのセルが全部詰まっていれば
            #そのブロック座標のy座標を記録する
            if n == 10:
                rowsToRemove.append(i)

        #リストの順番を逆にして並べ替え、つまり底辺のほうからになる
        rowsToRemove.reverse()
        for m in rowsToRemove:
            for k in range(m, Board.BoardHeight):
                for l in range(Board.BoardWidth):
                    #m(k)のyブロック座標が一列セルで詰まっていれば
                    #その列のセルのブロック形状を示す数値を
                    #上のものに置き換える。つまり一列が消える?
                    self.setShapeAt(l, k, self.shapeAt(l, k+1))

            numFullLines += len(rowsToRemove)

            if numFullLines > 0:
                self.numLinesRemoved += numFullLines
                statusbar.SetStatusText(str(self.numLinesRemoved))
                self.isWaitingAfterLine = True
                self.curPiece.setShape(Tetrominoes.NoShape)
                self.Refresh()

        
    #tryMove() メソッドで、ミノを動かそうとします。 もしミノ
    #がゲーム盤の端だったり、他のミノに隣接していたりすると
    #False を返します。 そうでなければ、現在落下中のミノを新
    #しい位置に置き、 True を返します
    def tryMove(self, newPiece, newX, newY):                
        print "Board tryMove"
        for i in range(4):
            x = newX + newPiece.x(i)
            y = newY - newPiece.y(i)
            #  ■■■■■■ 横10  xは0~9
            #  ■■■■■■  縦22    yは上が21~下が0までとなる
            #  ■■■■■■
            #  ■■■■■■
            #
            #どれかのセルがy=0となればそのセルは底辺についた事になる
            #どんな形のブロックだろうと最初はy=21 BoardHeightは22
            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
                return False
            #if self.shapeAt(x, y) != Tetrominoes.NoShape:
            #    return False

        #新しいnewX, newYをいれてぶつかっていないようであれば、curX curYに代入して
        #本当に移動させる
        #下一行のコード意味がない。なくてもいい
        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        #RefreshによってOnPaintが呼ばれる
        #OnPaintに描画へ行くコードあり 再描画される
        self.Refresh()
        return True

    #def drawSquare(self, dc, x, y, shape):
    def drawSquare(self, dc, x, y):

        dc.SetBrush(wx.Brush('#000000'))
        dc.DrawRectangle(x + 1, y + 1, self.squareWidth - 2,
                         self.squareHeight - 2)



#shape ブロックの形状を決める
class Tetrominoes(object):
    NoShape        = 0
    ZShape         = 1
    SShape         = 2
    LineShape      = 3
    TShape         = 4
    SquareShape    = 5
    LShape         = 6
    MirroredLShape = 7

#Shape クラスはテトリミノの情報を保持しています。
class Shape(object):
    coordTable = (
        ((0, 0), (0, 0), (0, 0), (0, 0)),

        ((0, -1), (0, 0), (-1, 0), (-1, 1)),
        ((0, -1), (0, 0), (1, 0), (1, 1)),

        ((0, -1), (0, 0), (0, 1), (0, 2)),
        ((-1, 0), (0, 0), (1, 0), (0, 1)),

        ((0, 0), (1, 0), (0, 1), (1, 1)),
        ((-1, -1), (0, -1), (0, 0), (0, 1)),
        ((1, -1), (0, -1), (0, 0), (0, 1)))

    def __init__(self):
        #ミノの作成中に、 空の座標リストを作成します。 この
        #リストは、テトリミノの座標を保持します。 例えば、(0,
        #        -1), (0, 0), (1, 0), (1, 1) のタプルは回転し
        #たS-テトリミノを表します。 以下の図表がミノを図示し
        #ています。

        #[[0, 0], [0, 0], [0, 0], [0, 0]]を作成
        self.coords = [[0, 0] for i in range(4)]
        #0をセット
        self.pieceShape = Tetrominoes.NoShape
        self.setShape(Tetrominoes.NoShape)

    def shape(self):
        print "shape",
        #ブロックの形状をしめす番号を返す coordTableのインデックス
        return self.pieceShape

    def setShape(self, shape):
        print "setShape",
        #self.coords = [[0, 1], [0, 0], [1, 0], [1, 1]]
        #self.pieceShape = 2
        self.coords = [[-1, -1], [0, -1], [0, 0], [0, 1]]
        self.pieceShape = 6


    def x(self, index):
        return self.coords[index][0]

    def y(self, index):
        return self.coords[index][1]

    def setX(self, index, x):
        self.coords[index][0] = x

    def setY(self, index, y):
        self.coords[index][1] = y

    #coordsの中で一番ちいさなxを返す
    def minX(self):
        m = self.coord[0][0]
        for i in range(4):
            m = min(m, self.coords[i][0])

        return m

    #coordsの中で一番大きいxを返す
    def maxX(self):
        m = self.coord[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])

        return m

    #self.coords [[0, -1], [0, 0], [1, 0], [1, 1]]とすると
    #coordsの中で一番ちいさなyを返す
    def minY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])
        return m

    #coordsの中で一番大きいyを返す
    def maxY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = max(m, self.coords[i][1])

        return m

    #時計まわりに回転
    def rotatedLeft(self):
        # SquareShape:5
        # ブロックの形状が5ならなにもしない
        #上の形状は座標の第一象限に四角のブロック形状なので
        #回転させると、四角の左下の原点を中心にして回転してしまう
        #ので振動したように見えてこれを回避しているのだろう
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        #回転させた仮のブロックのオブジェクトを作成
        #tryMoveによって回転後どこにもあたらないを判断されたら
        #tryMoveの self.curPiece = newPiece によって今のブロック形状
        #に代入される。

        #ブロックの形状を初期化
        result = Shape()
        #今のブロック形状を仮のブロック形状にいれる
        result.pieceShape = self.pieceShape
        #ブロック形状座標のi番目の要素のx成分と、y成分を取り替え
        #x成分に-をつける
        #上で座標に印をつけて確認したところ、確かにブロック座標が
        #時計まわりに回転する。
        for i in range(4):
            result.setX(i, self.y(i))
            result.setY(i, -self.x(i))

        return result

    #反時計回りに回転
    def rotatedRight(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.setX(i, -self.y(i))
            result.setY(i, self.x(i))

        return result



#thread = threading.Thread(target=metaGetkey)
#thread.start()

#app = wx.App()
app = MyApp()
tetris = Tetris(None, -1, 'tetris.py')
app.MainLoop()

0 件のコメント:

コメントを投稿

About

参加ユーザー

連絡フォーム

名前

メール *

メッセージ *

ブログ アーカイブ

ページ

Featured Posts