前回はネクストミノの表示を実装した
今回はホールドの実装を行う
これで左枠の削除しないで済みます
ホールド機能とは:テトリミノを1個だけ保存する機能
変数の追加
ホールドは連続して使えないので、フラグ用の変数とホールドしたミノを格納する変数を追加する。
ミノを置いたときにホールドを使えるようにフラグを初期化しておく。
start_pos = Pos(3,-2)
+hold_pos = Pos(4, 3)
next_pos_lst = tuple(Pos(24,3+3*i) for i in range(6))
class Game:
# ---中略---
+ hold_mino = None
+ is_hold_used = False
# ---中略---
def mino_setup(self):
self.wait_count = WAIT
self.lockdown_cnt = 0
self.is_draw_mino = True
+ self.is_hold_used = False
t = self.pop_bag()
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
ホールド処理の関数
holdを実行する関数を実装する
この関数ではまだ何もホールドしていないときは
ホールドに操作中のミノを格納した後そのまま次のミノを出現させる。
ホールド2回目以降ではホールドしたミノと操作中のミノを交換して、
ミノの位置を初期地点に移動させる。
def hold(self) -> None:
if self.hold_mino is None:
self.hold_mino = self.mino.m
self.mino_setup()
else:
self.g_cnt = 0
self.wait_count = WAIT
self.mino.r = 0
self.hold_mino, t = self.mino.m, self.hold_mino
self.mino = Mino(t,start_pos,0)
self.is_hold_used = True
キー入力追加
スペースキー入力でホールド発動のフラグを立てる。
- DAS,ARRは実装しない
import pygame
DAS_FRAME = 20
ARR_FRAME = 2
class Key:
# 各キーの押下状態を保持
is_pushed = {
"z": False,
"x": False,
+ "SPACE": False,
"UP": False,
"DOWN": False,
"LEFT": False,
"RIGHT": False
}
# 各キーが押された時点のフレーム数を保持
key_timestamps = {
"z": 0,
"x": 0,
+ "SPACE": 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,
+ "is_hold": 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 "SPACE":
+ self.flags["is_hold"] = 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
追加したキー入力があったら、ホールド関数を実行する。ホールド実行したらそのフレームでやることはないのでreturnしておく
def move_and_rotate_mino(self):
# 待機時間が設定の半分以下の場合は待機
if self.wait_count <= WAIT // 2:
self.wait()
return
+ # ホールドの処理
+ if self.key.flags["is_hold"] and not self.is_hold_used:
+ self.hold()
+ return
ホールドしたミノの描画
適当な箇所に描画処理を追加する
class Game:
# ---中略---
def run(self):
# ---中略---
while running:
# ---中略---
# NEXT
for i, (t, p) in enumerate(zip(self.nexts,next_pos_lst)):
for pt in t.shape_pos[0]:
np = pt + p
self.draw_tile(mino_block, np.x, np.y, t.tile)
+ # HOLD
+ if self.hold_mino is not None:
+ for p in self.hold_mino.shape_pos[0]:
+ hp = p + hold_pos
+ self.draw_tile(mino_block, hp.x, hp.y, self.hold_mino.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)
動作確認
実行してみた結果を録画してみた
動画では適当に自分が使えるテンプレ(BT砲、山岳積み2️号、罰金砲1)を打っているが
その際に何度かホールドを実行しているのが見えるだろうか。
まとめ
今回はホールド機能の実装を行った。
フラグ管理用の変数等を追加しホールド関数を作成、
ホールド関数ではまだ何も入っていないときには単純に操作中のミノを格納して
次のミノを出現させる。
2回目以降ではホールドしたミノと操作中のミノを交換して。
初期地点に戻す処理を行う。
スペースキー入力でホールドの実行フラグが立つようにし、
ホールド関数を実行。
そのときに格納されたミノは左の枠に表示した。
ここまで来るとコンシューマーゲームのテトリスと同じようなことができるようになってきた。
次回:テトリス風落ち物パズルを作る part11 ゴースト(落下予測地点の表示) #Python - Qiita
付録
main.py
from collections import deque
from random import choice
import pygame
from pytmx import util_pygame as tmx_util
from mino import Mino7Bag, RandomBag, 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)
hold_pos = Pos(4, 3)
next_pos_lst = tuple(Pos(24,3+3*i) for i in range(6))
class Game:
# 番兵用に連想配列でラクする
matrix = {
i:{
j:0 for j in range(-1,11)
} for i in range(-20,21)
}
is_debug = False
key = Key()
mino : Mino
put_mino_cnt = 0
lockdown_cnt = 0
gameover = False
bag = Mino7Bag()
nexts = deque()
hold_mino = None
is_hold_used = 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 pop_bag(self):
p = self.nexts.popleft()
self.nexts.append(self.bag.get_mino())
return p
def mino_setup(self):
self.wait_count = WAIT
self.lockdown_cnt = 0
self.is_draw_mino = True
self.is_hold_used = False
t = self.pop_bag()
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 hold(self) -> None:
if self.hold_mino is None:
self.hold_mino = self.mino.m
self.mino_setup()
else:
self.g_cnt = 0
self.wait_count = WAIT
self.mino.r = 0
self.hold_mino, t = self.mino.m, self.hold_mino
self.mino = Mino(t,start_pos,0)
self.is_hold_used = True
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 p.x < 0 or p.x > 10 or 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
# ホールドの処理
if self.key.flags["is_hold"] and not self.is_hold_used:
self.hold()
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')
for _ in range(len(next_pos_lst)):
self.nexts.append(self.bag.get_mino())
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)
# NEXT
for i, (t, p) in enumerate(zip(self.nexts,next_pos_lst)):
for pt in t.shape_pos[0]:
np = pt + p
self.draw_tile(mino_block, np.x, np.y, t.tile)
# HOLD
if self.hold_mino is not None:
for p in self.hold_mino.shape_pos[0]:
hp = p + hold_pos
self.draw_tile(mino_block, hp.x, hp.y, self.hold_mino.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 pygameDAS_FRAME = 20
ARR_FRAME = 2
class Key:
# 各キーの押下状態を保持
is_pushed = {
"z": False,
"x": False,
"SPACE": False,
"UP": False,
"DOWN": False,
"LEFT": False,
"RIGHT": False
}
# 各キーが押された時点のフレーム数を保持
key_timestamps = {
"z": 0,
"x": 0,
"SPACE": 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,
"is_hold": 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 "SPACE":
self.flags["is_hold"] = 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
-
Zスピンが上手くいかなくて失敗しているのは内緒 ↩