2019年1月6日日曜日

第2段 別のサイトのWxPythonテトリスのコードを読んでみた。


テトリス作成


blocks.png
ブロックの画像はなかったので自作しました。

参考サイト

http://euphorie.sakura.ne.jp/junk/page_python_teto.html
Python 入門


遊び方

(テトリスを作って遊び方は "スペース" でゲーム開始, "←", "→", "↓" で移動, "ENTER" で回転です.みる)
移動可能なテトリミノと固定されたテトリミノのそれぞれのデータがあり, 移動可能テトリミ
ノはキーまたはタイマで移動されます. 移動可能テトリミノは落下して移動不能になれば, 固
定テトリミノにコピーされる流れです.


移動ブロックについて

移動ブロックのセルに各番号を割り当てています。
下は参考サイトより引用しています。
移動テトリミノは, 図 3 のようにテトリミノ中心を 0 番となるように相対的に番号を割り当てます.
python_teto_blocks.png
図 3 移動可能テトリミノのデータ構造


各変数について

fixed_: わかりやすく背景情報と呼ぶことにします。
            要素数 GAME_H * GAME_W = 21*12 の一次配列
壁(-1)、固定ブロック(ブロックのid 1~7)、
           移動ブロックが移動できる領域(0) の各数字が入っています。

pos: 背景情報位置と呼ぶ事にする。0より始まります。
各関数の変数として出て来ます。例えば  pos = j * GAME_W + i で j=2 i= 5なら
上から2段目の左から6番目の位値を指します。(0より始まっているので)

背景情報位置    最上段 0~11
                              次段  12~23
これが21段続く

self.pos_ ブロックの位値。正確にはブロックのセル番号0に位値になります。
初期位置はself.pos_ = GAME_W + GAME_W / 2 - 1      #12+12/2-1 です。

self.id_ 1 ~ 7 ブロックのIDを示します。



移動ブロックの表示の概要

pos 背景情報位置 左上が0で右端が12 下の段左端が13と続きます。
このコードでは以下の順でブロック画像の表示されています。

block()で以下のように処理されます。
pos = j * GAME_W + i
for block in self.floating_:
if self.pos_ + block == pos:
return self.id_

初期位置self.pos_ = 17として。
self.floating_= [0, 1, -GAME_W, 2]  (-GAME_W=-12) のブロックだとしたら
self.pos_ + block = 17, 18, 5, 19 の各値になります。
(floating_のリストの中身はブロックの各セルの番号になります。)

それがpos(0から始まっています)と一致しますと、ブロックのIDが返ります。
下に上から4段だけのブロックの表示領域を示しました。
上左端から順に0から数えていくと■の部分で、self.pos_ + block == pos 
となりブロックのIDが返される事になります。

□□□□□■□□□□□□  左右端、一番下部の□は壁となります。
□□□□□■■■□□□□  ウィンドウには表示されません。
□□□□□□□□□□□□
□□□□□□□□□□□□


次に
OnPaintで
for j in range(GAME_H - 1):
for i in range(GAME_W - 2):
b = self.game_.block(i + 1, j)
if b >= 1 and b <= len(self.blockBitmaps_):
dc.DrawBitmap(self.blockBitmaps_[b - 1],
BLOCK_BMP_SIZE * i, BLOCK_BMP_SIZE * j)

とループさせて ほぼ全てのposに対して、さらにそのブロックの一個一個の 
セルの番号に対して block()にて
self.pos_ + block == pos が成り立てばブロックのID(1~7)が返されるので
if b >= 1 and b <= len(self.blockBitmaps_):
がTrueとなり、DrawBitmapでブロック画像が表示されます。



落下操作

OnTimer→FallAndTest()→fall() 経由で
ブロックが落下可能ならfall()で、 self.pos_ += GAME_W とされます。
self.floating_= [0, 1, -GAME_W, 2]  (-GAME_W=-12) のブロックだとしたら
self.pos_ + block = 17, 18, 5, 19 の各値に、さらに+12される事となります。
これと一致するposは一段下のposとなります。
故に、block() OnPaint()で一段下にブロックが表示される事となります。



