前回はハードドロップを実装した
今回はミノの出現規則である7-bagを実装する
7-bag
ワールドルールではテトリミノが出る順番に規則性がある
- 7種類のミノ(ZSJLTOI)を1つのセット(bag)とする
- 1回でたミノはセットの中で他のミノがすべて出現するまで発生させない
これを7-bagとか言う名前で呼ばれている。
実装としては単純で、7種類のミノをすべて用意してシャッフル、
dequeを使って先頭から順番に取り出していけばOK
足りなくなかったら補充する感じ
from collection import deque
class Mino7Bag:
bag = deque()
def get_mino(self):
if len(self.bag) <= 7:
nexts = MinoType.values()
shuffle(nexts)
self.bag.extend(nexts)
return self.bag.popleft()
上記に示した実装をmino.py
に追加する
実装では比較用にランダムbagを実装する(抽象化したクラスとかも追加している)
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()
検証コードとして70万回ミノを取り出してそして適当な箇所の1bag(7回分)を取り出してランダムと7-bagそれぞれ比較してみる。
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]]
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]]
print(f"{a:5d}", sub, Counter(sub))
print(Counter(m.name for m in mino_7bag))
実行してみた結果はこんな感じ
random:
0 ['O', 'J', 'I', 'L', 'I', 'I', 'S'] Counter({'I': 3, 'O': 1, 'J': 1, 'L': 1, 'S': 1})
1 ['L', 'I', 'S', 'L', 'Z', 'L', 'O'] Counter({'L': 3, 'I': 1, 'S': 1, 'Z': 1, 'O': 1})
100 ['T', 'T', 'S', 'S', 'O', 'O', 'O'] Counter({'O': 3, 'T': 2, 'S': 2})
1000 ['J', 'J', 'T', 'O', 'I', 'L', 'S'] Counter({'J': 2, 'T': 1, 'O': 1, 'I': 1, 'L': 1, 'S': 1})
10000 ['S', 'O', 'S', 'L', 'L', 'I', 'L'] Counter({'L': 3, 'S': 2, 'O': 1, 'I': 1})
18654 ['J', 'Z', 'L', 'I', 'L', 'O', 'S'] Counter({'L': 2, 'J': 1, 'Z': 1, 'I': 1, 'O': 1, 'S': 1})
22957 ['I', 'I', 'O', 'S', 'J', 'I', 'L'] Counter({'I': 3, 'O': 1, 'S': 1, 'J': 1, 'L': 1})
50000 ['Z', 'T', 'O', 'O', 'I', 'I', 'S'] Counter({'O': 2, 'I': 2, 'Z': 1, 'T': 1, 'S': 1})
52056 ['J', 'T', 'T', 'J', 'L', 'I', 'L'] Counter({'J': 2, 'T': 2, 'L': 2, 'I': 1})
71128 ['S', 'I', 'Z', 'Z', 'I', 'I', 'O'] Counter({'I': 3, 'Z': 2, 'S': 1, 'O': 1})
71544 ['S', 'T', 'T', 'O', 'J', 'I', 'O'] Counter({'T': 2, 'O': 2, 'S': 1, 'J': 1, 'I': 1})
99999 ['L', 'I', 'L', 'J', 'I', 'I', 'S'] Counter({'I': 3, 'L': 2, 'J': 1, 'S': 1})
Counter({'O': 100319, 'L': 100271, 'Z': 100012, 'S': 99936, 'I': 99924, 'J': 99770, 'T': 99768})
7bag:
0 ['T', 'L', 'J', 'Z', 'S', 'O', 'I'] Counter({'T': 1, 'L': 1, 'J': 1, 'Z': 1, 'S': 1, 'O': 1, 'I': 1})
1 ['T', 'S', 'J', 'I', 'Z', 'L', 'O'] Counter({'T': 1, 'S': 1, 'J': 1, 'I': 1, 'Z': 1, 'L': 1, 'O': 1})
100 ['J', 'L', 'I', 'O', 'T', 'Z', 'S'] Counter({'J': 1, 'L': 1, 'I': 1, 'O': 1, 'T': 1, 'Z': 1, 'S': 1})
1000 ['S', 'Z', 'O', 'J', 'L', 'I', 'T'] Counter({'S': 1, 'Z': 1, 'O': 1, 'J': 1, 'L': 1, 'I': 1, 'T': 1})
10000 ['O', 'T', 'I', 'L', 'S', 'Z', 'J'] Counter({'O': 1, 'T': 1, 'I': 1, 'L': 1, 'S': 1, 'Z': 1, 'J': 1})
18654 ['J', 'I', 'Z', 'O', 'S', 'L', 'T'] Counter({'J': 1, 'I': 1, 'Z': 1, 'O': 1, 'S': 1, 'L': 1, 'T': 1})
22957 ['J', 'I', 'Z', 'T', 'L', 'O', 'S'] Counter({'J': 1, 'I': 1, 'Z': 1, 'T': 1, 'L': 1, 'O': 1, 'S': 1})
50000 ['S', 'J', 'T', 'Z', 'I', 'O', 'L'] Counter({'S': 1, 'J': 1, 'T': 1, 'Z': 1, 'I': 1, 'O': 1, 'L': 1})
52056 ['T', 'S', 'J', 'L', 'O', 'I', 'Z'] Counter({'T': 1, 'S': 1, 'J': 1, 'L': 1, 'O': 1, 'I': 1, 'Z': 1})
71128 ['J', 'Z', 'L', 'O', 'I', 'T', 'S'] Counter({'J': 1, 'Z': 1, 'L': 1, 'O': 1, 'I': 1, 'T': 1, 'S': 1})
71544 ['I', 'Z', 'L', 'O', 'S', 'J', 'T'] Counter({'I': 1, 'Z': 1, 'L': 1, 'O': 1, 'S': 1, 'J': 1, 'T': 1})
99999 ['T', 'Z', 'S', 'O', 'J', 'L', 'I'] Counter({'T': 1, 'Z': 1, 'S': 1, 'O': 1, 'J': 1, 'L': 1, 'I': 1})
Counter({'T': 100000, 'L': 100000, 'J': 100000, 'Z': 100000, 'S': 100000, 'O': 100000, 'I': 100000})
70万回の全体的な出現数は大きな差がないが(±0.3%ぐらい?)ランダムで7個ずつ取り出した場合では大きな偏りが起きていることがわかる。
統計的にはだいたい同じでも、直近の数手のツモの差が大きく変わるとそれはゲームバランスとしてどうなのということで7-bagが実装されているわけですね。
7-bagを実装する
検証もしたところで実際にこのMino7Bagクラスをmain.py
の方に組み込んでいく。
- from mino import MinoTile, Mino, OtherTiles
+ from mino import Mino7Bag, Mino, OtherTiles
class Game:
# ---中略---
+ bag = Mino7Bag()
# ---中略---
def mino_setup(self):
self.wait_count = WAIT
self.is_draw_mino = True
- t = choice(MinoType.values())
+ t = self.bag.get_mino()
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
動作確認
実行してみた結果を録画してみた
分かりづらいが、おなじテトリミノが最大でも2連続しか来ていないことが確認できるだろうか。
比較用に前回までの出現ミノがランダムな場合の動作も録画してみたので各自で比較してほしい
まとめ
ワールドルールに従ったミノの出現規則通称7-bagを実装した。ランダムと比較してどのように出現ミノが変化するか検証コードを実行してみた。実装した7-bagをmain.py
に組み込んで、実際に出現ミノの偏りがないか、動画でも比較した。
次回:テトリス風落ち物パズルを作る part09 ネクストミノの表示 #Python - Qiita
付録
main.py
import pygame
from pytmx import util_pygame as tmx_util
from mino import Mino7Bag, 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
bag = Mino7Bag()
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 = self.bag.get_mino()
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()
mino.py