スペース・インベーダーゲーム

ゲーム

ChatGTPとのコラボレーションで最初に作成したゲームです。プログラミングをしたくて大学院に入ったのですがもっぱら化学の研究に没頭することになり、プログラミングとは無縁でした。自分でも独学でプログラム作成に取り組んでみたのですが、上手くいかず今回、ChatGPTと会話することで、ようやく実際に動くプログラムを作成することが出来ました。大感激です。

以下にコードの全文を掲載しました。

invaders_title.py

タイトル画面つき・サウンド内蔵(追加ファイル不要)

推奨: Python 3.14 + pygame-ce 2.5.6

import pygame, sys, random, math, io, wave, struct, time

===== サウンド合成ユーティリティ =====

SR = 44100

def _write_wav(int16_bytes):
bio = io.BytesIO()
with wave.open(bio, “wb”) as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SR)
wf.writeframes(int16_bytes)
bio.seek(0)
return bio

def synth_tone(freq=440, ms=200, vol=0.6, wave_type=”sine”, attack_ms=6, release_ms=10):
“””単音の効果音を生成して pygame.mixer.Sound を返す”””
n = int(SR * ms / 1000)
atk = int(SR * attack_ms / 1000)
rel = int(SR * release_ms / 1000)
buf = bytearray()
for i in range(n):
t = i / SR
if wave_type == “sine”:
v = math.sin(2math.pifreqt) elif wave_type == “square”: v = 1.0 if math.sin(2math.pifreqt) >= 0 else -1.0
elif wave_type == “noise”:
v = random.uniform(-1.0, 1.0)
else:
v = math.sin(2math.pifreq*t)
# ADSR(簡易) – 立ち上がりとリリースを軽く
if i < atk: v *= i/max(1, atk) if i > n-rel:
v *= max(0.0, (n-i)/max(1, rel))
s = int(max(-1.0, min(1.0, v)) * 32767 * vol)
buf += struct.pack(“<h”, s)
return pygame.mixer.Sound(_write_wav(buf))

def synth_melody(freqs, note_ms=160, vol=0.25, wave_type=”square”):
“””周波数列(0で休符)から短いループBGMを生成”””
seg_n = int(SR * note_ms / 1000)
atk = int(SR * 8 / 1000)
rel = int(SR * 12 / 1000)
out = bytearray()
for f in freqs:
for i in range(seg_n):
if f <= 0: v = 0.0 else: t = i / SR if wave_type == “square”: base = 1.0 if math.sin(2math.pift) >= 0 else -1.0 elif wave_type == “sine”: base = math.sin(2math.pift) else: base = math.sin(2math.pif*t) v = base # 簡易エンベロープ if i < atk: v *= i/max(1, atk) if i > seg_n-rel: v = max(0.0, (seg_n-i)/max(1, rel)) out += struct.pack(“32767*vol))
return pygame.mixer.Sound(_write_wav(out))

===== ゲーム定数 =====

W, H = 800, 600
FPS = 60
WHITE = (240,240,240)
CYAN = (120,220,235)
RED = (240, 80, 80)
GREEN = (120,235,120)
YELLOW= (250,220, 80)
BG = (10, 12, 18)

ALIEN_ROWS, ALIEN_COLS = 4, 10
ALIEN_W, ALIEN_H = 36, 24

===== 本体 =====