底辺にぶつかった場合

fall()でこれ以上落下出来ないと判断されたら、fix()で移動ブロックを固定させています。
とはいっても、fixed_[]に移動ブロックのIDをいれているだけです。
そしてnext()で次のブロックが移動ブロックにセットされますので、現在のブロックの固定
されたブロックは、移動しません。
固定ブロックもblock()でIDを返しますので、OnPaint()でその画像が表示されます。



回転の操作

turn()がなぜ回転の動作をするのか、テストコードを使って確認しました。
結果、セル番号0で右回転を90度していました。
E:\MyBackups\goolgedrive\myprg_main\python_my_prg\wxpython\game_tetoris_two1_turn.py

# -*- coding: utf-8 -*-

GAME_H = 21 # 画面の高さ (番兵を含む)
GAME_W = 12 # 画面の幅 (番兵を含む)

blocks_ = [
[0, 1, -GAME_W, -GAME_W - 1],   # Z, green
[0, 1, -GAME_W, -GAME_W * 2],   # L, orange
[0, 1, -GAME_W, GAME_W + 1], # S, pink
[0, 1, -GAME_W, 2], # J, blue
[0, 1, -GAME_W, -GAME_W + 1],   # O, yellow
[-1, 0, 1, 2],   # I, red
[-1, 0, 1, -GAME_W] # T, light blue
]

def turn(floating_):
print
movable = True
rot = []
for b in floating_:
#round:丸める(ほぼ四捨五入をする)
v = int(round(float(b) / float(GAME_W))) # 回転先の x 座標
w = b - v * GAME_W   # 回転先の y 座標
p = w * GAME_W - v   # 回転先の位置
rot.append(p)
print "b",b,
print "   ",
#print "v",v
#print "w",w
print "p",p


for bks in blocks_:
turn(bks)

#出力結果 
#初期位置での元ブロックbと回転ブロックpを表示する
#
#b 0      p 0     □□□□■■□□□□□□ □□□□□□■□□□□□ 
#b 1      p 12   □□□□□■■□□□□□ □□□□□■■□□□□□ 
#b -12   p 1    □□□□□□□□□□□□    □□□□□■□□□□□□
#b -13   p -11 □□□□□□□□□□□□    □□□□□□□□□□□□
#
#              ■      
#b 0      p 0  □□□□□■□□□□□□ □□□□□□□□□□□□ 
#b 1      p 12  □□□□□■■□□□□□ □□□□□■■■□□□□
#b -12   p 1    □□□□□□□□□□□□   □□□□□■□□□□□□
#b -24   p 2    □□□□□□□□□□□□   □□□□□□□□□□□□
#             
#b 0      p 0   □□□□□■□□□□□□  □□□□□□□□□□□□
#b 1      p 12   □□□□□■■□□□□□  □□□□□■■□□□□□
#b -12   p 1     □□□□□□■□□□□□  □□□□■■□□□□□□
#b 13    p 11   □□□□□□□□□□□□  □□□□□□□□□□□□
#
#b 0     p 0    □□□□□■□□□□□□ □□□□□□□□□□□□
#b 1     p 12  □□□□□■■■□□□□ □□□□□■■□□□□□
#b -12   p 1   □□□□□□□□□□□□   □□□□□■□□□□□□
#b 2     p 24  □□□□□□□□□□□□    □□□□□■□□□□□□
#               
#b 0      p 0    □□□□□■■□□□□□  □□□□□□□□□□□□
#b 1      p 12  □□□□□■■□□□□□  □□□□□■■□□□□□
#b -12   p 1    □□□□□□□□□□□□  □□□□□■■□□□□□
#b -11   p 13  □□□□□□□□□□□□  □□□□□□□□□□□□
#
#b -1    p -12  □□□□□□□□□□□□ □□□□□■□□□□□□
#b 0     p 0      □□□□■■■■□□□□ □□□□□■□□□□□□
#b 1     p 12    □□□□□□□□□□□□   □□□□□■□□□□□□
#b 2     p 24    □□□□□□□□□□□□   □□□□□■□□□□□□
#               
#b -1    p -12  □□□□□■□□□□□□  □□□□□■□□□□□□
#b 0     p 0      □□□□■■■□□□□□  □□□□□■■□□□□□
#b 1     p 12    □□□□□□□□□□□□  □□□□□■□□□□□□
#b -12    p 1    □□□□□□□□□□□□  □□□□□□□□□□□□




