2019年3月24日日曜日

Pythonでゲーム作成(テトリス)

だいぶ前にPythonでテトリスを実装してみたのですが,使わなくなったので公開します.

参考にしたのはhttp://www13.plala.or.jp/kymats/study/game_other/SPACE_TETRIS/st1.html

実際には,ピースのデータ構造のみ参考にして,盤面はtkinterのCanvas上にrectangleを並べると,これがobjectとして後から色を変更できるようになるので,これを使って実装 しました.

画面はこんな感じです.


点数処理も書いたのですが,これだけで結構な分量になったので,最後に点数処理の部分を除いたソースを以下に掲載します.

これを応用して六角形にしたものを「PythonでHextris」に載せておきます.昔X68000で遊んでいたゲームを作ってみました.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Python 2.7.5 on OS X 10.9.2
# by Mitsuharu Arimura 2015/3/14

import Tkinter as Tk
import datetime as dt
import time, random
import sys, os, csv

# ゲームフィールドのサイズ
FIELD_WIDTH = 18
FIELD_HEIGHT = 30

# ブロックのサイズ
PIECE_WIDTH = 4
PIECE_HEIGHT = 4

# 1個のセルのピクセル数
CELL_WIDTH = 20
CELL_HEIGHT = 20

# ゲーム本体の画面 View
class Board( Tk.Canvas ):
    # フィールドのピクセル数
    BOARD_WIDTH = FIELD_WIDTH * CELL_WIDTH
    BOARD_HEIGHT = FIELD_HEIGHT * CELL_HEIGHT
    margin = 2

    def __init__( self, master ):
        self.width = self.BOARD_WIDTH
        self.height = self.BOARD_HEIGHT
        Tk.Canvas.__init__( self, master, relief=Tk.RAISED, bd=self.margin,
                            bg='white',
                            width=self.BOARD_WIDTH, height=self.BOARD_HEIGHT )

        # Canvas上の rectangle の2次元配列
        self.Field = [[0 for y in range(FIELD_HEIGHT)]
                      for x in range(FIELD_WIDTH)]
        offset = self.margin * 2 + 3
        for x in range( FIELD_WIDTH ):
            for y in range( FIELD_HEIGHT ):
                x0 = x * CELL_WIDTH + offset
                y0 = y * CELL_HEIGHT + offset
                r = self.create_rectangle( x0, y0,
                                           x0 + CELL_WIDTH,
                                           y0 + CELL_HEIGHT,
                                           fill='white',
                                           width=0)
                self.Field[x][y]=r

        # PAUSEの文字
        TEXT_font = ('Helvetica','60','bold')
        TEXT_color = 'red'
        self.pause_text = self.create_text( self.BOARD_WIDTH/2, 100,
                                            text='PAUSE',
                                            font=TEXT_font,
                                            fill=TEXT_color,
                                            justify=Tk.CENTER)
        self.HidePauseText()

    def SetColor( self, x, y, value ):
        #print "SetColor:"
        if x < 0: return
        if x >= FIELD_WIDTH: return
        if y < 0: return
        if y >= FIELD_HEIGHT: return
        #print "(%d, %d)=>%d" % (x, y, value)
        if value == 1:
            self.itemconfigure( self.Field[x][y], fill='black')
        elif value == 2:
            self.itemconfigure( self.Field[x][y], fill='gray')
        elif value == 3:
            self.itemconfigure( self.Field[x][y], fill='slate gray')
        else:
            self.itemconfigure( self.Field[x][y], fill='white')

    # PAUSE中にPAUSEの文字を表示する
    def ShowPauseText( self ):
        self.itemconfigure( self.pause_text, state=Tk.NORMAL )

    # PAUSEが終わるときにPAUSEの文字を非表示にする
    def HidePauseText( self ):
        self.itemconfigure( self.pause_text, state=Tk.HIDDEN )

