前回はゲームオーバーを追加した
今回はハードドロップを実装する
ハードドロップ:即座に地面へ落下してその状態に固定して次のミノを出現させる
上級者とかこれ連打してとてつもない設置速度になっている。
キー入力の追加
今回ハードドロップさせるキーとして上キーを追加する、
左右の回転と同様に長押しで連続にハードドロップすると困るのでDASやARRのため押しで連続発動する動作は実装しない。
class Key:
# 各キーの押下状態を保持
is_pushed = {
"z": False,
"x": False,
"UP": False, # 追加
"DOWN": False,
"LEFT": False,
"RIGHT": False
}
# 各キーが押された時点のフレーム数を保持
key_timestamps = {
"z": 0,
"x": 0,
"UP": 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,
"is_hard_drop": False, # 追加
}
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 "UP": # 追加
self.flags["is_hard_drop"] = 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
ハードドロップの動作実装
上キーが入力されたタイミングでハードドロップを実行する、場所は重力の落下処理の次辺り、処理内容としては失敗するまで下に移動を試みて、失敗したらその位置でミノを固定して待機カウンターの値を変更して次のミノを即座に出させる。ただし、ミノの位置によってライン消去が発生して演出を入れる必要がある場合を考え、ライン消去の有無に応じてカウンタの値を調整する。
def move_and_rotate_mino(self):
# ---中略---
# 重力によるミノの移動
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 self.key.flags["is_hard_drop"]:
while self.can_mino_move(self.mino, Pos(0, 1), 0):
self.mino.move(Pos(0, 1))
self.put_mino()
if self.line_check():
self.wait_count = WAIT // 2 - 2
else:
self.wait_count = 2
ライン消去の判定に、消去できるラインがあるかどうかを返すようにしておく。
def line_check(self):
flg = False # 追加
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:
flg = True # 追加
for x in range(10):
self.matrix[y][x] = OtherTiles.Vanish.tile
return flg # 追加
動作確認
今回はデバッグコードを使用しない
class Game:
# ---中略---
is_debug = False
# ---中略---
def for_debug(self):
pass
動作確認の録画
まとめ
今回はハードドロップを実装した。実装にはキー入力の操作を追加し、重力落下のあとに上キーの入力があった場合に失敗するまで下に移動させ、その場で固定する、ライン消去があるかどうかで待機カウンタの値を変更させる。
次回:テトリス風落ち物パズルを作る part08 ミノの出現規則(7-bag) #Python - Qiita
https://qiita.com/comet725/items/8f508d6e1ca80a16f330
付録
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(3,-2)
class Game:
# 番兵用に連想配列でラクする
matrix = {
i:{
j:0 for j in range(-2,12)
} for i in range(-20,21)
}
is_debug = False
key = Key()
mino : Mino
put_mino_cnt = 0
lockdown_cnt = 0
gameover = False
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):
pass
def mino_setup(self):
self.wait_count = WAIT
self.is_draw_mino = True
t = choice(MinoType.values())
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
else:
self.gameover = True
def put_mino(self):
self.is_draw_mino = False
m = self.mino
flg = True
for p in m.get_shape():
self.matrix[p.y][p.x] = m.m.tile
if p.y >= 0:
flg = False
if flg:
self.gameover = True
def line_check(self):
flg = False
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:
flg = True
for x in range(10):
self.matrix[y][x] = OtherTiles.Vanish.tile
return flg
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 self.key.flags["is_hard_drop"]:
while self.can_mino_move(self.mino, Pos(0, 1), 0):
self.mino.move(Pos(0, 1))
self.put_mino()
if self.line_check():
self.wait_count = WAIT // 2 - 2
else:
self.wait_count = 2
# 可能であればミノを移動または回転
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()
gameover_cnt = 0
gameover_cnt_sub = 0
# 番兵を設置
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:
if not self.gameover:
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)
if self.gameover:
# ゲームオーバーの演出、下から灰色になっていく
for i in range(20,19-gameover_cnt,-1):
for j in range(10):
if self.matrix[i][j] != 0:
self.matrix[i][j] = 8
if gameover_cnt_sub == 2:
gameover_cnt = min(22, gameover_cnt+1)
gameover_cnt_sub = 0
else: gameover_cnt_sub += 1
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 not self.gameover:
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)
# 21段目チラ見せ
self.screen.fill((0, 0, 0),
(10*TILE_SIZE,0,12*TILE_SIZE,TILE_SIZE*2//3)
)
pygame.display.flip()
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
game = Game()
game.run()
key.py
import pygame
DAS_FRAME = 20
ARR_FRAME = 2
class Key:
# 各キーの押下状態を保持
is_pushed = {
"z": False,
"x": False,
"UP": False,
"DOWN": False,
"LEFT": False,
"RIGHT": False
}
# 各キーが押された時点のフレーム数を保持
key_timestamps = {
"z": 0,
"x": 0,
"UP": 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,
"is_hard_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 "UP":
self.flags["is_hard_drop"] = 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