リストのスライスがわかりませんでしたので、検証してみました。

def remove(self, line):
# 指定列を削除
#横一要素、縦5要素のfixed_のデータとして、それが[12345]として3が横いっぱい
#となったとしてそれを削除して、一番目に0を加えると[01245]となる
#ここのやり方は正しい
del self.fixed_[line * GAME_W: (line + 1) * GAME_W]

# 1 列先頭について
top = []
for i in range(GAME_W):
if i == 0 or i == GAME_W - 1:
top.append(-1)   # 左右端に(-1)壁を作成

else:
top.append(0)    # それ以外は移動領域を作成
self.fixed_[0: 0] = top
#fixed_[0: 0] 空のリストを作成している
#ではなく、リストのスライスによる値の代入が行われている
#fixed_の0の位値、つまりリストのいち番目にtopの値を入れている

#>>> ls =[0,0,0,0]
#>>> ls[0:0]
#[]
#>>> ls
#[0, 0, 0, 0]
#>>> top =[1]
#>>> ls[0: 0] = top
#>>> ls
#[1, 0, 0, 0, 0]


テトリスのコード

E:\MyBackups\goolgedrive\myprg_main\python_my_prg\wxpython\game_tetoris_two1.py

import wx
import random

BLOCK_BMP_SIZE = 20 # 1 ブロックのビットマップ サイズ
GAME_PANEL_W = BLOCK_BMP_SIZE * 10
GAME_PANEL_H = BLOCK_BMP_SIZE * 20
GAME_H = 21 # 画面の高さ (番兵を含む)
GAME_W = 12 # 画面の幅 (番兵を含む)

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 Game:
        # コンストラクタ
        def __init__(self):
                # ブロック形状の定義