# 次のピースを表示する画面 View
class NextPieceBoard( Tk.Canvas ):
    # 次のピースを表示する場所のサイズ
    NEXT_BOARD_WIDTH = PIECE_WIDTH * CELL_WIDTH
    NEXT_BOARD_HEIGHT = PIECE_HEIGHT * CELL_HEIGHT
    margin = 2
    def __init__( self, master ):
        Tk.Canvas.__init__( self, master, relief=Tk.RAISED, bd=self.margin,
                            bg='white',
                            width=self.NEXT_BOARD_WIDTH,
                            height=self.NEXT_BOARD_HEIGHT )
        self.RectArray = [[0 for y in range(PIECE_HEIGHT)]
                          for x in range(PIECE_WIDTH)]
        #print self.RectArray
        offset = self.margin * 2 + 3
        for x in range( PIECE_WIDTH ):
            for y in range( PIECE_HEIGHT ):
                x0 = x * CELL_WIDTH + offset
                y0 = y * CELL_HEIGHT + offset
                r = self.create_rectangle( x0, y0,
                                           x0 + CELL_WIDTH,
                                           y0 + CELL_HEIGHT,
                                           fill='white',
                                           width=0)
                self.RectArray[x][y]=r

    def DisplayPiecePattern( self, pattern ):
        p = pattern
        for x in range( PIECE_WIDTH ):
            for y in range( PIECE_HEIGHT ):
                # print "p[%d][%d] = %d" % (x, y, p[x][y])
                if p[x][y] == 1:
                    # print "p[%d][%d] = red" % (x, y)
                    self.itemconfigure( self.RectArray[x][y], fill='black' )
                else:
                    # print "p[%d][%d] = blue" % (x, y)
                    self.itemconfigure( self.RectArray[x][y], fill='white' )

# ゲーム全体 Controller
class Frame( Tk.Frame ):
    def __init__( self, master=None ):
        Tk.Frame.__init__( self, master )
        self.master.title( 'Tetris' )

        #########################################
        # board frame
        self.bframe = Tk.Frame( self, bd=1, relief=Tk.RIDGE )
        self.bframe.pack( side=Tk.LEFT, padx=10, pady=10 )
        self.board = Board( self.bframe )
        self.board.pack( side=Tk.LEFT, padx=0, pady=0 )

        #########################################
        # frame start
        frame = Tk.Frame( self, bd=1, relief=Tk.RIDGE )
        # exit button
        self.btExit = Tk.Button( frame, text='Exit', command=self.exitGame )
        self.btExit.pack( anchor=Tk.E, padx=10, pady=10 )
        # replay (not start) button
        #self.btReplay = Tk.Button( frame, text='Replay',
        #                           command=self.replayGame )
        #self.btReplay.pack( anchor=Tk.E, padx=10, pady=0 )
        # start button
        self.btStart = Tk.Button( frame, text='Start', command=self.startGame )
        self.btStart.pack( anchor=Tk.E, padx=10, pady=0 )
        # next piece display
        self.next_piece = NextPieceBoard( frame )
        self.next_piece.pack( anchor = Tk.E, padx=10, pady=10 )
        TEXT_font = ('Helvetica','12','italic')
        NUM_font = ('Helvetica','24','bold')
        # time text
        self.time_text = Tk.Label( frame, text='PLAY TIME', font=TEXT_font )
        self.time_text.pack( anchor = Tk.E, padx=10, pady=0 )
        # timer
        self.time_box = Tk.Label( frame, text='00\'00\"', font=NUM_font,
                                  bd=1, relief=Tk.RIDGE )
        self.time_box.pack( anchor = Tk.E, padx=10, pady=0 )
        frame.pack( side=Tk.RIGHT, padx=10, pady=10 )

        # key bind
        self.bind( "", self.keyPressed )
        self.bind( "", self.keyLeft )
        self.bind( "", self.keyRight )
        self.bind( "", self.keyUp )
        self.bind( "", self.keyDown )
        self.focus_set()
                    
        self.tetris = Tetris( self )

        self.processing_down = False
        self.timerEventCount = 0
        self.timerCount = 0

        self.tetris.PrepareStartGame()

    # exit button
    def exitGame( self ):
        self.tetris.playing = False
        sys.exit()

    # start button
    def startGame( self ):
        self.tetris.playing = True
        # ボタンは押せなくする
        self.DisableStartButton()
        #self.DisableReplayButton()
        # カウンタをリセットしてタイマーをスタートする
        self.timerEventCount = 0
        self.timerCount = 0
        # これ以降,timerEventが0.5秒に一回呼び出される
        self.after( 500, self.timerEvent )

    # replay
    def replayGame( self ):
        self.EnableStartButton()
        self.tetris.PrepareStartGame()
        self.timerEventCount = 0
        self.timerCount = 0
        self.DisplayTime( 0, 0 )

    # 0.5秒ごとに呼び出される
    def timerEvent( self ):
        if self.tetris.playing == False:
            return
        if self.tetris.pausing == True:
            return
        self.timerEventCount = self.timerEventCount + 1
        # 0.5秒に1回呼び出されるので2回→1秒に1回の点数アップ
        if self.timerEventCount % 2 == 0:
            self.timerCount = self.timerCount + 1
            self.DisplayTime( self.timerCount / 60, self.timerCount % 60 )
            # print "count %d" % self.timerEventCount
            # print "timer %d" % self.timerCount
        # ブロックを1段落とす
        self.keyDown( None )
        # 0.5秒後に自分を呼び出す
        self.after( 500, self.timerEvent )

    def endGame( self ):
        self.focus_set()
        self.replayGame()

    def DisableExitButton( self ):
        self.btExit.configure( state=Tk.DISABLED )

    def EnableExitButton( self ):
        self.btExit.configure( state=Tk.NORMAL )

    def DisableReplayButton( self ):
        #self.btReplay.configure( state=Tk.DISABLED )
        pass

    def EnableReplayButton( self ):
        #self.btReplay.configure( state=Tk.NORMAL )
        pass

    def DisableStartButton( self ):
        self.btStart.configure( state=Tk.DISABLED )

    def EnableStartButton( self ):
        self.btStart.configure( state=Tk.NORMAL )

    def DisplayTime( self, min=0, sec=0 ):
        self.time_box.configure( text="%02d\'%02d\"" % (min, sec) )

    # 以下はキー入力処理
    def keyPressed( self, event ):
        c = event.char
        # space: pause
        if self.tetris.playing == False:
            return
        if c == ' ':
            if self.tetris.pausing == False:
                print "PAUSE!!!"
                self.tetris.pausing = True
                # PAUSEを表示
                self.board.ShowPauseText()
            else:
                print "PAUSE END!!!"
                self.tetris.pausing = False
                # PAUSEを隠す
                self.board.HidePauseText()
                # 再度タイマーを開始
                self.after( 500, self.timerEvent )

    def keyLeft( self, event ):
        if self.tetris.playing == False:
            return
        if self.tetris.pausing == True:
            return
        # print "press Left"
        if self.tetris.CanMovePiece( move='left' ) == 0:
            self.tetris.MovePiece( move='left' )
            self.tetris.RedrawAllField()

    def keyRight( self, event ):
        if self.tetris.playing == False:
            return
        if self.tetris.pausing == True:
            return
        # print "press Right"
        if self.tetris.CanMovePiece( move='right' ) == 0:
            self.tetris.MovePiece( move='right' )
            self.tetris.RedrawAllField()

    # キーで呼ばれるのとタイマーで呼ばれるのが同時に重なる場合が
    # あるので再入防止している
    def keyDown( self, event ):
        if self.tetris.playing == False:
            return
        if self.tetris.pausing == True:
            return
        # print "press Down"
        if self.processing_down == True:
            return
        self.processing_down = True
        res = self.tetris.CanMovePiece( move='down' )
        if res == 0:
            self.tetris.MovePiece( move='down' )
            self.tetris.RedrawAllField()
        elif res == 2:
            self.tetris.FixPieceAndGetNextPiece()
        self.processing_down = False

    # 回転
    def keyUp( self, event ):
        if self.tetris.playing == False:
            return
        if self.tetris.pausing == True:
            return
        # print "press Up"
        if self.tetris.CanTurnPiece() == True:
            self.tetris.TurnPiece()
            self.tetris.RedrawAllField()

