0
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

テトリス風落ち物パズルを作る part05 SRSとロックダウン(ワールドルールの適応)

Last updated at Posted at 2024-06-16

前回はキー入力とミノの落下処理を作った。
今回はワールドルールの一つであるSRSとロックダウンを実装していく。

SRS: Super Rotation System

  • ミノが壁とか床に接している状態でも回転できるように位置の補正を行う処理
  • Tspin Tripleができるのはこのシステムのおかげ
  • どう補正されるの解説とかは色々複雑なので以下のページ等を参照してほしい

ロックダウン

  • 今まで設置判定適当だったけどある程度ワールドルールに準拠したい
  • 地面に設置した判定から、一定時間動かす余地を与えてから設置(これは今まで実装していた)
  • 地面に設置した状態回転や移動するとミノの固定までの時間が一旦リセットされる(今回これを追加)1
  • 何回もやれてしまうと無限ループが起きるためリセットは大体15回までに制限する

SRSの補正を考える

使用して要は「その位置でミノが回転できないなら、ミノの位置を補正した状態なら回転可能か順番にチェックする」ということ。
そして、その補正の仕方が「それぞれの向きから右、左回転するとき」、「Iミノかそれ以外のとき」で違うという話。

だから全パターンをjsonファイルなどで網羅すればヨシ!

srs_pattern.json
{
    "SRScommon":{
        "offset_ptn_lst":[
            [[ 0, 0],[ 0, 0],[ 0, 0],[ 0, 0],[ 0, 0]],
            [[ 0, 0],[-1, 0],[-1,-1],[ 0, 2],[-1, 2]],
            [[ 0, 0],[ 1, 0],[ 1,-1],[ 0, 2],[ 1, 2]],
            [[ 0, 0],[ 1, 0],[ 1, 1],[ 0,-2],[ 1,-2]],
            [[ 0, 0],[-1, 0],[-1, 1],[ 0,-2],[-1,-2]]
        ],
        "ptn_select_mat":[
            [ 0, 1, 0, 2],
            [ 3, 0, 3, 0],
            [ 0, 1, 0, 2],
            [ 4, 0, 4, 0]
        ]
    },
    "SRSminoI":{
        "offset_ptn_lst":[
            [[ 0, 0],[ 0, 0],[ 0, 0],[ 0, 0],[ 0, 0]],
            [[ 0, 0],[-1, 0],[ 2, 0],[-1,-2],[ 2, 1]],
            [[ 0, 0],[-2, 0],[ 1, 0],[-2, 1],[ 1,-2]],
            [[ 0, 0],[ 2, 0],[-1, 0],[ 2,-1],[-1, 2]],
            [[ 0, 0],[ 1, 0],[-1, 0],[ 1, 2],[-2,-1]]
        ],
        "ptn_select_mat":[
            [ 0, 2, 0, 1],
            [ 3, 0, 1, 0],
            [ 0, 4, 0, 3],
            [ 4, 0, 2, 0]
        ]
    }
}

これをminoクラスに読ませてタプルやPosに変換する。

mino.pyの更新(一部)
import json

srs_patterns = None
with open('srs_pattern.json', 'r') as f:
    srs_patterns = json.load(f)

# valuesメソッドを持つEnum (いろんなクラスで使うのでスーパークラス化した)
class VEnum(Enum):
    @classmethod
    def values(cls):
        # Order dictionary -> list
        return list(cls.__members__.values())

class SRS(VEnum):
    common = 'SRScommon', auto()
    minoI  = 'SRSminoI' , auto()
    def __init__(self, name, value):
        self._name = name
        self._value_ = value
        ptn = srs_patterns[name]
        self.offset_ptn_lst = tuple(tuple(Pos(*a) for a in A) for A in ptn["offset_ptn_lst"])
        self.ptn_select_mat = tuple(tuple(A) for A in ptn["ptn_select_mat"])
    def get_rotate_offsets(self, rot1, rot2):
        ptn_select = self.ptn_select_mat[rot1][rot2]
        for p in self.offset_ptn_lst[ptn_select]:
            yield p

class MinoType(VEnum):
    Z = 'Z', auto(), *get_mino_data(0), SRS.common
    S = 'S', auto(), *get_mino_data(1), SRS.common
    J = 'J', auto(), *get_mino_data(2), SRS.common
    L = 'L', auto(), *get_mino_data(3), SRS.common
    T = 'T', auto(), *get_mino_data(4), SRS.common
    O = 'O', auto(), *get_mino_data(5), SRS.common
    I = 'I', auto(), *get_mino_data(6), SRS.minoI
    def __init__(self, name, value, tile, shape, srs):
        self._name = name
        self._value_ = value
        self.tile = tile
        self.shape = tuple(tuple(s) for s in shape)
        self.shape_pos = tuple(tuple(Pos(*p) for p in s) for s in shape)
        self.srs = srs

