前回はキー入力とミノの落下処理を作った。
今回はワールドルールの一つであるSRSとロックダウンを実装していく。
SRS: Super Rotation System
- ミノが壁とか床に接している状態でも回転できるように位置の補正を行う処理
- Tspin Tripleができるのはこのシステムのおかげ
- どう補正されるの解説とかは色々複雑なので以下のページ等を参照してほしい
ロックダウン
- 今まで設置判定適当だったけどある程度ワールドルールに準拠したい
- 地面に設置した判定から、一定時間動かす余地を与えてから設置(これは今まで実装していた)
- 地面に設置した状態回転や移動するとミノの固定までの時間が一旦リセットされる(今回これを追加)1
- 何回もやれてしまうと無限ループが起きるためリセットは大体15回までに制限する
SRSの補正を考える
使用して要は「その位置でミノが回転できないなら、ミノの位置を補正した状態なら回転可能か順番にチェックする」ということ。
そして、その補正の仕方が「それぞれの向きから右、左回転するとき」、「Iミノかそれ以外のとき」で違うという話。
だから全パターンを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に変換する。
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
を使用する
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に変換できていそう
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のデータを回転・移動処理のメソッドの中で使用するようにする。
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
ロックダウンの追加
ロックダウンの実装は単純で設置後に回るカウンターのリセット処理をメソッドに入れて
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
移動処理で左右に動かしたり回転できたりした場合にそのメソッドを呼ぶ
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の動きを確認するためミノの出現順を固定させたり、地形を追加したりのデバッグコードを追加した。
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
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
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()
-
今までこのタイマーリセットの仕様をロックダウンと思っていたのは内緒 ↩