# テトリスゲームの Data
class Tetris:
    # ゲームフィールド
    field = [[0 for y in range(FIELD_HEIGHT)]
             for x in range(FIELD_WIDTH)]
    # 現在移動中のブロック
    piece = [[0 for y in range(PIECE_HEIGHT)]
             for x in range(PIECE_WIDTH)]
    # ブロックの左上端の座標(フィールドは左上が(x,y) = (0,0)でyが下向き)
    location = [0, 0]
    # 次のブロック
    nextPiece = [[0 for y in range(PIECE_HEIGHT)]
                 for x in range(PIECE_WIDTH)]
    # ブロックの全てのパターン
    piecePattern = [
        [[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]], # 0
        [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], # 1
        [[0,0,0,0],[0,1,1,1],[0,0,1,0],[0,0,0,0]], # 2
        [[0,0,0,0],[0,1,1,1],[0,1,0,0],[0,0,0,0]], # 3
        [[0,0,0,0],[0,1,0,0],[0,1,1,1],[0,0,0,0]], # 4
        [[0,0,0,0],[0,0,1,1],[0,1,1,0],[0,0,0,0]], # 5
        [[0,0,0,0],[0,1,1,0],[0,0,1,1],[0,0,0,0]]] # 6

    def __init__( self, parent ):
        self.main_frame = parent
        self.main_board = parent.board
        self.next_piece = parent.next_piece

    # ゲームの開始準備
    def PrepareStartGame( self ):
        self.playing = False
        self.pausing = False
        self.game_end = False

        # 盤面の初期化
        # 全部を白で塗る
        for y in range( FIELD_HEIGHT ):
            for x in range( FIELD_WIDTH ):
                self.field[x][y] = 0
        # 周囲1列を3で塗る
        for y in range( FIELD_HEIGHT ):
            self.field[0][y] = 3
            self.field[ FIELD_WIDTH - 1 ][y] = 3
        for x in range( FIELD_WIDTH ):
            self.field[x][ FIELD_HEIGHT - 1 ] = 3

        # 障害物を置いておく
        self.field[1][22] = 2
        self.field[2][22] = 2
        self.field[3][21] = 2
        self.field[4][21] = 2
        self.field[5][20] = 2
        self.field[6][20] = 2

        # 1個目のピースを準備する
        self.GetNextPiece( True )
        self.RedrawAllField()

    # 1行だけ消す処理
    def DeleteOneLine( self, line ):
        # 上から落とす
        for y in range(line, 1, -1):
            for x in range(1, FIELD_WIDTH-1):
                self.field[x][y] = self.field[x][y-1]
        self.RedrawAllField()

    # 埋まっている行を全て消す処理
    def DeleteFilledLines( self ):
        #print "Start Delete Line!!!"
        y = FIELD_HEIGHT - 2 # 一番下の行を除く
        deleted_line = 0 # 削除できた行の数
        all_clear = 0
        # 一番上の行まで確認
        count_sum = 0
        while y >= 0:
            # 1行に埋まってるセルの個数を数える
            #print "count line %d:" % y,
            count = 0
            for x in range(1, FIELD_WIDTH-1): # 両端を除く
                if self.field[x][y] == 2:
                    count = count + 1
            #print "count %d" % count
            # 全部が埋まっていたら行の削除処理
            if count == FIELD_WIDTH - 2: # 両端を除いた数
                # 削除できた場合にはyは増えない
                print "clear line %d" % y
                self.DeleteOneLine(y)
                deleted_line = deleted_line + 1
            else:
                count_sum = count_sum + count
                # 全部が埋まっていなかったら次の行へ行く
                y = y - 1
        print "end Delete Line!!! (%d lines)" % deleted_line
        print "%d cells left" % count_sum

        # count_sumが0だったらボーナス
        if count_sum == 0:
            all_clear == 1
            print '########################################'
            print '######## ALL CELLS CLEARED #############'
            print '########################################'

    # 現在のピースが落ち切ったので,ここに固定する
    # 行が埋まったら消す
    # 次のピースを開始する
    def FixPieceAndGetNextPiece( self ):
        # 現在のピースを現在の位置に固定する
        for x in range(PIECE_WIDTH):
            for y in range(PIECE_HEIGHT):
                if self.piece[x][y] == 1:
                    self.field[self.location[0]+x][self.location[1]+y] = 2
        # 埋まっている行を消す
        self.DeleteFilledLines()
        # 次のピース
        self.GetNextPiece( False )
        self.RedrawAllField()

    # next pieceの領域に次のピースを作成する
    def CreatePiece( self ):
        i = random.randint(0, len(self.piecePattern)-1)
        # print "[%d]" % i,
        self.nextPiece = self.piecePattern[i]
        # print "-->", self.nextPiece
        self.next_piece.DisplayPiecePattern( self.nextPiece )

    # next pieceの領域からピースを持ってきて一番上に置く
    # 既にここまでピースが積んであったらゲームオーバー判定
    def GetNextPiece( self, is_first ):
        if is_first:
            self.CreatePiece()
        #print self.nextPiece
        self.location[0] = 5
        self.location[1] = 0
        for y in range( PIECE_HEIGHT ):
            for x in range( PIECE_WIDTH ):
                # 次のピースを持ってくる
                self.piece[x][y] = self.nextPiece[x][y]
                # フィールドに置く
                self.main_board.SetColor( self.location[0]+x,
                                          self.location[1]+y,
                                          self.piece[x][y])
        # ゲーム終了判定
        for y in range( PIECE_HEIGHT ):
            # 内側のxのloopから抜けて来ていたら,ここでも抜ける
            if self.game_end == True:
                break
            for x in range( PIECE_WIDTH ):
                # フィールドに置いた瞬間,既にその場所にブロックがあったら
                if ( self.piece[x][y] == 1 and
                     self.field[self.location[0]+x][self.location[1]+y] == 2):
                    # 終わり
                    self.game_end = True
                    # タイマーでのloopを止める
                    self.playing = False
                    # 1個重なっていれば十分なので,ここで抜ける
                    break
        if self.game_end == False:
            # 終わっていなかったら続ける
            self.CreatePiece()
        else:
            # ゲームオーバー処理の開始
            print "GAME END !!!"
            self.main_frame.endGame()

    def MoveOrientation( self, move ):
        xmove = ymove = 0
        if move == 'down':
            ymove = 1
        elif move == 'left':
            xmove = -1
        elif move == 'right':
            xmove = 1
        return ( xmove, ymove )

    def MovePiece( self, move ):
        ( xmove, ymove ) = self.MoveOrientation( move )
        self.location[0] = self.location[0] + xmove
        self.location[1] = self.location[1] + ymove
        
    def TurnPiece( self ):
        turnedPiece = [[0 for y in range(PIECE_HEIGHT)]
                       for x in range(PIECE_WIDTH)]
        for x in range(PIECE_WIDTH):
            for y in range(PIECE_HEIGHT):
                turnedPiece[y][PIECE_HEIGHT-1-x] = self.piece[x][y]
        self.piece = turnedPiece

    # 移動した先を計算して,fieldとかぶらないか,はみ出ないかを見る
    # return 1: 左右の壁に当たって移動できない
    # return 2: 積んであるフィールド上のセルと重なるか,
    #           下に当たっているので,これで固定する
    def CanMovePiece( self, move='down' ):
        ( xmove, ymove ) = self.MoveOrientation( move )
        ( x0, y0 ) = ( self.location[0], self.location[1] )
        # フィールド上のセルと重なっている場合
        flag = 0
        for x in range(PIECE_WIDTH):
            for y in range(PIECE_HEIGHT):
                if ( self.piece[x][y] == 1 and
                     self.field[x0+x+xmove][y0+y+ymove] >= 1 ):
                    flag = 1
                    #print "move NG (4) (%d,%d)->(%d,%d)" % (
                    #    self.location[0]+x, self.location[1]+y,
                    #    self.location[0]+x+xmove, self.location[1]+y+ymove )
                    break
        if flag == 1:
            if ymove == 1:
                return 2 # 下に動けない場合はこれで固定
            else:
                return 1 # 左右ぶつかっている場合は,動けないだけ
        # 以上のチェックに引っかからない場合は動ける
        return 0

    # 回転した先を計算して,fieldとかぶらないかを見る
    # True: 回転できる
    def CanTurnPiece( self ):
        turnedPiece = [[0 for y in range(PIECE_HEIGHT)]
                       for x in range(PIECE_WIDTH)]
        for x in range(PIECE_WIDTH):
            for y in range(PIECE_HEIGHT):
                turnedPiece[y][PIECE_HEIGHT-1-x] = self.piece[x][y]
        # 確認
        flag = 0
        for x in range(PIECE_WIDTH):
            for y in range(PIECE_HEIGHT):
                # 添字が範囲を越えているかどうかの判定
                xx = self.location[0] + x
                yy = self.location[1] + y
                if ( xx < 0 or xx >= FIELD_WIDTH ):
                    flag = 1
                    print "turn NG (1)"
                    break
                if ( yy < 0 or yy >= FIELD_HEIGHT ):
                    flag = 1
                    print "turn NG (2)"
                    break
                # 重なっている場合
                if ( turnedPiece[x][y] == 1 and
                     self.field[xx][yy] >= 1 ):
                    flag = 1
                    print "turn NG (3)"
                    break
        if flag == 1:
            return False
        else:
            return True

    # 再描画
    def RedrawAllField( self ):
        # ゲームの終了後は実行しない
        # 開始前は表示する
        if self.game_end == True:
            return
        #print "RedrawAllField"
        # まず固定セル
        for x in range( FIELD_WIDTH ):
            for y in range( FIELD_HEIGHT ):
                self.main_board.SetColor( x, y, self.field[x][y] )
        # 次に移動中のpiece
        for x in range( PIECE_WIDTH ):
            for y in range( PIECE_HEIGHT ):
                xx = x + self.location[0]
                yy = y + self.location[1]
                if xx < 0: pass
                if xx >= FIELD_WIDTH: pass
                if yy < 0: pass
                if yy >= FIELD_HEIGHT: pass
                # 描かれていない場所まで塗らない
                if self.piece[x][y] == 1:
                    self.main_board.SetColor( xx, yy, self.piece[x][y] )

##------
if __name__ == '__main__':
    f = Frame()
    f.pack()
    f.mainloop()

# EOF

0 件のコメント:

コメントを投稿