テストコードでは表示を見やすいようにpprintを使用する

mino.py 更新(その2)
from pprint import pprint

# ---中略---

def main():
    for mino in MinoType.values():
        print(mino.name, mino.value, mino.tile)
    for mino in OtherTiles.values(): 
        print(mino.name, mino.value, mino.tile)
    # 追加
    for srs in SRS.values():
        print(srs)
        print(srs._name)
        print(srs.value)
        pprint(srs.offset_ptn_lst)
        pprint(srs.ptn_select_mat)

実行結果、すべてtupleとPosに変換できていそう

mino.py 実行結果
SRS.common
SRScommon
1
((Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0)),
 (Pos(x=0, y=0),
  Pos(x=-1, y=0),
  Pos(x=-1, y=-1),
  Pos(x=0, y=2),
  Pos(x=-1, y=2)),
 (Pos(x=0, y=0), Pos(x=1, y=0), Pos(x=1, y=-1), Pos(x=0, y=2), Pos(x=1, y=2)),
 (Pos(x=0, y=0), Pos(x=1, y=0), Pos(x=1, y=1), Pos(x=0, y=-2), Pos(x=1, y=-2)),
 (Pos(x=0, y=0),
  Pos(x=-1, y=0),
  Pos(x=-1, y=1),
  Pos(x=0, y=-2),
  Pos(x=-1, y=-2)))
((0, 1, 0, 2), (3, 0, 3, 0), (0, 1, 0, 2), (4, 0, 4, 0))
SRS.minoI
SRSminoI
2
((Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0), Pos(x=0, y=0)),
 (Pos(x=0, y=0), Pos(x=-1, y=0), Pos(x=2, y=0), Pos(x=-1, y=-2), Pos(x=2, y=1)),
 (Pos(x=0, y=0), Pos(x=-2, y=0), Pos(x=1, y=0), Pos(x=-2, y=1), Pos(x=1, y=-2)),
 (Pos(x=0, y=0), Pos(x=2, y=0), Pos(x=-1, y=0), Pos(x=2, y=-1), Pos(x=-1, y=2)),
 (Pos(x=0, y=0), Pos(x=1, y=0), Pos(x=-1, y=0), Pos(x=1, y=2), Pos(x=-2, y=-1)))
((0, 2, 0, 1), (3, 0, 1, 0), (0, 4, 0, 3), (4, 0, 2, 0))

回転処理にSRSを組み込む

追加したSRSのデータを回転・移動処理のメソッドの中で使用するようにする。

main.pyの更新
def move_and_rotate_mino(self):
    # ---中略---
    if vpos.x == 0 and vpos.y == 0 and vr == 0:
        pass
    elif self.can_mino_move(self.mino, vpos, vr):
        self.mino.move(vpos, vr)
    elif vr != 0: # 追加
        # SRSの実行
        rot1 = self.mino.r % 4
        rot2 = (self.mino.r + vr) % 4
        for p in self.mino.m.srs.get_rotate_offsets(rot1, rot2):
            if self.can_mino_move(self.mino, vpos + p, vr):
                self.mino.move(vpos + p, vr)
                break

ロックダウンの追加

ロックダウンの実装は単純で設置後に回るカウンターのリセット処理をメソッドに入れて

main.py lock_downメソッドの追加

class Game:
    # ---中略---
    lockdown_cnt = 0 # 追加
    # ---中略---
    def lock_down(self):
        if self.wait_count == WAIT:
            return
        if self.lockdown_cnt < 15:
            self.lockdown_cnt += 1
            self.g_cnt = 0
            self.wait_count = WAIT

移動処理で左右に動かしたり回転できたりした場合にそのメソッドを呼ぶ

main.py lock_downメソッドの呼び出し
def move_and_rotate_mino(self):
    # 可能であればミノを移動または回転
    if vpos.x == 0 and vpos.y == 0 and vr == 0:
        pass
    elif self.can_mino_move(self.mino, vpos, vr):
        self.mino.move(vpos, vr)

        # 追加
        if vpos.y > 0:
            self.lockdown_cnt = 0
        else:
            self.lock_down()
    elif vr != 0:
        # SRSの実行
        rot1 = self.mino.r % 4
        rot2 = (self.mino.r + vr) % 4
        for p in self.mino.m.srs.get_rotate_offsets(rot1, rot2):
            if self.can_mino_move(self.mino, vpos + p, vr):
                self.mino.move(vpos + p, vr)
                self.lock_down() # 追加
                break