# ブロックのセルに番号を打っている
                self.blocks_ = [
                        [0, 1, -GAME_W, -GAME_W - 1],   # Z, green
                        [0, 1, -GAME_W, -GAME_W * 2],   # L, orange
                        [0, 1, -GAME_W, GAME_W + 1], # S, pink
                        [0, 1, -GAME_W, 2], # J, blue
                        [0, 1, -GAME_W, -GAME_W + 1],   # O, yellow
                        [-1, 0, 1, 2],   # I, red
                        [-1, 0, 1, -GAME_W] # T, light blue
                ]

                # 固定ブロックの初期化
                #下端左右端を-1に、それ以外を0にする
                #fixed_: 壁(-1)、固定ブロック(ブロックのid 1~7)、
                #移動ブロックが移動できる領域(0)を示している背景情報
                #と思えばいいか。
                self.fixed_ = []
                for j in range(GAME_H):
                        for i in range(GAME_W):
                                if j == GAME_H - 1:
                                        self.fixed_.append(-1)   # 下端
                                elif i == 0 or i == GAME_W - 1:
                                        self.fixed_.append(-1)   # 左右端
                                else:
                                        self.fixed_.append(0)    # それ以外
             
                ## フローティング ブロックの初期化
                #ブロックの形状からどれか一つをランダムに選ぶ
                #randint:1~7 len(self.blocks_) までをランダムに返す。1と7を含む
                self.id_ = random.randint(1, len(self.blocks_))

                #floating にブロックの形状の一つをセットする
                self.floating_ = self.blocks_[self.id_ - 1]
                #pos_:ブロックの位置、多分上左端を0として順に番号を打っていったものだ                  #ろう
                #下はこれだと、上より2段目の真ん中になる。-1されているのはpos_が0より
                #始まるため
                self.pos_ = GAME_W + GAME_W / 2 - 1        #12+12/2-1

        # クリアする
        # fixed_の壁以外を0にして移動ブロックが移動できるようにする
        def clear(self):
                for j in range(GAME_H - 1):
                        for i in range(1, GAME_W - 1):
                                self.fixed_[j * GAME_W + i] = 0

        # 左右に移動する (引数 delta, -1: 左, 1: 右, 戻り値, True: 移動完了,
        # False: 移動不 可)
        def move(self, delta):
                # 移動可能か判定
                movable = True
                for b in self.floating_:
                        pos = self.pos_ + b + delta
                        if pos >= 0 and self.fixed_[pos] != 0:
                                movable = False
                                break

                # 移動可能であるときは移動
                if movable:
                        self.pos_ = self.pos_ + delta
                return movable

     
        # 回転する (戻り値, True:回転可 回転後のセル番号を返す , False: 回転不可)
        # なぜ以下の計算で回転のセル番号がわかるのか解らないが
        #game_tetoris_two1_turn.py でした所合っていた。
        #90度右回転を一回させている
        def turn(self):
                movable = True
                rot = []
                for b in self.floating_:
                        #round:丸める(ほぼ四捨五入をする)
                        v = int(round(float(b) / float(GAME_W))) # 回転先の x 座標
                        w = b - v * GAME_W   # 回転先の y 座標
                        p = w * GAME_W - v   # 回転先の位置に当たるセル番号
                        rot.append(p)
                     
                        pos = self.pos_ + p
                        if pos >= 0 and self.fixed_[pos] != 0:
                                movable = False
                                break

                if movable:
                        self.floating_ = rot
                return movable


        # 落下移動する (戻り値, True: 移動完了, False: 移動不可)
        def fall(self):
                # 落下可能か判定
                movable = True
                for b in self.floating_:
                        pos = self.pos_ + b + GAME_W
                        #posが0より小さくなる事があるのだろうか?
                        #posが0以上でpos位置が壁であったら
                        if pos >= 0 and self.fixed_[pos] != 0:
                                #セル一個でも落下不可能であればブロックとして落下不能になる
                                movable = False
                                break

                # 落下可能であれば落下
                if movable:
                        self.pos_ = self.pos_ + GAME_W
                return movable


        # 現在のフローティング ブロックを固定する
        def fix(self):
                # ブロック固定
                #ブロックのセル位置の背景情報をブロックid番号にする
                for b in self.floating_:
                        self.fixed_[self.pos_ + b] = self.id_ #id 1~7


        # 次のブロックを落とし始める (戻り値, True: 開始完了, False: 開始不能)
        #次のブロックを落とせる時は、ブロック、初期値をセットする
        def next(self):
                # 次のブロック決定
                nextId = random.randint(1, len(self.blocks_))
                nextFloating = self.blocks_[nextId - 1]
                nextPos = GAME_W + GAME_W / 2 - 1

                # 次のブロックを落とし始めることができるか判定
                #このファイルによくあるテクニックを使っている
                #次のブロックの各セルの背景情報fixed_が0でない(壁がある)
                #またはpos>=0の時、つまりセル番号-24、初期位置17ではpos=-7となり
                #セルの表示ができない時、ブロックを落とせないとして
                #Falseを返す
                starting = True
                for b in nextFloating:
                        pos = nextPos + b
                        if pos >= 0 and self.fixed_[pos] != 0:
                                starting = False
                                break

                # 落とし始めることができるときは開始
                if starting:
                        self.id_ = nextId
                        self.floating_ = nextFloating
                        self.pos_ = nextPos
                return starting

        # 固定ブロックが 1 列がそろっていることの判定
        # (引数 line, 列番号, 戻り値: True: そろっている, False: そろっていない)
        def full(self, line):
                #fixed_のリストのスライス 
                #fixed_[a:b] インデックスaからb-1までの要素を取り出す(一列分になる)
                for b in self.fixed_[line * GAME_W: (line + 1) * GAME_W]:
                        if b == 0: #bが0なら移動領域である
                                return False
                return True

        # 固定ブロックの 1 列削除 (引数 line, 列番号)
        def remove(self, line):
                # 指定列を削除
                #横一要素、縦5要素のfixed_のデータとして、それが[12345]として
                #3が横いっぱい
                #となったとしてそれを削除して、一番目に0を加えると[01245]となる
                del self.fixed_[line * GAME_W: (line + 1) * GAME_W]
             
                # 1 列先頭について
                top = []
                for i in range(GAME_W):
                        if i == 0 or i == GAME_W - 1:
                                top.append(-1)   # 左右端に(-1)壁を作成
                             
                        else:
                                top.append(0)    # それ以外は移動領域を作成
                self.fixed_[0: 0] = top


        # フローティングブロックの場合はブロックのidを
        #そうでない時は、固定ブロックの情報を返す
        # 指定した座標 (i, j) の値を返す
        def block(self, i, j):
                pos = j * GAME_W + i

                # フローティング ブロックであれば、ブロックのid(1~7)を返す
                #pos_:フローティングブロックの位置
                #例えば、floating_= [0, 1, -GAME_W, -GAME_W - 1] となる
                #floating_の要素はそのセルの位置をしめしているので
                for block in self.floating_:
                        if self.pos_ + block == pos:
                                return self.id_
             
                # フローティング ブロックでないときは固定ブロックされたブロックのid
                #を返すか、壁側は-1 その内側は0を返す。
                return self.fixed_[pos]

     
