テトリス風落ち物パズルを作る part11 ゴースト(落下予測地点の表示)

[前回](https://qiita.com/comet725/items/c2cd679a0bcf2d956a5e)はホールド機能を実装した

今回はゴースト(落下予測地点の表示)を行う

# 準備

mino.tmxにゴースト用のブロックを追加しておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398956/8fc438c9-be33-1791-b844-82993db37208.png)

そして追加したタイルを使えるようにMinoTypeクラスを編集する

```diff_python
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

```diff_python: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)
```

# 動作確認
実行してみた結果を録画[^movie]

[^movie]: 見栄を張って30回くらい撮り直したのは内緒

<iframe width="560" height="315" src="https://www.youtube.com/embed/YIr-2ok-tXk?si=droxda-c2feVx3Bq" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

ゴーストの表示で、ミノをハードドロップするとき、どの位置に固定されるかわかりやすくなった。

# まとめ
今回はゴーストの表示を行った
専用のタイルをtmxファイルについかし、MinoTypeに記述
そして、操作中のミノを下に繰り返し移動して着地した箇所にゴーストを表示した。

次回：[テトリス風落ち物パズルを作る part12 消去ライン数等の文字表示 #Python - Qiita](https://qiita.com/comet725/items/24d25a00911f46ed4071)

# 付録

<details><summary>main.py</summary>

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

```

</details>

<details><summary>mino.py</summary>

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

</details>

<details><summary>mino_block.tmx</summary>

```xml: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>

```

</details>