動作確認

動作確認用にSRSの動きを確認するためミノの出現順を固定させたり、地形を追加したりのデバッグコードを追加した。

main.py デバッグコード

class Game:
    # ---中略---
    # デバッグコード
    is_debug = True
    debug_mino = [
        MinoType.T,
        MinoType.T,
        MinoType.T,
        MinoType.J,
        MinoType.L,
        MinoType.Z,
        MinoType.L,
        MinoType.J,
        MinoType.S,
        MinoType.O,
        MinoType.O,
    ]
    
    put_mino_cnt = 0

    def for_debug(self):
        # DT砲とTDアタックを組み合わせたような地形を作る
        DTD = [
            [0,0,7,7,7,7,7,7,7,7,],
            [0,0,0,7,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,0,0,7,7,7,7,7,7,7,],
            [7,0,0,0,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,0,7,7,7,7,7,7,7,7,],
        ]
        # フィールドに書き込む
        for i, c in enumerate(DTD,start=12):
            for j, b in enumerate(c):
                self.matrix[i][j] = b

    def run(self):
        clock = pygame.time.Clock()
        running = True
        
        tmx_data = tmx_util.load_pygame('field.tmx')
        mino_block = tmx_util.load_pygame('mino_block.tmx')
        
        self.mino_setup()
        
        # 番兵を設置
        for i in range(-20,21):
            for j in range(-2,12):
                if j < 0 or j >= 10:
                    self.matrix[i][j] = 1
                else:
                    self.matrix[i][j] = 0
        for i in range(-2,12):
            self.matrix[20][i] = 1

        # デバッグコード
        if self.is_debug :self.for_debug()
        
        while running:
            # 省略

動作確認用の動画

SRSやミノを動かしたときの設置の猶予時間等が反映されていることが確認できる。

まとめ

ワールドルール準拠にしたSRSとロックダウン(設置判定)の適応した

次回:テトリス風落ち物パズルを作る part06 ゲームオーバーの判定 #Python - Qiita

付録

今回更新したソースコードコード全体を以下に示す

main.py
main.py
from random import choice
import pygame
from pytmx import util_pygame as tmx_util

from mino import MinoType, Mino, OtherTiles
from key import Key
from pos import Pos

TILE_SIZE = 32

MATRIX_SIZE = (20,10)
MATRIX_POS  = (11,1)
WAIT = 60
GRAVITY = 60
SOFT_DROP_SPEED = 20

start_pos = Pos(4,-2)