class Game:
def init(self):
pygame.init()
# サウンドは安全初期化(失敗しても画面は出る)
self.sound_ok = False
try:
pygame.mixer.init(frequency=SR, size=-16, channels=1, buffer=1024)
pygame.mixer.set_num_channels(8)
self.bgm_ch = pygame.mixer.Channel(0) # BGM専用
self.sound_ok = True
except Exception as e:
print(“サウンド初期化エラー:”, e)

    self.screen = pygame.display.set_mode((W, H))
    pygame.display.set_caption("Space Invaders (Title Screen)")
    self.clock = pygame.time.Clock()
    self.font  = pygame.font.SysFont(None, 32)
    self.big   = pygame.font.SysFont(None, 84)
    self.mid   = pygame.font.SysFont(None, 48)

    # 効果音
    if self.sound_ok:
        self.snd_shoot = synth_tone(880, 100, 0.55, "square")
        self.snd_hit   = synth_tone(300, 140, 0.6, "sine")
        self.snd_ouch  = synth_tone(120, 220, 0.6, "noise")
    else:
        self.snd_shoot = self.snd_hit = self.snd_ouch = None

    # BGM(短いループ)
    self.sound_enabled = True
    if self.sound_ok:
        # タイトル用:ゆるい進行
        title_pattern = [659,494,523,587, 659,0,659,587, 523,494,440,0, 392,440,494,0]
        self.bgm_title = synth_melody(title_pattern, note_ms=180, vol=0.20, wave_type="square")
        # プレイ用:少し速め
        play_pattern  = [784,0,784,659, 784,0,880,784, 659,587,523,0, 587,659,698,0]
        self.bgm_play = synth_melody(play_pattern,  note_ms=140, vol=0.22, wave_type="square")
    else:
        self.bgm_title = self.bgm_play = None

    # 状態管理
    self.state = "TITLE"  # TITLE / PLAY / PAUSE / GAMEOVER
    self.score = 0

    # タイトル用背景(星)
    self.stars = self._make_stars(120)

    # ゲーム用オブジェクト
    self.player = pygame.Rect(W//2-20, H-60, 40, 20)
    self.bullets = []
    self.aliens  = []
    self.alien_dx = 1

    self.start_title()

# ---------- BGM制御 ----------
def play_bgm(self, sound):
    if not (self.sound_ok and self.sound_enabled and sound):
        return
    self.bgm_ch.stop()
    self.bgm_ch.play(sound, loops=-1)

def stop_bgm(self):
    if self.sound_ok:
        self.bgm_ch.stop()

# ---------- 画面ごとの初期化 ----------
def start_title(self):
    self.state = "TITLE"
    self.stop_bgm()
    self.play_bgm(self.bgm_title)

def start_game(self):
    self.state = "PLAY"
    self.score = 0
    self.player.update(W//2-20, H-60, 40, 20)
    self.bullets = []
    self.aliens = [pygame.Rect(x*60+120, y*40+80, ALIEN_W, ALIEN_H)
                   for y in range(ALIEN_ROWS) for x in range(ALIEN_COLS)]
    self.alien_dx = 1
    self.stop_bgm()
    self.play_bgm(self.bgm_play)

def start_gameover(self):
    self.state = "GAMEOVER"
    self.stop_bgm()

# ---------- 星背景 ----------
def _make_stars(self, n):
    stars = []
    for _ in range(n):
        x = random.randint(0, W-1)
        y = random.randint(0, H-1)
        sp = random.uniform(0.5, 2.0)
        stars.append([x, y, sp])
    return stars

def _update_stars(self):
    for s in self.stars:
        s[1] += s[2]
        if s[1] >= H:
            s[0] = random.randint(0, W-1)
            s[1] = 0
            s[2] = random.uniform(0.5, 2.0)

def _draw_stars(self, surf):
    for x,y,sp in self.stars:
        c = (180,180,200) if sp < 1.2 else (230,230,255)
        surf.fill(c, (int(x), int(y), 2, 2))

# ---------- メインループ ----------
def run(self):
    while True:
        dt = self.clock.tick(FPS)
        self.handle_events()
        self.update(dt)
        self.draw()

# ---------- 入力 ----------
def handle_events(self):
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            pygame.quit(); sys.exit()
        if e.type == pygame.KEYDOWN:
            if self.state == "TITLE":
                if e.key == pygame.K_RETURN:
                    self.start_game()
                elif e.key == pygame.K_ESCAPE:
                    pygame.quit(); sys.exit()
                elif e.key == pygame.K_m:
                    self.toggle_mute()
            elif self.state == "PLAY":
                if e.key == pygame.K_ESCAPE:
                    self.state = "PAUSE"
                elif e.key == pygame.K_m:
                    self.toggle_mute()
            elif self.state == "PAUSE":
                if e.key == pygame.K_ESCAPE:
                    self.state = "PLAY"
                elif e.key == pygame.K_m:
                    self.toggle_mute()
            elif self.state == "GAMEOVER":
                if e.key == pygame.K_RETURN:
                    self.start_title()
                elif e.key == pygame.K_m:
                    self.toggle_mute()

    # ゲーム中の連射
    if self.state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT] and self.player.left > 0:
            self.player.x -= 5
        if keys[pygame.K_RIGHT] and self.player.right < W:
            self.player.x += 5
        if keys[pygame.K_SPACE]:
            if len(self.bullets) < 2:
                self.bullets.append(pygame.Rect(self.player.centerx-2, self.player.top-10, 4, 10))
                if self.sound_ok and self.sound_enabled and self.snd_shoot:
                    self.snd_shoot.play()

def toggle_mute(self):
    self.sound_enabled = not self.sound_enabled
    if not self.sound_enabled:
        self.stop_bgm()
    else:
        # 状態に応じてBGM再開
        if self.state == "TITLE": self.play_bgm(self.bgm_title)
        if self.state == "PLAY":  self.play_bgm(self.bgm_play)

# ---------- 更新処理 ----------
def update(self, dt):
    # タイトル/ポーズ/ゲームオーバーでも星は動かす
    self._update_stars()

    if self.state != "PLAY":
        return

    # 弾
    for b in self.bullets:
        b.y -= 7
    self.bullets = [b for b in self.bullets if b.bottom > 0]

    # エイリアン移動(端で折り返し+降下)
    move_down = False
    for a in self.aliens:
        a.x += self.alien_dx
        if a.left < 16 or a.right > W-16:
            move_down = True
    if move_down:
        self.alien_dx = -self.alien_dx
        for a in self.aliens: a.y += 20

    # ヒット判定
    for b in self.bullets[:]:
        hit = next((a for a in self.aliens if a.colliderect(b)), None)
        if hit:
            self.aliens.remove(hit)
            self.bullets.remove(b)
            self.score += 10
            if self.sound_ok and self.sound_enabled and self.snd_hit:
                self.snd_hit.play()

    # 到達でゲームオーバー
    for a in self.aliens:
        if a.bottom >= self.player.top:
            if self.sound_ok and self.sound_enabled and self.snd_ouch:
                self.snd_ouch.play()
            self.start_gameover()
            break

    # 全滅で次ウェーブ(少し加速)
    if not self.aliens:
        self.aliens = [pygame.Rect(x*60+120, y*40+80, ALIEN_W, ALIEN_H)
                       for y in range(ALIEN_ROWS) for x in range(ALIEN_COLS)]
        self.alien_dx = 1 if random.random()<0.5 else -1

# ---------- 描画 ----------
def draw(self):
    self.screen.fill(BG)
    self._draw_stars(self.screen)

    if self.state == "TITLE":
        t = self.big.render("SPACE  INVADERS", True, WHITE)
        self.screen.blit(t, (W//2 - t.get_width()//2, 120))
        sub = self.mid.render("Press ENTER to Start", True, CYAN)
        self.screen.blit(sub, (W//2 - sub.get_width()//2, 220))
        tip1 = self.font.render("← → Move   /   Space: Shoot   /   Esc: Quit", True, WHITE)
        self.screen.blit(tip1, (W//2 - tip1.get_width()//2, 300))
        tip2 = self.font.render("M: Mute / Unmute", True, (210,210,210))
        self.screen.blit(tip2, (W//2 - tip2.get_width()//2, 334))

        # タイトルのデモ用エイリアン
        for i in range(10):
            x = 120 + i*60 + int(10*math.sin(pygame.time.get_ticks()/600))
            y = 400 + int(5*math.sin((i*0.7)+pygame.time.get_ticks()/400))
            pygame.draw.rect(self.screen, GREEN if i%2==0 else YELLOW, (x,y,ALIEN_W,ALIEN_H), border_radius=4)

    elif self.state in ("PLAY","PAUSE"):
        # エンティティ
        pygame.draw.rect(self.screen, CYAN, self.player)
        for b in self.bullets: pygame.draw.rect(self.screen, WHITE, b)
        for a in self.aliens: pygame.draw.rect(self.screen, GREEN, a)

        # HUD
        score = self.font.render(f"Score: {self.score}", True, WHITE)
        mute = self.font.render("M: Mute OFF" if self.sound_enabled else "M: Mute ON", True, WHITE)
        self.screen.blit(score, (10, 10))
        self.screen.blit(mute, (W - mute.get_width() - 10, 10))

        if self.state == "PAUSE":
            box = pygame.Surface((W, H), pygame.SRCALPHA)
            box.fill((0,0,0,160))
            self.screen.blit(box, (0,0))
            p = self.mid.render("PAUSED", True, WHITE)
            s = self.font.render("Press ESC to Resume", True, WHITE)
            self.screen.blit(p, (W//2 - p.get_width()//2, H//2 - 40))
            self.screen.blit(s, (W//2 - s.get_width()//2, H//2 + 8))

    elif self.state == "GAMEOVER":
        g = self.big.render("GAME  OVER", True, WHITE)
        s = self.mid.render("Press ENTER to return to Title", True, CYAN)
        sc = self.font.render(f"Final Score: {self.score}", True, WHITE)
        self.screen.blit(g, (W//2 - g.get_width()//2, 180))
        self.screen.blit(sc, (W//2 - sc.get_width()//2, 260))
        self.screen.blit(s, (W//2 - s.get_width()//2, 320))

    pygame.display.flip()

===== エントリーポイント =====

if name == “main“:
Game().run()

タイトルとURLをコピーしました