前回はホールド機能を実装した
今回はゴースト(落下予測地点の表示)を行う
準備
mino.tmxにゴースト用のブロックを追加しておく。
そして追加したタイルを使えるようにMinoTypeクラスを編集する
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):
+ Z = 'Z', auto(), *get_mino_data(0), SRS.common, Pos(28,0)
+ S = 'S', auto(), *get_mino_data(1), SRS.common, Pos(28,1)
+ J = 'J', auto(), *get_mino_data(2), SRS.common, Pos(28,2)
+ L = 'L', auto(), *get_mino_data(3), SRS.common, Pos(28,3)
+ T = 'T', auto(), *get_mino_data(4), SRS.common, Pos(28,4)
+ O = 'O', auto(), *get_mino_data(5), SRS.common, Pos(28,5)
+ I = 'I', auto(), *get_mino_data(6), SRS.minoI , Pos(28,6)
+ def __init__(self, name, value, tile, shape, srs, gpos):
self._name = name
self._value_ = value
self.tile = tile
+ self.ghost = tmx_data.get_tile_gid(gpos.x, gpos.y, 0)
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
描画
ゴーストを描画する、場所は操作ミノを表示する箇所の直前。
描画する位置は操作位置から自由落下して地面に着地した所なので
操作中のミノをコピーして失敗するまで繰り返し下に移動して、
移動に失敗したポイントで描画すればOK
main.py
class Game:
# ---中略---
def run(self):
# ---中略---
while running:
# ---中略---
if not self.gameover:
if self.is_draw_mino:
+
+ #ghost
+ gst = deepcopy(self.mino)
+ while self.can_mino_move(gst,Pos(0,1),0):
+ gst.move(Pos(0,1))
+ for p in gst.get_shape():
+ self.draw_tile(mino_block, x+p.x, y+p.y, gst.m.ghost)
+
# current
for p in self.mino.get_shape():
self.draw_tile(mino_block, x+p.x, y+p.y, self.mino.m.tile)
動作確認
実行してみた結果を録画1
ゴーストの表示で、ミノをハードドロップするとき、どの位置に固定されるかわかりやすくなった。
まとめ
今回はゴーストの表示を行った
専用のタイルをtmxファイルについかし、MinoTypeに記述
そして、操作中のミノを下に繰り返し移動して着地した箇所にゴーストを表示した。
次回:テトリス風落ち物パズルを作る part12 消去ライン数等の文字表示 #Python - Qiita
付録
main.py
main.py
from collections import deque
from copy import deepcopy
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:
#ghost
gst = deepcopy(self.mino)
while self.can_mino_move(gst,Pos(0,1),0):
gst.move(Pos(0,1))
for p in gst.get_shape():
self.draw_tile(mino_block, x+p.x, y+p.y, gst.m.ghost)
# current
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()
mino.py
mino.py
from abc import ABCMeta, abstractmethod
from collections import Counter, deque
from dataclasses import dataclass
from enum import Enum, auto
import json
from pprint import pprint
from random import choice, randrange, shuffle
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, Pos(28,0)
S = 'S', auto(), *get_mino_data(1), SRS.common, Pos(28,1)
J = 'J', auto(), *get_mino_data(2), SRS.common, Pos(28,2)
L = 'L', auto(), *get_mino_data(3), SRS.common, Pos(28,3)
T = 'T', auto(), *get_mino_data(4), SRS.common, Pos(28,4)
O = 'O', auto(), *get_mino_data(5), SRS.common, Pos(28,5)
I = 'I', auto(), *get_mino_data(6), SRS.minoI , Pos(28,6)
def __init__(self, name, value, tile, shape, srs, gpos):
self._name = name
self._value_ = value
self.tile = tile
self.ghost = tmx_data.get_tile_gid(gpos.x, gpos.y, 0)
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
class absBag(metaclass=ABCMeta):
@abstractmethod
def get_mino(self):
pass
class RandomBag(absBag):
def get_mino(self):
return choice(MinoType.values())
class Mino7Bag(absBag):
bag = deque()
def get_mino(self):
if len(self.bag) <= 7:
nexts = MinoType.values()
shuffle(nexts)
self.bag.extend(nexts)
return self.bag.popleft()
def minobag_test():
bg_rand = RandomBag()
bg_mino7 = Mino7Bag()
mino_rand = []
mino_7bag = []
for _ in range(7*10**5):
mr = bg_rand.get_mino()
m7 = bg_mino7.get_mino()
mino_rand += [mr]
mino_7bag += [m7]
A = [0,1,100,1000,10000,50000,99999]+[randrange(0,10**5) for _ in range(5)]
A.sort()
print("random:")
for a in A:
sub = [m.name for m in mino_rand[a*7:(a+1)*7]]
sub.sort()
print(f"{a:5d}", sub, Counter(sub))
print(Counter(m.name for m in mino_rand))
print("7bag:")
for a in A:
sub = [m.name for m in mino_7bag[a*7:(a+1)*7]]
sub.sort()
print(f"{a:5d}", sub, Counter(sub))
print(Counter(m.name for m in mino_7bag))
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)
minobag_test()
if __name__ == '__main__':
main()
mino_block.tmx
mino_block.tmx
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="29" height="16" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="mino_block.tsx"/>
<layer id="1" name="mino_block" width="29" height="16">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
0,2,2,0,0,0,3,3,0,4,0,0,0,0,0,5,0,0,6,0,0,7,7,0,8,8,8,8,11,
0,0,2,2,0,3,3,0,0,4,4,4,0,5,5,5,0,6,6,6,0,7,7,0,0,0,0,0,12,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,14,
0,0,0,2,0,0,3,0,0,0,4,4,0,0,5,0,0,0,6,0,0,7,7,0,0,0,8,0,15,
0,0,2,2,0,0,3,3,0,0,4,0,0,0,5,0,0,0,6,6,0,7,7,0,0,0,8,0,16,
0,0,2,0,0,0,0,3,0,0,4,0,0,0,5,5,0,0,6,0,0,0,0,0,0,0,8,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,
0,2,2,0,0,0,3,3,0,4,4,4,0,5,5,5,0,6,6,6,0,7,7,0,8,8,8,8,0,
0,0,2,2,0,3,3,0,0,0,0,4,0,5,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,
0,0,2,0,0,3,0,0,0,0,4,0,0,5,5,0,0,0,6,0,0,7,7,0,0,8,0,0,0,
0,2,2,0,0,3,3,0,0,0,4,0,0,0,5,0,0,6,6,0,0,7,7,0,0,8,0,0,0,
0,2,0,0,0,0,3,0,0,4,4,0,0,0,5,0,0,0,6,0,0,0,0,0,0,8,0,0,17
</data>
</layer>
</map>
-
見栄を張って30回くらい撮り直したのは内緒 ↩