class Game:
    
    # 番兵用に連想配列でラクする
    matrix = {
        i:{
            j:0 for j in range(-2,12)
        } for i in range(-20,21)
    }
    
    is_debug = True
    debug_mino = [
        MinoType.T,
        MinoType.T,
        MinoType.T,
        MinoType.J,
        MinoType.L,
        MinoType.Z,
        MinoType.L,
        MinoType.J,
        MinoType.S,
        MinoType.O,
        MinoType.O,
    ]
    
    key = Key()
    
    mino : Mino
    put_mino_cnt = 0
    lockdown_cnt = 0
    
    g_cnt = 0
    wait_count = WAIT
    is_draw_mino = True
    
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((1024, 768))

    def draw_tile(self, tmx, x, y, t):
        tile_image = tmx.get_tile_image_by_gid(t)
        if tile_image:
            self.screen.blit(tile_image, (x * TILE_SIZE, y * TILE_SIZE))

    def for_debug(self):
        DTD = [
            [0,0,7,7,7,7,7,7,7,7,],
            [0,0,0,7,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,0,0,7,7,7,7,7,7,7,],
            [7,0,0,0,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,7,0,7,7,7,7,7,7,7,],
            [7,0,7,7,7,7,7,7,7,7,],
        ]
        for i, c in enumerate(DTD,start=12):
            for j, b in enumerate(c):
                self.matrix[i][j] = b

    def mino_setup(self):
        self.wait_count = WAIT
        self.is_draw_mino = True
        
        t = choice(MinoType.values())
        if self.is_debug and self.put_mino_cnt < len(self.debug_mino):
            t = self.debug_mino[self.put_mino_cnt]
        self.put_mino_cnt+=1
        self.mino = Mino(t,start_pos,0)
        if not self.can_mino_move(self.mino,Pos(0,0),0):
            for i in range(3):
                if self.can_mino_move(self.mino,Pos(0,-i),0):
                    self.mino.move(Pos(0,-i))
                    break
        
    def put_mino(self):
        self.is_draw_mino = False
        m = self.mino
        for p in m.get_shape():
            self.matrix[p.y][p.x] = m.m.tile
    def line_check(self):
        for y in range(19, -2, -1):
            n = 0
            for x in range(10):
                if self.matrix[y][x]!=0:
                    n += 1
            if n != 10:
                continue
            else:
                for x in range(10):
                    self.matrix[y][x] = OtherTiles.Vanish.tile
    def clear_line(self):
        for y in range(19,-3,-1):
            while self.matrix[y][0] == OtherTiles.Vanish.tile:
                self.wait_count = WAIT // 2 - 2
                for i in range(y, -20, -1):
                    for x in range(10):
                        t = self.matrix[i - 1][x]
                        self.matrix[i][x] = t
                for x in range(10):
                    self.matrix[-20][x] = 0
    def wait(self) -> None:
        if self.wait_count == 0:
            self.mino_setup()
        else:
            self.wait_count -=1
        if self.wait_count == WAIT // 2  - 1:
            self.put_mino()
            self.line_check()
        if self.wait_count == 1:
            self.clear_line()

    def lock_down(self):
        if self.wait_count == WAIT:
            return
        if self.lockdown_cnt < 15:
            self.lockdown_cnt += 1
            self.g_cnt = 0
            self.wait_count = WAIT

    def can_mino_move(self,mino:Mino,pos:Pos,r:int):
        for p in mino.get_moved_mino(pos,r):
            if self.matrix[p.y][p.x] != 0:
                return False
        return True
    def move_and_rotate_mino(self):
        # 待機時間が設定の半分以下の場合は待機
        if self.wait_count <= WAIT // 2:
            self.wait()
            return
        vpos = Pos()
        vr = 0
        # 床に着地しているかのチェック
        is_floor_landed = not self.can_mino_move(self.mino, Pos(0, 1), 0)
        # 回転と移動のフラグをチェックして状態を更新
        if self.key.flags["is_left_rot"]:
            vr += 3
        elif self.key.flags["is_right_rot"]:
            vr += 1
        elif self.key.flags["is_left_move"]:
            vpos += Pos(-1, 0)
        elif self.key.flags["is_right_move"]:
            vpos += Pos(1, 0)
        # ソフトドロップと重力の処理
        if not is_floor_landed and self.key.flags["is_soft_drop"]:
            self.g_cnt += SOFT_DROP_SPEED
        elif self.g_cnt < GRAVITY:
            self.g_cnt += 1
            
        if is_floor_landed:
            self.wait_count -= 1
        # 重力によるミノの移動
        while self.g_cnt >= GRAVITY:
            if not is_floor_landed:
                self.mino.p += Pos(0, 1)
                self.g_cnt -= GRAVITY
                self.wait_count = WAIT
            else:
                self.g_cnt = 0
                break
        # 可能であればミノを移動または回転
        if vpos.x == 0 and vpos.y == 0 and vr == 0:
            pass
        elif self.can_mino_move(self.mino, vpos, vr):
            self.mino.move(vpos, vr)
            if vpos.y > 0:
                self.lockdown_cnt = 0
            else:
                self.lock_down()
        elif vr != 0:
            # SRSの実行
            rot1 = self.mino.r % 4
            rot2 = (self.mino.r + vr) % 4
            for p in self.mino.m.srs.get_rotate_offsets(rot1, rot2):
                if self.can_mino_move(self.mino, vpos + p, vr):
                    self.mino.move(vpos + p, vr)
                    self.lock_down()
                    break

    def run(self):
        clock = pygame.time.Clock()
        running = True
        
        tmx_data = tmx_util.load_pygame('field.tmx')
        mino_block = tmx_util.load_pygame('mino_block.tmx')
        
        self.mino_setup()
        
        # 番兵を設置
        for i in range(-20,21):
            for j in range(-2,12):
                if j < 0 or j >= 10:
                    self.matrix[i][j] = 1
                else:
                    self.matrix[i][j] = 0
        for i in range(-2,12):
            self.matrix[20][i] = 1
            
        if self.is_debug :self.for_debug()
        
        while running:
            self.key.process_key_input() # キー入力の処理
            self.move_and_rotate_mino()
            
            
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

            self.screen.fill((0, 0, 0))

            for layer in tmx_data.layers:
                if layer.name == "mainfield":
                    for x, y, gid in layer.iter_data():
                        self.draw_tile(tmx_data, x, y, gid)
            
            x, y = MATRIX_POS
            for i in range(-1, MATRIX_SIZE[0]):
                for j in range(MATRIX_SIZE[1]):
                    self.draw_tile(mino_block, x+j, y+i, self.matrix[i][j])
            if self.is_draw_mino:
                for p in self.mino.get_shape():
                    self.draw_tile(mino_block, x+p.x, y+p.y, self.mino.m.tile)

            pygame.display.flip()
            clock.tick(60)

        pygame.quit()