class GamePanel(wx.Panel):
        def __init__(self, parent, status):
                wx.Panel.__init__(self, parent, size=(GAME_PANEL_W, GAME_PANEL_H),
                                style=wx.ALIGN_LEFT)
             
                # ゲームを初期化
                self.game_ = Game()
                self.playing_ = False
                self.score_ = 0

                #画像の縦横がBLOCK_BMP_SIZEのものを、横に7つ並べたものを用意する。
                #その画像のビットマップをRectで始点座標、終点座標を引数にして
#切り取り、ブロックごとの画像にしている
                #ブロックの形状によってどれが一つの画像をえらぶ
#
#"blocks.png"を作成しようとして
                #Windowsのペイントによって縦横BLOCK_BMP_SIZEのものを、
                #7つコピーす
                #るとincorrect sbit chunk length のエラーを起こす
#
                self.blockBitmaps_ = []
                # ビットマップ データをロードする
                blockBitmap = wx.Bitmap("blocks.png")
                for i in range(7):
                        self.blockBitmaps_.append(blockBitmap.GetSubBitmap(
                                wx.Rect(BLOCK_BMP_SIZE * i, 0, BLOCK_BMP_SIZE,                                                             BLOCK_BMP_SIZE)))

                #テトリスとは関係ないがブロック画像をを保存してみる
                self.blockBitmaps_[0].SaveFile( "blocks_save.png",                                                                                                         wx.BITMAP_TYPE_PNG )
                self.backgroundBitmap_ = wx.Bitmap("back.png")

                # タイマーの設定
                self.timer_ = wx.Timer(self, 1)
             
                # ステータス バーの設定
                self.status_ = status
                status.SetStatusText("SPACE key to start the game!")

                # イベントハンドラの設定
                self.Bind(wx.EVT_PAINT, self.OnPaint)
                self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
                self.Bind(wx.EVT_TIMER, self.OnTimer)
     
        #スペースキーが押されたら→Start()
        # ゲーム開始
        def Start(self):
                self.game_.clear()
                self.timer_.Start(250)
                self.playing_ = True
                self.score_ = 0
                status.SetStatusText("Score: " + str(self.score_))

        # ゲームオーバー
        def Over(self):
                self.timer_.Stop()
                #以下の行でOnTimerとOnKeyDownが無効になるので、
                #上の行はいらにのでは?
                self.playing_ = False
                status.SetStatusText("Score: " + str(self.score_) + ".
                                                                   SPACE key to restart!")

        def FallAndTest(self):
                #fall:落下出来ない時False 出来た時Trueを返する
                #fall:落下できる時はブロックの位置の一段下げる
                #Trueの場合ブロック位置self.pos_ を変更している
                if not self.game_.fall():
                        # 落下できないとき, 移動ブロックを固定
                        self.game_.fix()

                        # そろった列を削除
                        #full:列が揃っていればTrue を返す
                        lines = 0
                        for line in range(GAME_H - 1):
                                if self.game_.full(line):
                                        self.game_.remove(line)
                                        lines = lines + 1
                     
                        # スコア加算
                        if lines != 0:
                                #妙なスコアの計算、単に100*lineではいけないのか
                                self.score_ = self.score_ + 100 * pow(2, lines - 1)
                                status.SetStatusText("Score: " + str(self.score_))

                        # 次のブロックを落とし始める
                        # nest():落とす事ができたら、次のブロック、初期位置をセットして
                        #Trueを返す
                        if not self.game_.next():
                                # 落とし始めることができないときは, ゲームオーバー
                                self.Over()

        def OnPaint(self, evt):
                dc = wx.PaintDC(self)
                dc = wx.BufferedDC(dc)  # ダブルバッファ
                #0, 0 に背景画像をセットする
                dc.DrawBitmap(self.backgroundBitmap_, 0, 0)
                #game_: Gameクラスのオブジェクト
                for j in range(GAME_H - 1):
                        #なんで-2なのか? i=1 ~ GAME_W-1 ならわかるが
                        #一段目のposの移動領域は1~10なのでこれでいいのだ
                        for i in range(GAME_W - 2):
                                #block():
                                #移動ブロックの場合id(1~7)、
                                #固定ブロック(1~7)(fixed_[pos]の値) 壁-1 その内側0
                                b = self.game_.block(i + 1, j)
                                #bがブロックidのどれかであれば、そのidのブロックの
                                #画像を i jの位置に表示する。
                                #移動ブロック、固定ブロックとも表示
                                #len(saelf.blockBitmaps_)=7
                                if b >= 1 and b <= len(self.blockBitmaps_):
                                        dc.DrawBitmap(self.blockBitmaps_[b - 1],
                                        BLOCK_BMP_SIZE * i, BLOCK_BMP_SIZE * j)


        #Refresh()によって、OnPaint()に移動する
        def OnKeyDown(self, evt):
                #スペースキーを押せばplaying_がTrueになる
                if self.playing_:
                        if evt.KeyCode == wx.WXK_LEFT:
                                self.game_.move(-1)
                                self.Refresh(False)
                        elif evt.KeyCode == wx.WXK_RIGHT:
                                self.game_.move(1)
                                self.Refresh(False)
                        elif evt.KeyCode == wx.WXK_DOWN:
                                self.FallAndTest()
                                self.Refresh(False)
                        elif evt.KeyCode == wx.WXK_RETURN:
                                # 回転させる (戻り値, True:回転可 回転後のセル番号を返す ,
                                #      False: 回転不可)
                                self.game_.turn()
                                self.Refresh(False)
                else:
                        if evt.KeyCode == wx.WXK_SPACE:
                                self.Start()
     
        def OnTimer(self, evt):
                #スペースキーを押すことのよりplaying_がTrueになる
                if self.playing_:
                        self.FallAndTest()
                        self.Refresh(False)


if __name__ == "__main__":
        app = MyApp()
        frame = wx.Frame(None, wx.ID_ANY, "Tetris",
                        style=wx.MINIMIZE_BOX | wx.CLOSE_BOX | wx.SYSTEM_MENU | wx.CAPTION)
        status = frame.CreateStatusBar()
        gamePanel = GamePanel(frame, status)

        #SetClientSizeWH: frameのタイトルバー、ステータスバーなどを除いた領域
        #を設定していると思う
        #gamePanel.Size = (200,400)だが print"frame.Size", frame.Sizeを実行すると
        #出力結果(210,449)となりおかしい
        #frame.SetClientSizeWH(gamePanel.Size.width + 4, gamePanel.Size.height + 4)
        #  SUNKEN_BORDER の幅は 2 px らしい

        #SUNKEN_BORDER というスタイルだとClidenSizeが少なくなると思って+4
        #されているようだが
        #代わりに以下の2行のどちらかのコードをいれてみたら、これでも行けそう
        #frame.SetClientSize((200, 400))
        frame.SetClientSizeWH(200, 400)

        frame.Show()
        app.MainLoop()









0 件のコメント:

コメントを投稿

About

参加ユーザー

連絡フォーム

名前

メール *

メッセージ *

ブログ アーカイブ

ページ

Featured Posts