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

テトリス風落ち物パズルを作る part03 キー入力・落下処理

Last updated at Posted at 2024-06-13

前回ではテトリミノを配置した。
今回はキー入力とミノの落下処理を追加する。

座標管理

x, y の位置管理を一つのクラスにまとめておく

pos.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Pos(object):
    x: int = 0
    y: int = 0
    def __hash__(self):
        return hash((self.x, self.y))

    def __eq__(self, other):
        if isinstance(other, Pos):
            return self.x == other.x and self.y == other.y
        return False
    def __iadd__(self, other):
        return NotImplemented
    def __add__(self, other):
        match other:
            case Pos():
                return Pos(self.x+other.x, self.y+other.y)
            case (x, y):
                return Pos(self.x+x, self.y+y)
            case _:
                kls = other.__class__.__name__
                raise NotImplementedError(
                    f'Addtion between Pos and {kls} is not supported')
    def plus(self,*args):
        match args:
            case Pos(),:
                other = args[0]
                return Pos(self.x+other.x, self.y+other.y)
            case x, y, *_:
                return Pos(self.x+x, self.y+y)
            case tuple() as p, *_:
                x, y, *_ = p 
                return Pos(self.x+x, self.y+y)
def main():
    # Posオブジェクト同士の加算演算子を使用して新しいPosオブジェクトを作成
    print(Pos(1,1)+Pos(2,3))
    print(Pos(1,1)+(2,3))
    # Posオブジェクトに同じPosオブジェクトを加えるメソッドを使用
    print(Pos(1,1).plus(Pos(1,1)))
    # Posオブジェクトにタプル形式の値を加えるメソッドを使用
    print(Pos(1,1).plus((1,1)))

if __name__ == '__main__':
    main()

実行してみた結果

Pos(x=3, y=4)
Pos(x=3, y=4)
Pos(x=2, y=2)
Pos(x=2, y=2)

計算をまとめてできる

キー入力

ワールドルールの以下のシステムも適応している(ため押ししたら連続で動くしくみ: 参考)

  • DAS: Delayer Auto Shift
  • ARR: Auto Repeat Rate
key.py
import pygame

DAS_FRAME = 20
ARR_FRAME = 2
class Key:
    # 各キーの押下状態を保持
    is_pushed = {
            "z": False,
            "x": False,
            "DOWN": False,
            "LEFT": False,
            "RIGHT": False
        }
        
    # 各キーが押された時点のフレーム数を保持
    key_timestamps = {
            "z": 0,
            "x": 0,
            "DOWN": 0,
            "LEFT": 0,
            "RIGHT": 0
        }
    is_moved : bool = False
    is_spined: bool = False
    def process_key_input(self):
        keys = pygame.key.get_pressed()
        self.flags = {
            "is_left_rot": False,
            "is_right_rot": False,
            "is_left_move": False,
            "is_right_move": False,
            "is_soft_drop": False,
        }       
        for key, state in self.is_pushed.items():
            if keys[getattr(pygame, f'K_{key}')] and not state:
                # キーが押された瞬間
                self.is_pushed[key] = True
                self.key_timestamps[key] += 1
            elif not keys[getattr(pygame, f'K_{key}')] and state:
                # キーが離された瞬間
                self.is_pushed[key] = False
                self.key_timestamps[key] = 0
            elif state:
                self.handle_key_press(key) 
                self.key_timestamps[key] += 1
    def handle_key_press(self, key):
        
        cnt = self.key_timestamps[key]
        # DAS: Delayer Auto Shift, ARR: Auto Repeat Rate
        das = (cnt >= DAS_FRAME and cnt % ARR_FRAME == 0)
        
        match key:
            case "z":
                self.flags["is_left_rot"] = cnt == 1
            case "x":
                self.flags["is_right_rot"] = cnt == 1
            case "DOWN":
                self.flags["is_soft_drop"] = cnt == 1 or das
            case "LEFT":
                self.flags["is_left_move"] = cnt == 1 or das
            case "RIGHT":
                self.flags["is_right_move"] = cnt == 1 or das

プレイヤーの操作情報を追加

プレイヤーが操作しているテトリミノの情報を管理するクラスをmino.pyに追加する

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

import pytmx

from pos import Pos

# tmx ファイルからミノを構成するブロックの配置を定義する
tmx_data = pytmx.TiledMap('mino_block.tmx')
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

# ミノの形を定義する列挙型
class MinoType(Enum):
    Z = 'Z', auto(), *get_mino_data(0)
    S = 'S', auto(), *get_mino_data(1)
    J = 'J', auto(), *get_mino_data(2)
    L = 'L', auto(), *get_mino_data(3)
    T = 'T', auto(), *get_mino_data(4)
    O = 'O', auto(), *get_mino_data(5)
    I = 'I', auto(), *get_mino_data(6)
    def __init__(self, name, value, tile, shape):
        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)
    
    @classmethod
    def values(cls):
        # Order dictionary -> list
        return list(cls.__members__.values())

# 追加
@dataclass
class Mino:
    m : MinoType
    p : Pos
    r : int
    def move(self,pos:Pos,r):
        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):
        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)
        print(mino.shape)
        print(mino.shape_pos)
        
if __name__ == '__main__':
    main()

落下処理

追加したクラスを使って処理を実装する

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

from mino import MinoType, Mino
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)
    }
    
    key = Key()
    
    mino : Mino
    
    g_cnt = 0
    
    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 mino_setup(self):
        t = choice(MinoType.values())
        self.mino = Mino(t,start_pos,0)

    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):
        vpos = Pos()
        vr = 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 self.key.flags["is_soft_drop"]:
            self.g_cnt += SOFT_DROP_SPEED
        elif self.g_cnt < GRAVITY:
            self.g_cnt += 1
            
        # 重力によるミノの移動
        while self.g_cnt >= GRAVITY:
            if self.can_mino_move(self.mino, Pos(0, 1), 0):
                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)

    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
        
        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])
            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()

動作チェック

実行してみた結果を録画してみた

まとめ

駆け足になってしまったが以下を追加した

  • 座標を管理するクラス
  • キー入力を管理するクラス
  • 操作しているミノの情報を管理するクラス
  • 上記のクラスを使ってミノを落下させたり、操作する処理

次回: テトリス風落ち物パズルを作る part04 ミノの設置とライン消去 #Python - Qiita

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