From 13179af11163262bc3465ef323355cd075d5083e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:36:47 +0000 Subject: [PATCH] Add Ultimate Python Shooter with stages and boss Co-authored-by: muumuu8181 <87556753+muumuu8181@users.noreply.github.com> --- .gitignore | 2 + README.md | 30 +++- shooting_game.py | 331 ++++++++++++++++++++++++++++++++++++ test_shooting_game_logic.py | 34 ++++ 4 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 shooting_game.py create mode 100644 test_shooting_game_logic.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index a3a7888..d819e5a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,38 @@ -# Python Visual Effects 🎬✨ +# Python Visual Effects & Games 🎬✨🎮 -Python で作成した美しい視覚効果とアニメーションのコレクション +Python で作成した美しい視覚効果とアニメーション、そして全力で作ったシューティングゲームのコレクション ## 機能 🌟 +### 🎮 Ultimate Python Shooter (シューティングゲーム) +ターミナルで遊べる本格的なシューティングゲームです。 + +- **ステージシステム**: 敵を倒してステージを進めよう! +- **ボスバトル**: 強力なボスが登場! +- **パワーアップ**: アイテムを取ってショットを強化! +- **ボム**: 画面内の敵を一掃する必殺技! + +### 🎨 Visual Effects - **🌈 レインボーテキスト**: 文字が色とりどりに表示される効果 - **📝 タイプライター効果**: 文字が一文字ずつゆっくり現れる演出 - **🖍️ カラータイプライター効果**: 文字ごとに色が変わるタイプライティング - **🔄 スピナーエフェクト**: くるくる回るスピナーアニメーション - **💻 マトリックス風エフェクト**: 日本語文字が緑色で画面を流れる -- **💥 爆発アニメーション**: 殡階的に拡大する爆発エフェクト +- **💥 爆発アニメーション**: 段階的に拡大する爆発エフェクト ## 実行方法 🚀 +### シューティングゲーム +```bash +python3 shooting_game.py +``` +**操作方法**: +- **矢印キー**: 移動 +- **スペース**: ショット / 決定 +- **B**: ボム発射 +- **Q**: 終了 / 中断 + +### ビジュアルエフェクト ```bash python3 hello.py ``` @@ -21,10 +41,11 @@ python3 hello.py - Python 3.x - ターミナル/コマンドプロンプト (カラー表示対応) +- Linux環境推奨(`curses` ライブラリを使用するため) ## デモ 🎥 -実行すると以下のエフェクトが順番に表示されます: +実行すると以下のエフェクトが順番に表示されます(`hello.py`): 1. 長文のタイプライター効果でメッセージが表示 2. カラータイプライター効果 @@ -38,6 +59,7 @@ python3 hello.py - **カラーエフェクト**: ANSI エスケープコードを使用 - **アニメーション**: time.sleep() による時間制御 - **文字セット**: 日本語ひらがな + 数字の組み合わせ +- **ゲームライブラリ**: `curses` (標準ライブラリ) ## 作成者 👨‍💻 diff --git a/shooting_game.py b/shooting_game.py new file mode 100644 index 0000000..f2a2be2 --- /dev/null +++ b/shooting_game.py @@ -0,0 +1,331 @@ +import curses +import random +import time + +class Bullet: + def __init__(self, x, y, dy, dx=0, char='|'): + self.x = x + self.y = y + self.dy = dy + self.dx = dx + self.char = char + + def update(self): + self.y += self.dy + self.x += self.dx + + def draw(self, stdscr): + try: + if self.y >= 0: + stdscr.addch(int(self.y), int(self.x), self.char) + except curses.error: + pass + +class Enemy: + def __init__(self, x, y, dy, hp=1, char='V'): + self.x = x + self.y = y + self.dy = dy + self.hp = hp + self.char = char + + def update(self, max_x): + self.y += self.dy + + def draw(self, stdscr): + try: + if self.y >= 0: + stdscr.addch(int(self.y), int(self.x), self.char) + except curses.error: + pass + +class Boss(Enemy): + def __init__(self, x, y): + super().__init__(x, y, 0.1, hp=50, char='W') + self.move_dir = 1 + self.move_timer = 0 + self.shoot_timer = 0 + + def update(self, max_x): + self.y += self.dy + + if self.y > 5: + self.dy = 0 + + self.move_timer += 1 + if self.move_timer > 5: + self.x += self.move_dir + self.move_timer = 0 + if self.x <= 2 or self.x >= max_x - 3: + self.move_dir *= -1 + +class PowerUp: + def __init__(self, x, y): + self.x = x + self.y = y + self.dy = 0.5 + self.char = 'P' + + def update(self): + self.y += self.dy + + def draw(self, stdscr): + try: + stdscr.addch(int(self.y), int(self.x), self.char) + except curses.error: + pass + +class Explosion: + def __init__(self, x, y): + self.x = x + self.y = y + self.life = 5 + self.chars = ['.', 'o', 'O', '@', '*'] + + def update(self): + self.life -= 1 + + def draw(self, stdscr): + if self.life > 0: + char = self.chars[min(len(self.chars)-1, self.life)] + try: + stdscr.addch(int(self.y), int(self.x), char) + except curses.error: + pass + +class Player: + def __init__(self, x, y, char='^'): + self.x = x + self.y = y + self.char = char + self.power = 1 + self.bombs = 2 + + def move(self, dx, dy, max_x, max_y): + self.x = max(0, min(max_x - 1, self.x + dx)) + self.y = max(0, min(max_y - 1, self.y + dy)) + + def shoot(self): + bullets = [] + bullets.append(Bullet(self.x, self.y - 1, -1, 0, '|')) + if self.power >= 2: + bullets.append(Bullet(self.x - 1, self.y - 1, -1, -0.3, '/')) + bullets.append(Bullet(self.x + 1, self.y - 1, -1, 0.3, '\\')) + return bullets + + def draw(self, stdscr): + try: + stdscr.addch(int(self.y), int(self.x), self.char) + except curses.error: + pass + +def check_collision(obj1, obj2): + return int(obj1.x) == int(obj2.x) and int(obj1.y) == int(obj2.y) + +def draw_title(stdscr, sh, sw): + stdscr.clear() + title = "ULTIMATE PYTHON SHOOTER" + try: + stdscr.addstr(sh // 2 - 2, sw // 2 - len(title) // 2, title, curses.A_BOLD) + msg = "Press SPACE to Start" + stdscr.addstr(sh // 2, sw // 2 - len(msg) // 2, msg) + except curses.error: + pass + stdscr.refresh() + while True: + key = stdscr.getch() + if key == ord(' '): + return True + elif key == ord('q'): + return False + time.sleep(0.05) + +def draw_gameover(stdscr, sh, sw, score): + stdscr.clear() + msg = "GAME OVER" + try: + stdscr.addstr(sh // 2 - 2, sw // 2 - len(msg) // 2, msg, curses.A_BOLD) + score_msg = f"Final Score: {score}" + stdscr.addstr(sh // 2, sw // 2 - len(score_msg) // 2, score_msg) + retry_msg = "Press SPACE to Restart or 'q' to Quit" + stdscr.addstr(sh // 2 + 2, sw // 2 - len(retry_msg) // 2, retry_msg) + except curses.error: + pass + stdscr.refresh() + while True: + key = stdscr.getch() + if key == ord(' '): + return True + elif key == ord('q'): + return False + time.sleep(0.05) + +def game_loop(stdscr, sh, sw): + player = Player(sw // 2, sh - 2) + bullets = [] + enemies = [] + powerups = [] + explosions = [] + + score = 0 + lives = 3 + level = 1 + enemies_spawned = 0 + boss_spawned = False + + enemy_spawn_timer = 0 + + running = True + while running: + stdscr.clear() + + # Spawn enemies + if not boss_spawned: + enemy_spawn_timer += 1 + if enemy_spawn_timer > max(5, 25 - level * 2): + enemies.append(Enemy(random.randint(1, sw - 2), 0, 0.2 + random.random() * 0.1 * level, hp=1 + level//2)) + enemies_spawned += 1 + enemy_spawn_timer = 0 + + if enemies_spawned > 10 * level and len(enemies) == 0: + boss = Boss(sw // 2, -3) + enemies.append(boss) + boss_spawned = True + + # Update entities + player.draw(stdscr) + + for bullet in bullets[:]: + bullet.update() + bullet.draw(stdscr) + if bullet.y < 0 or bullet.y >= sh or bullet.x < 0 or bullet.x >= sw: + bullets.remove(bullet) + + for enemy in enemies[:]: + enemy.update(sw) + enemy.draw(stdscr) + if enemy.y >= sh: + enemies.remove(enemy) + + # Collision with Player + if check_collision(player, enemy): + lives -= 1 + explosions.append(Explosion(enemy.x, enemy.y)) + enemies.remove(enemy) + if isinstance(enemy, Boss): + boss_spawned = False + enemies_spawned = 0 + level += 1 + score += 500 + + if lives <= 0: + running = False + + # Collision: Bullets vs Enemies + for bullet in bullets[:]: + hit = False + for enemy in enemies[:]: + if check_collision(bullet, enemy): + enemy.hp -= 1 + hit = True + explosions.append(Explosion(bullet.x, bullet.y)) + if enemy.hp <= 0: + if enemy in enemies: enemies.remove(enemy) + score += 10 * level + explosions.append(Explosion(enemy.x, enemy.y)) + if isinstance(enemy, Boss): + score += 1000 + level += 1 + enemies_spawned = 0 + boss_spawned = False + lives += 1 + elif random.random() < 0.1: + powerups.append(PowerUp(enemy.x, enemy.y)) + break + if hit: + bullets.remove(bullet) + + # Update and Draw PowerUps + for pu in powerups[:]: + pu.update() + pu.draw(stdscr) + if pu.y >= sh: + powerups.remove(pu) + elif check_collision(player, pu): + player.power = min(3, player.power + 1) + powerups.remove(pu) + score += 50 + player.bombs = min(5, player.bombs + 1) + + # Update and Draw Explosions + for exp in explosions[:]: + exp.update() + exp.draw(stdscr) + if exp.life <= 0: + explosions.remove(exp) + + # UI + try: + stdscr.addstr(0, 0, f"Score: {score} | Lives: {lives} | Level: {level} | Bombs: {player.bombs}") + if boss_spawned: + stdscr.addstr(0, sw - 10, "BOSS!!", curses.A_BLINK) + stdscr.addstr(1, 0, "Move: Arrows | Shoot: Space | Bomb: 'b' | Quit: 'q'") + except curses.error: + pass + + stdscr.refresh() + + # Input Handling + key = stdscr.getch() + + if key == ord('q'): + return score # Quit game but show score? Or just exit? Let's show score. + # running = False + elif key == curses.KEY_LEFT: + player.move(-1, 0, sw, sh) + elif key == curses.KEY_RIGHT: + player.move(1, 0, sw, sh) + elif key == curses.KEY_UP: + player.move(0, -1, sw, sh) + elif key == curses.KEY_DOWN: + player.move(0, 1, sw, sh) + elif key == ord(' '): + bullets.extend(player.shoot()) + elif key == ord('b'): + if player.bombs > 0: + player.bombs -= 1 + for e in enemies[:]: + e.hp -= 10 + explosions.append(Explosion(e.x, e.y)) + if e.hp <= 0: + enemies.remove(e) + score += 10 + if isinstance(e, Boss): + score += 1000 + level += 1 + enemies_spawned = 0 + boss_spawned = False + lives += 1 + + time.sleep(0.02) + + return score + +def main(stdscr): + # Initial Setup + curses.curs_set(0) + stdscr.nodelay(1) + stdscr.timeout(50) + + sh, sw = stdscr.getmaxyx() + + if not draw_title(stdscr, sh, sw): + return + + while True: + score = game_loop(stdscr, sh, sw) + if not draw_gameover(stdscr, sh, sw, score): + break + +if __name__ == "__main__": + curses.wrapper(main) diff --git a/test_shooting_game_logic.py b/test_shooting_game_logic.py new file mode 100644 index 0000000..68e905b --- /dev/null +++ b/test_shooting_game_logic.py @@ -0,0 +1,34 @@ +import unittest +import sys +from unittest.mock import MagicMock +sys.modules['curses'] = MagicMock() + +from shooting_game import Bullet, Enemy, Boss, PowerUp, Player, check_collision + +class TestShootingGameLogic(unittest.TestCase): + def test_collision(self): + b = Bullet(10, 10, -1) + e = Enemy(10, 10, 1) + self.assertTrue(check_collision(b, e)) + + def test_boss_inheritance(self): + boss = Boss(10, 0) + self.assertIsInstance(boss, Enemy) + self.assertEqual(boss.hp, 50) + + def test_powerup_collision(self): + p = Player(10, 10) + pu = PowerUp(10, 10) + self.assertTrue(check_collision(p, pu)) + + def test_player_shoot_powerup(self): + p = Player(10, 10) + bullets = p.shoot() + self.assertEqual(len(bullets), 1) + + p.power = 2 + bullets = p.shoot() + self.assertEqual(len(bullets), 3) + +if __name__ == '__main__': + unittest.main()