if __name__ == "__main__":
    game = Game()
    game.run()

mino.py
mino.py
from dataclasses import dataclass
from enum import Enum, auto
import json
from pprint import pprint

import pytmx


from pos import Pos

# tmx ファイルからミノを構成するブロックの配置を定義する
tmx_data = pytmx.TiledMap('mino_block.tmx')
srs_patterns = None
with open('srs_pattern.json', 'r') as f:
    srs_patterns = json.load(f)

    

def get_mino_data(t, size=4):
    
    shapes = [[] for _ in range(4)]
    tile = 0
    for r in range(4):
        for y in range(size):
            for x in range(size):
                gid = tmx_data.get_tile_gid(size*t+x, size*(r%4)+y, 0)
                if gid != 0:
                    tile = gid
                    shapes[r].append((x,y))        
    return tile, shapes

# valuesメソッドを持つEnum (いろんなクラスで使うのでスーパークラス化した)
class VEnum(Enum):
    @classmethod
    def values(cls):
        # Order dictionary -> list
        return list(cls.__members__.values())

class SRS(VEnum):
    common = 'SRScommon', auto()
    minoI  = 'SRSminoI' , auto()
    def __init__(self, name, value):
        self._name = name
        self._value_ = value
        ptn = srs_patterns[name]
        self.offset_ptn_lst = tuple(tuple(Pos(*a) for a in A) for A in ptn["offset_ptn_lst"])
        self.ptn_select_mat = tuple(tuple(A) for A in ptn["ptn_select_mat"])
    def get_rotate_offsets(self, rot1, rot2):
        ptn_select = self.ptn_select_mat[rot1][rot2]
        for p in self.offset_ptn_lst[ptn_select]:
            yield p

class OtherTiles(VEnum):
    Vanish = "Vanish", -1, Pos(28,15)
    def __init__(self, name, value, p):
        self._name = name
        self._value_ = value
        self.tile = tmx_data.get_tile_gid(p.x,p.y,0)

# ミノの形を定義する列挙型
class MinoType(VEnum):
    Z = 'Z', auto(), *get_mino_data(0), SRS.common
    S = 'S', auto(), *get_mino_data(1), SRS.common
    J = 'J', auto(), *get_mino_data(2), SRS.common
    L = 'L', auto(), *get_mino_data(3), SRS.common
    T = 'T', auto(), *get_mino_data(4), SRS.common
    O = 'O', auto(), *get_mino_data(5), SRS.common
    I = 'I', auto(), *get_mino_data(6), SRS.minoI
    def __init__(self, name, value, tile, shape, srs):
        self._name = name
        self._value_ = value
        self.tile = tile
        self.shape = tuple(tuple(s) for s in shape)
        self.shape_pos = tuple(tuple(Pos(*p) for p in s) for s in shape)
        self.srs = srs

@dataclass
class Mino:
    m : MinoType
    p : Pos
    r : int
    def move(self,pos:Pos,r:int=0):
        self.r = (r+self.r) % 4
        self.p += pos
    def get_shape(self):
        for p in self.m.shape_pos[self.r%4]:
            yield p + self.p
    def get_moved_mino(self,pos:Pos,r:int=0):
        for p in self.m.shape_pos[(100+r+self.r)%4]:
            yield p + self.p + pos

def main():
    for mino in MinoType.values():
        print(mino.name, mino.value, mino.tile)
    for mino in OtherTiles.values(): 
        print(mino.name, mino.value, mino.tile)
    for srs in SRS.values():
        print(srs)
        print(srs._name)
        print(srs.value)
        pprint(srs.offset_ptn_lst)
        pprint(srs.ptn_select_mat)
        
if __name__ == '__main__':
    main()

  1. 今までこのタイマーリセットの仕様をロックダウンと思っていたのは内緒

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0