前回に続いてPythonでゲームを作っていきます。
前回までに作ったプログラムをきれいにしましたので、以下のプログラムからスタートしたいと思います。
こちらのコードを活用してテトリスのような落ちもの系パズルゲームを作っていきます。
from __future__ import annotations
import sys, pygame, random
# ============================================================================
# 定数(ゲーム全体で使う値)
# ============================================================================
FPS = 60 # 1秒あたりの描画回数(フレームレート)
CELL = 30 # 1マスの大きさ(ピクセル)
COLS, ROWS = 10, 20 # 盤面の列数(横)、行数(縦)
PANEL_WIDTH = 240 # 右側の情報パネルの幅(ピクセル)
# 画面サイズ(横幅は「盤面の横幅 + パネル幅」)
WIDTH = CELL * COLS + PANEL_WIDTH
HEIGHT = CELL * ROWS
# 色(RGB)
WHITE = (255, 255, 255)
GRAY = (200, 200, 200)
BLACK = (0, 0, 0)
BLUE = (0, 120, 255)
COLORS = [
(0,180,255), (255,200,0), (170,0,255),
(0,200,80), (255,100,0), (255,0,100), (0,120,255)
]
# 4つの相対座標で形を表現(テトリスの7種)
SHAPES = {
"I": [(-2,0),(-1,0),(0,0),(1,0)],
"O": [(-1,0),(0,0),(-1,1),(0,1)],
"T": [(-1,0),(0,0),(1,0),(0,1)],
"S": [(0,0),(1,0),(-1,1),(0,1)],
"Z": [(-1,0),(0,0),(0,1),(1,1)],
"J": [(-1,0),(-1,1),(0,0),(1,0)],
"L": [(-1,0),(0,0),(1,0),(1,1)],
}
# ============================================================================
# クラス定義
# ============================================================================
class CellBlock(pygame.sprite.Sprite):
"""盤面上の『1マス』を表すスプライト。
grid_x, grid_y は「マスの座標」(ピクセルではない点に注意)。
画面上のピクセル位置は update_rect() で計算して rect に反映します。
"""
def __init__(self, grid_x: int, grid_y: int, color: tuple[int, int, int], cell_size: int) -> None:
super().__init__()
# -1 するのは、グリッド線と重なって見えづらくならないための微調整
self.image = pygame.Surface((cell_size - 1, cell_size - 1))
self.image.fill(color)
self.rect = self.image.get_rect()
self.grid_x = grid_x
self.grid_y = grid_y
self.cell_size = cell_size
self.update_rect()
def update_rect(self) -> None:
"""グリッド座標(マス)→ 画面上のピクセル座標に変換して配置する。"""
self.rect.topleft = (self.grid_x * self.cell_size, self.grid_y * self.cell_size)
class Piece:
"""4マスで構成される落下ピース。
- 形は SHAPES の相対座標(原点を中心としたオフセット)で表現
- 実際の盤面上の位置は「基準座標 (self.x, self.y)」+各相対座標
- 見た目は 4 個の CellBlock スプライトで描画(self.group にまとめる)
"""
def __init__(self, kind=None, color=None):
# ピースの種類(I, O, T, S, Z, J, L)と色。指定がなければランダム。
self.kind = kind or random.choice(list(SHAPES.keys()))
self.color = color or random.choice(COLORS)
# ピースの「基準位置(軸)」を盤面中央付近に置く(x は列、y は行)
self.x = COLS // 2
self.y = 1
# 形を表す4つの相対座標((0,0) が基準位置)。list(p) にして後で回転で書き換えやすく。
self.offsets = [list(p) for p in SHAPES[self.kind]]
# 見た目としての 4 マス分のスプライトを生成し、描画グループに入れる
self.blocks = []
self.group = pygame.sprite.Group()
for (ox, oy) in self.offsets:
# 相対座標を基準位置に足して、実際のグリッド座標に変換
b = CellBlock(self.x + ox, self.y + oy, self.color, CELL)
self.blocks.append(b)
self.group.add(b)
def world_cells(self):
"""現在の基準位置 (self.x, self.y) を反映した4マス分の
グリッド座標 [(x,y), ...] を返す。
衝突判定や固定処理のときに使う。"""
return [(self.x + ox, self.y + oy) for (ox, oy) in self.offsets]
def _hits_wall(self):
"""盤面の外に出ていないかを判定する。
※この段階では「固定ブロックとの衝突」は見ない(壁・床・天井のみ)。"""
for (gx, gy) in self.world_cells():
if not (0 <= gx < COLS and 0 <= gy < ROWS):
return True
return False
def update_sprites(self):
"""内部状態(self.x, self.y, self.offsets)を
実際のスプライト座標(ピクセル)に反映する。"""
for i, (ox, oy) in enumerate(self.offsets):
self.blocks[i].grid_x = self.x + ox
self.blocks[i].grid_y = self.y + oy
self.blocks[i].update_rect()
def move(self, dx, dy):
"""ピースを (dx, dy) だけグリッド単位で移動させる。
壁に当たるなどで移動できない場合は元に戻して False、成功なら True。"""
old_x, old_y = self.x, self.y
self.x += dx
self.y += dy
if self._hits_wall():
# ダメなら元の位置に戻す(ロールバック)
self.x, self.y = old_x, old_y
return False
self.update_sprites()
return True
def rotate_cw(self):
"""時計回りに回転する。
点 (x, y) → (-y, x) の回転を各相対座標に適用。
その後、簡易的な壁キック(左右に数マスずらす)で盤面内に収まるか試す。"""
return self._rotate([[-oy, ox] for (ox, oy) in self.offsets])
def rotate_ccw(self):
"""反時計回りに回転する。
点 (x, y) → (y, -x) の回転を各相対座標に適用。
その後、簡易的な壁キックで盤面内に収まるか試す。"""
return self._rotate([[oy, -ox] for (ox, oy) in self.offsets])
def _rotate(self, new_offsets):
"""回転後の相対座標(new_offsets)を一時的に適用し、
入らなければ左右へ少しずらして(壁キック)成立を試みる。
どれもダメなら元に戻して False、成功で True。"""
old_offsets, old_x = self.offsets, self.x
self.offsets = new_offsets
# ずらし量の候補(0 → 左右1 → 左右2 の順で試す)
for dx in [0, -1, 1, -2, 2]:
self.x = old_x + dx
if not self._hits_wall():
self.update_sprites()
return True
# すべて失敗:回転を諦めて元に戻す
self.offsets, self.x = old_offsets, old_x
return False
def draw(self, screen):
"""4マス分のスプライトをまとめて描画。"""
self.group.draw(screen)
# ============================================================================
# 各種関数
# ============================================================================
def draw_board(screen: pygame.Surface) -> None:
"""左側の盤面(白背景+マス目の線)を描画する。"""
# 盤面の白い背景(左側だけを塗る)
screen.fill(WHITE, (0, 0, CELL * COLS, HEIGHT))
# 薄いグリッド線(縦線)
for x in range(COLS + 1):
pygame.draw.line(screen, GRAY, (x * CELL, 0), (x * CELL, HEIGHT))
# 薄いグリッド線(横線)
for y in range(ROWS + 1):
pygame.draw.line(screen, GRAY, (0, y * CELL), (CELL * COLS, y * CELL))
# ============================================================================
# メイン処理
# ============================================================================
def main() -> None:
"""ゲームの初期化・イベント処理・描画を行うメインループ。"""
pygame.init()
pygame.display.set_caption("落ちゲー")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
# 今落ちているピースを1つ生成
current = Piece() # 種類はランダム
running = True
while running:
dt = clock.tick(FPS) # 経過ミリ秒(将来、落下速度などに使える)
for event in pygame.event.get():
# ウィンドウ右上の「×」ボタンで終了
if event.type == pygame.QUIT:
running = False
# キーが押されたとき:1マスずつ動かす(盤面の外に出ないように制限)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
current.move(-1, 0)
elif event.key == pygame.K_RIGHT:
current.move(1, 0)
elif event.key == pygame.K_DOWN:
current.move(0, 1)
elif event.key == pygame.K_UP:
current.rotate_cw()
elif event.key == pygame.K_z:
current.rotate_ccw()
# 背景を濃いグレーで塗り、盤面(白+グリッド)を描く
screen.fill((30, 30, 30))
draw_board(screen)
# 現在のピースを描画
current.draw(screen)
# 画面に反映
pygame.display.flip()
# 後片付け
pygame.quit()
sys.exit()
# スクリプトとして直接実行されたときだけ main() を呼ぶ
if __name__ == "__main__":
main()ステップ1 1マスのスプライトを登場させる
ステップ2 4マスのピースを登場させる
ステップ3
ステップ3-1:重力を実装(一定間隔で自動的に1マス落ちる)
- やること:一定時間ごとに
current.move(0, 1)を試す - 底や壁に当たったらそれ以上は落ちません(この段階では固定しない)
変更点:
定数部分に落下間隔の定数を追加:
# 追加:落下間隔(ミリ秒)
FALL_MS = 700メインループに「重力タイマー」を追加:
def main() -> None:
"""ゲームの初期化・イベント処理・描画を行うメインループ。"""
pygame.init()
pygame.display.set_caption("落ちゲー")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
# --- ここから追加:重力タイマー ---
fall_timer = 0 # 経過ミリ秒をためる
# --- 追加ここまで ---
・・・・・
running = True
while running:
dt = clock.tick(FPS) # 経過ミリ秒(将来、落下速度などに使える)
for event in pygame.event.get():
# ウィンドウ右上の「×」ボタンで終了
if event.type == pygame.QUIT:
running = False
# キーが押されたとき:1マスずつ動かす(盤面の外に出ないように制限)
elif event.type == pygame.KEYDOWN:
・・・・・
# --- ここから追加:重力 ---
fall_timer += dt
if fall_timer >= FALL_MS:
fall_timer = 0
# 1マス落下(底や壁なら何も起きない)
current.move(0, 1)
# --- 追加ここまで ---
# 背景を濃いグレーで塗り、盤面(白+グリッド)を描く
screen.fill((30, 30, 30))
draw_board(screen)これで放っておいてもゆっくり落ちるようになります。
※まだ「固定」はしません。底についたらそれ以上は下がらないだけです。
ステップ3-2:固定を実装(固定後は次のブロックを生成)
追加①:盤面と当たり判定ヘルパー
下記コードを「各種関数」部分に追記
# ★追加:固定ブロックを保持する2次元配列(None=空/(R,G,B)=固定ブロックの色)
def empty_board():
return [[None for _ in range(COLS)] for _ in range(ROWS)]
# ★追加:boardと壁を含めた当たり判定(dx,dy分だけ動かせるか?)
def can_place(board, piece, dx=0, dy=0, offsets=None):
offs = offsets if offsets is not None else piece.offsets
for ox, oy in offs:
gx = piece.x + dx + ox
gy = piece.y + dy + oy
# 盤面外?
if not (0 <= gx < COLS and 0 <= gy < ROWS):
return False
# 既に固定ブロック?
if board[gy][gx] is not None:
return False
return True
# ★追加:固定ブロック描画
def draw_fixed_from_board(screen, board):
for y in range(ROWS):
for x in range(COLS):
color = board[y][x]
if color is not None:
rect = pygame.Rect(x*CELL, y*CELL, CELL-1, CELL-1)
pygame.draw.rect(screen, color, rect)追加②:main() の初期化に盤面を用意(1行だけ)
fall_timer = 0 # 経過ミリ秒をためる
board = empty_board() # ★追加
置換③:重力処理に“固定→次ブロック”だけを追加(数行)
moveに失敗したら固定ではなく、先にcan_placeで落下可否を判定して、
落ちられない場合だけ boardへ書き込み→新ピース生成 にします。
(ステップ3-1のブロックを置き換え)
# --- 重力(ステップ3-1のブロックを置き換え) ---
fall_timer += dt
if fall_timer >= FALL_MS:
fall_timer = 0
if can_place(board, current, dx=0, dy=1):
current.move(0, 1) # ← 既存のmove(壁のみチェック)をそのまま使用
else:
# ★固定:現在ピースの4マスをboardに反映
for gx, gy in current.world_cells():
if 0 <= gx < COLS and 0 <= gy < ROWS:
board[gy][gx] = current.color
# ★次のブロックを出す
current = Piece()変更④:描画に固定ブロックを1行追加
# 背景を濃いグレーで塗り、盤面(白+グリッド)を描く
screen.fill((30, 30, 30))
draw_board(screen)
draw_fixed_from_board(screen, board) # ★追加:固定を先に描く
# 現在のピースを描画
current.draw(screen)ステップ3-3:キー操作でめり込む部分を修正
置換:キー操作(左右・下・回転)を置換
既存の current.move(...) 呼び出しの直前で can_place(...) を見て、通る時だけ move を実行します。
回転は簡易壁キック(0, -1, +1)だけ入れていますが、クラスは触りません。
# キーが押されたとき:1マスずつ動かす(盤面の外に出ないように制限)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
if can_place(board, current, dx=-1, dy=0):
current.move(-1, 0) # ← Pieceは壁判定のみの既存実装をそのまま使用
elif event.key == pygame.K_RIGHT:
if can_place(board, current, dx=1, dy=0):
current.move(1, 0)
elif event.key == pygame.K_DOWN:
if can_place(board, current, dx=0, dy=1):
current.move(0, 1)
elif event.key == pygame.K_UP:
# 簡易:時計回り((x,y)->(-y,x))+ 壁キック[0,-1,+1]
new_offsets = [[-oy, ox] for (ox, oy) in current.offsets]
for kick in (0, -1, 1):
if can_place(board, current, dx=kick, dy=0, offsets=new_offsets):
current.x += kick
current.offsets = new_offsets
current.update_sprites()
break
elif event.key == pygame.K_z:
# 反時計回り((x,y)->(y,-x))+ 壁キック
new_offsets = [[oy, -ox] for (ox, oy) in current.offsets]
for kick in (0, -1, 1):
if can_place(board, current, dx=kick, dy=0, offsets=new_offsets):
current.x += kick
current.offsets = new_offsets
current.update_sprites()
breakこれでできること
- 放置→自動で落下(従来通り)
- 底や積みに当たると固定して次のブロック出現
- 左右/下/回転も、固定ブロックに重ならない範囲のみ通る(最小実装)
※この段階では「ライン消去/ゲームオーバー判定/ネクスト表示/スコア」は未実装。
ステップを崩さず、次に安全に足せます。
ステップ3-4:ソフトドロップ(下キー長押しで落下加速)
- やること:下キーを押している間だけ、落下間隔を短くする
- 実装方法:
pygame.key.get_pressed()で押下状態を常時チェックし、fall_intervalを切り替える
差分:
① 定数の差し替え(ファイル上部・定数群の中)
変更前:
# 追加:落下間隔(ミリ秒)
FALL_MS = 700変更後:
# 追加:2種類の落下間隔
FALL_MS_NORMAL = 700
FALL_MS_SOFT = 80② main() の完成形(ステップ3-2+ソフトドロップのみ追加)
前提:empty_board, can_place, draw_fixed_from_board, draw_board, Piece は既に定義済み(ステップ3-2の最小変更版のまま)
下記 main() に置き換えれば、インデントはそのまま通ります。
def main() -> None:
pygame.init()
pygame.display.set_caption("落ちゲー")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
board = empty_board()
current = Piece()
fall_timer = 0
fall_interval = FALL_MS_NORMAL # ★追加:現在の落下間隔
running = True
while running:
dt = clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
if can_place(board, current, dx=-1, dy=0):
current.move(-1, 0)
elif event.key == pygame.K_RIGHT:
if can_place(board, current, dx=1, dy=0):
current.move(1, 0)
elif event.key == pygame.K_DOWN:
# 単発の1マス移動(押しっぱなしの加速は下のソフトドロップで実現)
if can_place(board, current, dx=0, dy=1):
current.move(0, 1)
elif event.key == pygame.K_UP:
# 時計回り回転 + 簡易壁キック
new_offsets = [[-oy, ox] for (ox, oy) in current.offsets]
for kick in (0, -1, 1):
if can_place(board, current, dx=kick, dy=0, offsets=new_offsets):
current.x += kick
current.offsets = new_offsets
current.update_sprites()
break
elif event.key == pygame.K_z:
# 反時計回り回転 + 簡易壁キック
new_offsets = [[oy, -ox] for (ox, oy) in current.offsets]
for kick in (0, -1, 1):
if can_place(board, current, dx=kick, dy=0, offsets=new_offsets):
current.x += kick
current.offsets = new_offsets
current.update_sprites()
break
# ★追加:ソフトドロップ(下キー長押しで落下間隔を短縮)
keys = pygame.key.get_pressed()
if keys[pygame.K_DOWN]:
fall_interval = FALL_MS_SOFT
else:
fall_interval = FALL_MS_NORMAL
# --- 重力(fall_interval を使用) ---
fall_timer += dt
if fall_timer >= fall_interval:
fall_timer = 0
if can_place(board, current, dx=0, dy=1):
current.move(0, 1)
else:
# 固定 → 次のブロック
for gx, gy in current.world_cells():
if 0 <= gx < COLS and 0 <= gy < ROWS:
board[gy][gx] = current.color
current = Piece()
# --- 描画 ---
screen.fill((30, 30, 30))
draw_board(screen)
draw_fixed_from_board(screen, board)
current.draw(screen)
pygame.display.flip()
pygame.quit()
sys.exit()
補足
KEYDOWN の K_DOWN で1マス即時移動は残しています。
「押した瞬間に1マス進み、押しっぱなし中は高速落下」になります。
これが不要なら、その行(current.move(0, 1))をコメントアウトしてください。
ここまでの進捗まとめ
- ベースコードをリファクタ→読みやすく拡張しやすい形に整備
- 重力で自動落下
- 固定して新しいブロックが出現
- ソフトドロップで落下加速(長押し対応)
次の提案(この先の小分けステップ)
- ライン消去(1行そろったら削除して上から詰める)
- ゲームオーバー判定(出現直後に積みに当たったら終了)
- ネクスト表示(右パネルの黒背景に次の形をミニ表示)
- スコア(消した行数に応じて加点)
- ハードドロップ(スペースで一気に着地)
- ゴースト(影)表示