diff --git a/.gitignore b/.gitignore index 5ebd21a..f17e638 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,6 @@ pip-log.txt # Mac crap .DS_Store + +# 虛擬環境 +venv/ diff --git a/README.md b/README.md index ad9c8ab..2f7b560 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,125 @@ -### Pacman in Python with PyGame +# Pacman - Bomb & Powerups -This is a very minimal implementation of the Pacman game, having only one level and without ghosts strategy, not even with random movements (yes, the routes are programmed). However, we may improve this game in the future and everyone else interested can feel free to fork and contribute to this project. +一個使用 PyGame 開發的升級版 Pacman 遊戲,融合了炸彈和道具系統。 -Download installer from here: https://github.com/hbokmann/Pacman/blob/master/pacman.exe +## 遊戲特色 -![Pacman Game Window](https://raw.github.com/hbokmann/Pacman/master/images/pacman.jpg) +### 核心玩法 +- **方向控制**:左右上下箭頭鍵控制 Pacman 移動 +- **炸彈系統**:按 `Ctrl` 放置炸彈,炸彈在 50 幀後爆炸 + - 爆炸會消滅範圍內的鬼魂(+50 分) + - 爆炸會傷害無敵狀態下的 Pacman +- **道具系統**: + - **星星** (Star):獲得無敵狀態 5 秒,Pacman 快速閃爍彩色 + - **冰塊** (Ice):凍結所有鬼魂 5 秒 +### 視覺效果 +- **智能幽靈**:8 隻最多幽靈同時出現,每隻有隨機移動路徑 +- **方向旋轉**:Pacman 根據移動方向自動旋轉(上下左右) +- **無敵視效**:無敵時快速切換黃、紅、藍、紫、青五種顏色 +- **爆炸動畫**:炸彈爆炸時的十字形爆炸效果 -# Future development +### 音效系統 +- 背景音樂 +- 吃豆子音效 +- 道具音效(星星、冰塊) +- 炸彈爆炸音效 +- 吃掉鬼魂音效 -* Fix Pacman's movement -* Ghosts moving algorithm and artificial intelligence -* Better design -* Better algorithm for the walls -* Additional levels? +### 計分系統 +- 吃豆子:+1 分 +- 炸死鬼魂:+50 分(普通)/ +100 分(無敵狀態下) +- 目標:吃完所有豆子並存活 +## 操作方式 -Tested with [PyGame 1.9](http://pygame.org/ftp/pygame-1.9.2a0.win32-py3.2.msi ) and [Python 3.2 32bit](http://www.python.org/ftp/python/3.2.3/python-3.2.3.msi) +| 按鍵 | 功能 | +|------|------| +| ⬅️ ➡️ ⬆️ ⬇️ | 移動 Pacman | +| `Ctrl` | 放置炸彈(最多 3 個) | +| `Enter` | 遊戲結束時重新開始 | +| `Esc` | 遊戲結束時退出 | +## 安裝與運行 -### Additional resources -* [Pac-Man Dossier - strategy of the ghosts movement](http://home.comcast.net/~jpittman2/pacman/pacmandossier.html) -* [HTML5 Pacman](http://arandomurl.com/2010/07/25/html5-pacman.html) -* [PyGame tutorials](http://programarcadegames.com/index.php?lang=en) -* [How To Write a Pacman Game in JavaScript](http://www.masswerk.at/JavaPac/pacman-howto.html) -* [Original Pacman game](http://originalpacman.com/) +### 環境需求 +- Python 3.8+ +- PyGame 2.6.1+ +### 安裝步驟 +```bash +# 1. 克隆或下載專案 +git clone https://github.com/chichichi959595/Pacman.git +cd Pacman +# 2. 創建虛擬環境 +python -m venv venv +.\venv\Scripts\Activate.ps1 # Windows +source venv/bin/activate # Mac/Linux + +# 3. 安裝依賴 +pip install pygame + +# 4. 執行遊戲 +python pacman.py +``` + +## 專案結構 + +``` +Pacman/ +├── pacman.py # 主遊戲檔案 +├── README.md # 說明文檔 +├── images/ # 遊戲圖片資源 +│ ├── pacman.png +│ ├── Blinky.png # 紅色幽靈 +│ ├── Pinky.png # 粉紅幽靈 +│ ├── Inky.png # 藍色幽靈 +│ ├── Clyde.png # 橘色幽靈 +│ ├── bomb.png # 炸彈 +│ ├── bomb_flash.png # 炸彈閃爍 +│ ├── star.png # 星星道具 +│ ├── ice.png # 冰塊道具 +│ └── explosion_*.png # 爆炸效果 +└── sounds/ # 遊戲音效 + ├── pacman.mp3 # 背景音樂 + ├── powerup.wav # 道具音效 + ├── freeze.wav # 冰凍音效 + ├── eat_ghost.wav # 吃鬼音效 + └── bomb_sound.mp3 # 炸彈音效 +``` + +## 遊戲界面 + +- **分數顯示**:`Score: X/總豆子數 Bombs: 剩餘炸彈數` +- **狀態指示**: + - 無敵時顯示「INVINCIBLE!」(橘色) + - 凍結時顯示「GHOSTS FROZEN!」(青色) + +## 遊戲規則 + +1. 移動 Pacman 吃掉地圖上的所有豆子 +2. 避免接觸鬼魂(除非處於無敵狀態) +3. 使用炸彈消滅鬼魂或自衛 +4. 道具隨機出現,收集來獲得特殊能力 +5. 吃完所有豆子或撞到鬼魂時遊戲結束 + +## 功能 + +- Pacman 方向旋轉(上下左右) +- 無敵狀態下的彩色閃爍效果 +- 炸彈放置與爆炸系統 +- 星星和冰塊道具 +- 鬼魂 AI(隨機尋路) +- 音效和音樂系統 +- 爆炸動畫效果 +- 分數計算系統 + +## 技術棧 + +- **Python 3.8+** +- **PyGame 2.6.1** +- **Sprite 碰撞檢測** +- **時間管理系統** +- **事件驅動設計** -### Support or Contact -Twitter: https://twitter.com/hbokmann diff --git a/images/bomb.png b/images/bomb.png new file mode 100644 index 0000000..cebdaec Binary files /dev/null and b/images/bomb.png differ diff --git a/images/bomb_flash.png b/images/bomb_flash.png new file mode 100644 index 0000000..6e975a0 Binary files /dev/null and b/images/bomb_flash.png differ diff --git a/images/explosion_center.png b/images/explosion_center.png new file mode 100644 index 0000000..df83360 Binary files /dev/null and b/images/explosion_center.png differ diff --git a/images/explosion_end.png b/images/explosion_end.png new file mode 100644 index 0000000..7f57c0d Binary files /dev/null and b/images/explosion_end.png differ diff --git a/images/explosion_mid.png b/images/explosion_mid.png new file mode 100644 index 0000000..cb98fad Binary files /dev/null and b/images/explosion_mid.png differ diff --git a/images/ice.png b/images/ice.png new file mode 100644 index 0000000..a99b984 Binary files /dev/null and b/images/ice.png differ diff --git a/images/star.png b/images/star.png new file mode 100644 index 0000000..9f28c82 Binary files /dev/null and b/images/star.png differ diff --git a/pacman.ico.lnk b/pacman.ico.lnk new file mode 100644 index 0000000..5ef4579 Binary files /dev/null and b/pacman.ico.lnk differ diff --git a/pacman.py b/pacman.py index 022bf6d..0f5a05e 100644 --- a/pacman.py +++ b/pacman.py @@ -1,580 +1,453 @@ -#Pacman in Python with PyGame -#https://github.com/hbokmann/Pacman - -import pygame._view - +import pygame +import sys +import random +from pathlib import Path + +# --- 顏色設定 --- black = (0,0,0) white = (255,255,255) blue = (0,0,255) green = (0,255,0) red = (255,0,0) purple = (255,0,255) -yellow = ( 255, 255, 0) +yellow = (255, 255, 0) +orange = (255, 165, 0) +cyan = (0, 255, 255) + +MAX_GHOSTS = 8 +POWER_DURATION = 50 # 50 frames at 10 FPS, about 5 seconds +ROOT = Path(__file__).resolve().parent -Trollicon=pygame.image.load('images/Trollman.png') -pygame.display.set_icon(Trollicon) +# --- 基礎初始化 --- +pygame.init() +try: + pygame.mixer.init() +except pygame.error as exc: + print(f"Audio disabled: {exc}") +screen = pygame.display.set_mode([606, 606]) +pygame.display.set_caption('Pacman - Bomb & Powerups') +clock = pygame.time.Clock() -#Add music -pygame.mixer.init() -pygame.mixer.music.load('pacman.mp3') -pygame.mixer.music.play(-1, 0.0) +def asset_path(*parts): + return str(ROOT.joinpath(*parts)) + +def load_sound(*parts): + if not pygame.mixer.get_init(): + return None + path = asset_path(*parts) + try: + return pygame.mixer.Sound(path) + except (pygame.error, FileNotFoundError) as exc: + print(f"Could not load sound {path}: {exc}") + return None + +# --- 資源載入 (包含原始炸彈音效) --- +bomb_sound = load_sound('sounds', 'bomb_sound.mp3') +sfx_powerup = load_sound('sounds', 'powerup.wav') +sfx_freeze = load_sound('sounds', 'freeze.wav') +sfx_eat = load_sound('sounds', 'eat_ghost.wav') +if pygame.mixer.get_init(): + music_path = asset_path('sounds', 'pacman.mp3') + try: + pygame.mixer.music.load(music_path) + pygame.mixer.music.play(-1, 0.0) + except (pygame.error, FileNotFoundError) as exc: + print(f"Could not load music {music_path}: {exc}") + +# --- 爆炸視覺素材 --- +try: + _expl_center = pygame.transform.scale(pygame.image.load(asset_path('images', 'explosion_center.png')).convert_alpha(), (30, 30)) + _expl_mid = pygame.transform.scale(pygame.image.load(asset_path('images', 'explosion_mid.png')).convert_alpha(), (30, 30)) + _expl_end = pygame.transform.scale(pygame.image.load(asset_path('images', 'explosion_end.png')).convert_alpha(), (30, 30)) +except (pygame.error, FileNotFoundError) as exc: + print(f"Could not load explosion images: {exc}") + _expl_center = _expl_mid = _expl_end = None + +# --- 類別定義 --- + +class Bomb(pygame.sprite.Sprite): + def __init__(self, x, y): + super().__init__() + self.timer = 50 + self.exploded = False + try: + self._img_normal = pygame.transform.scale(pygame.image.load(asset_path('images', 'bomb.png')).convert_alpha(), (30, 30)) + self._img_flash = pygame.transform.scale(pygame.image.load(asset_path('images', 'bomb_flash.png')).convert_alpha(), (30, 30)) + except (pygame.error, FileNotFoundError) as exc: + print(f"Could not load bomb images: {exc}") + self._img_normal = pygame.Surface([30, 30]); self._img_normal.fill((80,80,80)) + self._img_flash = pygame.Surface([30, 30]); self._img_flash.fill(red) + self.image = self._img_normal + self.rect = self.image.get_rect(topleft=(x, y)) + + def update(self, wall_list): + if self.timer > 0: + self.timer -= 1 + if self.timer < 10: + self.image = self._img_flash if (self.timer // 3) % 2 == 0 else self._img_normal + else: + self.exploded = True + + def get_explosion_cells(self, wall_list): + cells = [] + cx, cy, cell = self.rect.left, self.rect.top, 30 + cells.append((pygame.Rect(cx, cy, cell, cell), 'center', 0)) + for rot, dx, dy in [(0, 1, 0), (180, -1, 0), (-90, 0, 1), (90, 0, -1)]: + for step in range(1, 21): + r = pygame.Rect(cx + dx*cell*step, cy + dy*cell*step, cell, cell) + if not screen.get_rect().contains(r) or any(r.colliderect(w.rect) for w in wall_list): + break + cells.append((r, 'mid', rot)) + return cells + +class PowerUp(pygame.sprite.Sprite): + def __init__(self, x, y, p_type): + super().__init__() + self.type = p_type + try: + img_file = 'images/star.png' if p_type == 'star' else 'images/ice.png' + self.image = pygame.transform.scale(pygame.image.load(img_file).convert_alpha(), (40, 40)) + except: + self.image = pygame.Surface([20, 20]) + self.image.fill(orange if p_type == 'star' else cyan) + self.rect = self.image.get_rect(center=(x, y)) -# This class represents the bar at the bottom that the player controls class Wall(pygame.sprite.Sprite): - # Constructor function def __init__(self,x,y,width,height, color): - # Call the parent's constructor - pygame.sprite.Sprite.__init__(self) - - # Make a blue wall, of the size specified in the parameters - self.image = pygame.Surface([width, height]) - self.image.fill(color) - - # Make our top-left corner the passed-in location. - self.rect = self.image.get_rect() - self.rect.top = y - self.rect.left = x - -# This creates all the walls in room 1 -def setupRoomOne(all_sprites_list): - # Make the walls. (x_pos, y_pos, width, height) - wall_list=pygame.sprite.RenderPlain() - - # This is a list of walls. Each is in the form [x, y, width, height] - walls = [ [0,0,6,600], - [0,0,600,6], - [0,600,606,6], - [600,0,6,606], - [300,0,6,66], - [60,60,186,6], - [360,60,186,6], - [60,120,66,6], - [60,120,6,126], - [180,120,246,6], - [300,120,6,66], - [480,120,66,6], - [540,120,6,126], - [120,180,126,6], - [120,180,6,126], - [360,180,126,6], - [480,180,6,126], - [180,240,6,126], - [180,360,246,6], - [420,240,6,126], - [240,240,42,6], - [324,240,42,6], - [240,240,6,66], - [240,300,126,6], - [360,240,6,66], - [0,300,66,6], - [540,300,66,6], - [60,360,66,6], - [60,360,6,186], - [480,360,66,6], - [540,360,6,186], - [120,420,366,6], - [120,420,6,66], - [480,420,6,66], - [180,480,246,6], - [300,480,6,66], - [120,540,126,6], - [360,540,126,6] - ] - - # Loop through the list. Create the wall, add it to the list - for item in walls: - wall=Wall(item[0],item[1],item[2],item[3],blue) - wall_list.add(wall) - all_sprites_list.add(wall) - - # return our new list - return wall_list + super().__init__() + self.image = pygame.Surface([width, height]); self.image.fill(color) + self.rect = self.image.get_rect(topleft=(x, y)) -def setupGate(all_sprites_list): - gate = pygame.sprite.RenderPlain() - gate.add(Wall(282,242,42,2,white)) - all_sprites_list.add(gate) - return gate - -# This class represents the ball -# It derives from the "Sprite" class in Pygame class Block(pygame.sprite.Sprite): - - # Constructor. Pass in the color of the block, - # and its x and y position def __init__(self, color, width, height): - # Call the parent class (Sprite) constructor - pygame.sprite.Sprite.__init__(self) - - # Create an image of the block, and fill it with a color. - # This could also be an image loaded from the disk. - self.image = pygame.Surface([width, height]) - self.image.fill(white) - self.image.set_colorkey(white) - pygame.draw.ellipse(self.image,color,[0,0,width,height]) - - # Fetch the rectangle object that has the dimensions of the image - # image. - # Update the position of this object by setting the values - # of rect.x and rect.y - self.rect = self.image.get_rect() - -# This class represents the bar at the bottom that the player controls -class Player(pygame.sprite.Sprite): - - # Set speed vector - change_x=0 - change_y=0 - - # Constructor function - def __init__(self,x,y, filename): - # Call the parent's constructor - pygame.sprite.Sprite.__init__(self) - - # Set height, width - self.image = pygame.image.load(filename).convert() - - # Make our top-left corner the passed-in location. + super().__init__() + self.image = pygame.Surface([width, height], pygame.SRCALPHA) + pygame.draw.ellipse(self.image, color, [0,0,width,height]) self.rect = self.image.get_rect() - self.rect.top = y - self.rect.left = x - self.prev_x = x - self.prev_y = y - - # Clear the speed of the player - def prevdirection(self): - self.prev_x = self.change_x - self.prev_y = self.change_y - - # Change the speed of the player - def changespeed(self,x,y): - self.change_x+=x - self.change_y+=y - - # Find a new position for the player - def update(self,walls,gate): - # Get the old position, in case we need to go back to it + +class Player(pygame.sprite.Sprite): + def __init__(self, x, y, filename): + super().__init__() + self.bomb_count = 3 + try: + self.image_normal = pygame.image.load(asset_path(filename)).convert_alpha() + self.image_normal = pygame.transform.scale(self.image_normal, (30, 30)) + except (pygame.error, FileNotFoundError) as exc: + print(f"Could not load player image {filename}: {exc}") + self.image_normal = pygame.Surface([30, 30]); self.image_normal.fill(yellow) - old_x=self.rect.left - new_x=old_x+self.change_x - prev_x=old_x+self.prev_x - self.rect.left = new_x + # 預先生成不同顏色的Pacman版本用於無敵時閃爍 + self.image_colors = [self.image_normal] # 原色 (黃色) + for color in [red, blue, purple, cyan]: # 紅、藍、紫、青 + colored_img = self.image_normal.copy() + colored_img.fill(color, special_flags=pygame.BLEND_RGB_MULT) + self.image_colors.append(colored_img) - old_y=self.rect.top - new_y=old_y+self.change_y - prev_y=old_y+self.prev_y - - # Did this update cause us to hit a wall? - x_collide = pygame.sprite.spritecollide(self, walls, False) - if x_collide: - # Whoops, hit a wall. Go back to the old position - self.rect.left=old_x - # self.rect.top=prev_y - # y_collide = pygame.sprite.spritecollide(self, walls, False) - # if y_collide: - # # Whoops, hit a wall. Go back to the old position - # self.rect.top=old_y - # print('a') + self.image = self.image_normal + self.rect = self.image.get_rect(topleft=(x, y)) + self.change_x = self.change_y = 0 + self.direction = (1, 0) # 預設面向右邊 + + def changespeed(self, x, y): + self.change_x += x; self.change_y += y + + def set_direction(self, x, y): + """只用來設置方向 (按鍵時調用)""" + if x != 0: + self.direction = (x // abs(x), 0) if x != 0 else (0, 0) + elif y != 0: + self.direction = (0, y // abs(y)) if y != 0 else (0, 0) + + def get_rotated_image(self, image, direction): + """根據方向旋轉圖像""" + dx, dy = direction + # (1,0)右=0°, (-1,0)左=180°, (0,-1)上=90°, (0,1)下=270° + if dx == 1: # 右 + return pygame.transform.rotate(image, 0) + elif dx == -1: # 左 + return pygame.transform.rotate(image, 180) + elif dy == -1: # 上 + return pygame.transform.rotate(image, 90) + elif dy == 1: # 下 + return pygame.transform.rotate(image, 270) + return image + + def update(self, walls, gate, invincible=False): + if invincible: + # 無敵時快速切換顏色 (每100ms切換一次,共5種顏色) + color_idx = (pygame.time.get_ticks() // 100) % len(self.image_colors) + base_image = self.image_colors[color_idx] else: + base_image = self.image_normal + + # 根據方向旋轉圖像 + self.image = self.get_rotated_image(base_image, self.direction) + + old_x, old_y = self.rect.topleft + self.rect.left += self.change_x + if pygame.sprite.spritecollide(self, walls, False) or (gate and pygame.sprite.spritecollide(self, gate, False)): + self.rect.left = old_x + self.rect.top += self.change_y + if pygame.sprite.spritecollide(self, walls, False) or (gate and pygame.sprite.spritecollide(self, gate, False)): + self.rect.top = old_y - self.rect.top = new_y - - # Did this update cause us to hit a wall? - y_collide = pygame.sprite.spritecollide(self, walls, False) - if y_collide: - # Whoops, hit a wall. Go back to the old position - self.rect.top=old_y - # self.rect.left=prev_x - # x_collide = pygame.sprite.spritecollide(self, walls, False) - # if x_collide: - # # Whoops, hit a wall. Go back to the old position - # self.rect.left=old_x - # print('b') - - if gate != False: - gate_hit = pygame.sprite.spritecollide(self, gate, False) - if gate_hit: - self.rect.left=old_x - self.rect.top=old_y - -#Inheritime Player klassist class Ghost(Player): - # Change the speed of the ghost - def changespeed(self,list,ghost,turn,steps,l): - try: - z=list[turn][2] - if steps < z: - self.change_x=list[turn][0] - self.change_y=list[turn][1] - steps+=1 - else: - if turn < l: - turn+=1 - elif ghost == "clyde": - turn = 2 - else: - turn = 0 - self.change_x=list[turn][0] - self.change_y=list[turn][1] - steps = 0 - return [turn,steps] - except IndexError: - return [0,0] - -Pinky_directions = [ -[0,-30,4], -[15,0,9], -[0,15,11], -[-15,0,23], -[0,15,7], -[15,0,3], -[0,-15,3], -[15,0,19], -[0,15,3], -[15,0,3], -[0,15,3], -[15,0,3], -[0,-15,15], -[-15,0,7], -[0,15,3], -[-15,0,19], -[0,-15,11], -[15,0,9] -] - -Blinky_directions = [ -[0,-15,4], -[15,0,9], -[0,15,11], -[15,0,3], -[0,15,7], -[-15,0,11], -[0,15,3], -[15,0,15], -[0,-15,15], -[15,0,3], -[0,-15,11], -[-15,0,3], -[0,-15,11], -[-15,0,3], -[0,-15,3], -[-15,0,7], -[0,-15,3], -[15,0,15], -[0,15,15], -[-15,0,3], -[0,15,3], -[-15,0,3], -[0,-15,7], -[-15,0,3], -[0,15,7], -[-15,0,11], -[0,-15,7], -[15,0,5] -] - -Inky_directions = [ -[30,0,2], -[0,-15,4], -[15,0,10], -[0,15,7], -[15,0,3], -[0,-15,3], -[15,0,3], -[0,-15,15], -[-15,0,15], -[0,15,3], -[15,0,15], -[0,15,11], -[-15,0,3], -[0,-15,7], -[-15,0,11], -[0,15,3], -[-15,0,11], -[0,15,7], -[-15,0,3], -[0,-15,3], -[-15,0,3], -[0,-15,15], -[15,0,15], -[0,15,3], -[-15,0,15], -[0,15,11], -[15,0,3], -[0,-15,11], -[15,0,11], -[0,15,3], -[15,0,1], -] - -Clyde_directions = [ -[-30,0,2], -[0,-15,4], -[15,0,5], -[0,15,7], -[-15,0,11], -[0,-15,7], -[-15,0,3], -[0,15,7], -[-15,0,7], -[0,15,15], -[15,0,15], -[0,-15,3], -[-15,0,11], -[0,-15,7], -[15,0,3], -[0,-15,11], -[15,0,9], -] - -pl = len(Pinky_directions)-1 -bl = len(Blinky_directions)-1 -il = len(Inky_directions)-1 -cl = len(Clyde_directions)-1 - -# Call this function so the Pygame library can initialize itself -pygame.init() - -# Create an 606x606 sized screen -screen = pygame.display.set_mode([606, 606]) - -# This is a list of 'sprites.' Each block in the program is -# added to this list. The list is managed by a class called 'RenderPlain.' - - -# Set the title of the window -pygame.display.set_caption('Pacman') - -# Create a surface we can draw on -background = pygame.Surface(screen.get_size()) - -# Used for converting color maps and such -background = background.convert() - -# Fill the screen with a black background -background.fill(black) - - - -clock = pygame.time.Clock() - -pygame.font.init() -font = pygame.font.Font("freesansbold.ttf", 24) + def __init__(self, x, y, filename): + super().__init__(x, y, filename) + self.change_x, self.change_y = 15, 0 + self.steps = 0 + + def update(self, walls, gate, frozen=False): + if frozen: return + old_x, old_y = self.rect.topleft + self.rect.left += self.change_x; self.rect.top += self.change_y + self.steps += 1 + hit_wall = pygame.sprite.spritecollide(self, walls, False) + hit_gate = gate and pygame.sprite.spritecollide(self, gate, False) + if hit_wall or hit_gate or self.steps > random.randint(15, 60): + self.rect.topleft = (old_x, old_y) + dirs = [(15,0), (-15,0), (0,15), (0,-15)] + random.shuffle(dirs) + for dx, dy in dirs: + self.rect.left, self.rect.top = old_x + dx, old_y + dy + blocked = pygame.sprite.spritecollide(self, walls, False) or (gate and pygame.sprite.spritecollide(self, gate, False)) + if not blocked: + self.change_x, self.change_y, self.steps = dx, dy, 0 + self.rect.topleft = (old_x, old_y) + break + else: + self.rect.topleft = (old_x, old_y) + +# --- 地圖建置 --- +def setupRoomOne(all_sprites): + wall_list = pygame.sprite.Group() + walls = [[0,0,6,600],[0,0,600,6],[0,600,606,6],[600,0,6,606],[300,0,6,66],[60,60,186,6],[360,60,186,6],[60,120,66,6],[60,120,6,126],[180,120,246,6],[300,120,6,66],[480,120,66,6],[540,120,6,126],[120,180,126,6],[120,180,6,126],[360,180,126,6],[480,180,6,126],[180,240,6,126],[180,360,246,6],[420,240,6,126],[240,240,42,6],[324,240,42,6],[240,240,6,66],[240,300,126,6],[360,240,6,66],[0,300,66,6],[540,300,66,6],[60,360,66,6],[60,360,6,186],[480,360,66,6],[540,360,6,186],[120,420,366,6],[120,420,6,66],[480,420,6,66],[180,480,246,6],[300,480,6,66],[120,540,126,6],[360,540,126,6]] + for i in walls: + w = Wall(i[0],i[1],i[2],i[3],blue) + wall_list.add(w); all_sprites.add(w) + return wall_list -#default locations for Pacman and monstas -w = 303-16 #Width -p_h = (7*60)+19 #Pacman height -m_h = (4*60)+19 #Monster height -b_h = (3*60)+19 #Binky height -i_w = 303-16-32 #Inky width -c_w = 303+(32-16) #Clyde width +def doNext(message, left, all_sprites_list, block_list, monsta_list, wall_list, gate, total_blocks, score): + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + pygame.quit() + sys.exit() + if event.key == pygame.K_RETURN: + return True + + # Grey background + w = pygame.Surface((400, 200)) + w.set_alpha(10) + w.fill((128, 128, 128)) + screen.blit(w, (100, 200)) + + # Won or lost + font_big = pygame.font.Font(None, 36) + text1 = font_big.render(message, True, white) + text1_rect = text1.get_rect(center=(300, 245)) + screen.blit(text1, text1_rect) + + text2 = font_big.render("To play again, press ENTER.", True, white) + text2_rect = text2.get_rect(center=(300, 290)) + screen.blit(text2, text2_rect) + text3 = font_big.render("To quit, press ESCAPE.", True, white) + text3_rect = text3.get_rect(center=(300, 335)) + screen.blit(text3, text3_rect) + + pygame.display.flip() + clock.tick(10) def startGame(): - - all_sprites_list = pygame.sprite.RenderPlain() - - block_list = pygame.sprite.RenderPlain() - - monsta_list = pygame.sprite.RenderPlain() - - pacman_collide = pygame.sprite.RenderPlain() - - wall_list = setupRoomOne(all_sprites_list) - - gate = setupGate(all_sprites_list) - - - p_turn = 0 - p_steps = 0 - - b_turn = 0 - b_steps = 0 - - i_turn = 0 - i_steps = 0 - - c_turn = 0 - c_steps = 0 - - - # Create the player paddle object - Pacman = Player( w, p_h, "images/Trollman.png" ) - all_sprites_list.add(Pacman) - pacman_collide.add(Pacman) - - Blinky=Ghost( w, b_h, "images/Blinky.png" ) - monsta_list.add(Blinky) - all_sprites_list.add(Blinky) - - Pinky=Ghost( w, m_h, "images/Pinky.png" ) - monsta_list.add(Pinky) - all_sprites_list.add(Pinky) - - Inky=Ghost( i_w, m_h, "images/Inky.png" ) - monsta_list.add(Inky) - all_sprites_list.add(Inky) - - Clyde=Ghost( c_w, m_h, "images/Clyde.png" ) - monsta_list.add(Clyde) - all_sprites_list.add(Clyde) - - # Draw the grid - for row in range(19): - for column in range(19): - if (row == 7 or row == 8) and (column == 8 or column == 9 or column == 10): - continue - else: - block = Block(yellow, 4, 4) - - # Set a random location for the block - block.rect.x = (30*column+6)+26 - block.rect.y = (30*row+6)+26 - - b_collide = pygame.sprite.spritecollide(block, wall_list, False) - p_collide = pygame.sprite.spritecollide(block, pacman_collide, False) - if b_collide: - continue - elif p_collide: - continue - else: - # Add the block to the list of objects - block_list.add(block) - all_sprites_list.add(block) - - bll = len(block_list) - - score = 0 - - done = False - - i = 0 - - while done == False: - # ALL EVENT PROCESSING SHOULD GO BELOW THIS COMMENT - for event in pygame.event.get(): - if event.type == pygame.QUIT: - done=True - - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_LEFT: - Pacman.changespeed(-30,0) - if event.key == pygame.K_RIGHT: - Pacman.changespeed(30,0) - if event.key == pygame.K_UP: - Pacman.changespeed(0,-30) - if event.key == pygame.K_DOWN: - Pacman.changespeed(0,30) - - if event.type == pygame.KEYUP: - if event.key == pygame.K_LEFT: - Pacman.changespeed(30,0) - if event.key == pygame.K_RIGHT: - Pacman.changespeed(-30,0) - if event.key == pygame.K_UP: - Pacman.changespeed(0,30) - if event.key == pygame.K_DOWN: - Pacman.changespeed(0,-30) - - # ALL EVENT PROCESSING SHOULD GO ABOVE THIS COMMENT - - # ALL GAME LOGIC SHOULD GO BELOW THIS COMMENT - Pacman.update(wall_list,gate) - - returned = Pinky.changespeed(Pinky_directions,False,p_turn,p_steps,pl) - p_turn = returned[0] - p_steps = returned[1] - Pinky.changespeed(Pinky_directions,False,p_turn,p_steps,pl) - Pinky.update(wall_list,False) - - returned = Blinky.changespeed(Blinky_directions,False,b_turn,b_steps,bl) - b_turn = returned[0] - b_steps = returned[1] - Blinky.changespeed(Blinky_directions,False,b_turn,b_steps,bl) - Blinky.update(wall_list,False) - - returned = Inky.changespeed(Inky_directions,False,i_turn,i_steps,il) - i_turn = returned[0] - i_steps = returned[1] - Inky.changespeed(Inky_directions,False,i_turn,i_steps,il) - Inky.update(wall_list,False) - - returned = Clyde.changespeed(Clyde_directions,"clyde",c_turn,c_steps,cl) - c_turn = returned[0] - c_steps = returned[1] - Clyde.changespeed(Clyde_directions,"clyde",c_turn,c_steps,cl) - Clyde.update(wall_list,False) - - # See if the Pacman block has collided with anything. - blocks_hit_list = pygame.sprite.spritecollide(Pacman, block_list, True) - - # Check the list of collisions. - if len(blocks_hit_list) > 0: - score +=len(blocks_hit_list) - - # ALL GAME LOGIC SHOULD GO ABOVE THIS COMMENT - - # ALL CODE TO DRAW SHOULD GO BELOW THIS COMMENT - screen.fill(black) + all_sprites = pygame.sprite.Group() + block_list = pygame.sprite.Group() + monsta_list = pygame.sprite.Group() + bomb_list = pygame.sprite.Group() + power_list = pygame.sprite.Group() + + wall_list = setupRoomOne(all_sprites) + gate = pygame.sprite.Group() + gate.add(Wall(282,242,42,2,white)) + all_sprites.add(gate) + Pacman = Player(287, 439, "images/pacman.png"); all_sprites.add(Pacman) + + valid_points = [] + for r in range(19): + for c in range(19): + if (r == 7 or r == 8) and (8 <= c <= 10): continue + b = Block(yellow, 4, 4); b.rect.x = (30*c+32); b.rect.y = (30*r+32) + if not pygame.sprite.spritecollide(b, wall_list, False): + block_list.add(b); all_sprites.add(b) + valid_points.append(b.rect.center) + + ghost_imgs = ["images/Blinky.png", "images/Pinky.png", "images/Inky.png", "images/Clyde.png"] + def point_is_far_from_player(point, distance=100): + return abs(point[0] - Pacman.rect.centerx) > distance or abs(point[1] - Pacman.rect.centery) > distance + + ghost_points = [pt for pt in valid_points if point_is_far_from_player(pt)] or valid_points[:] + random.shuffle(ghost_points) + for i in range(min(4, len(ghost_points))): + pt = ghost_points[i] + # 调整初始位置计算,避免偏差 + ghost_x = pt[0] - 15 + ghost_y = pt[1] - 15 + g = Ghost(int(ghost_x), int(ghost_y), ghost_imgs[i]); monsta_list.add(g); all_sprites.add(g) + + ADD_GHOST = pygame.USEREVENT + 1; pygame.time.set_timer(ADD_GHOST, 15000) + ADD_POWER = pygame.USEREVENT + 2; pygame.time.set_timer(ADD_POWER, 8000) + + total_blocks = len(block_list) + score = 0; inv_timer = 0; froz_timer = 0 + active_explosions = []; space_held = False + font = pygame.font.Font(None, 32) + + def finish_game(message): + pygame.time.set_timer(ADD_GHOST, 0) + pygame.time.set_timer(ADD_POWER, 0) + return doNext(message, 0, all_sprites, block_list, monsta_list, wall_list, gate, total_blocks, score) + + def choose_power_block(): + candidates = [] + for block in block_list: + if block.rect.colliderect(Pacman.rect): + continue + if any(block.rect.colliderect(ghost.rect) for ghost in monsta_list): + continue + candidates.append(block) + return random.choice(candidates) if candidates else None + + def choose_ghost_point(): + candidates = [] + for point in valid_points: + ghost_rect = pygame.Rect(point[0] - 15, point[1] - 15, 30, 30) + if not point_is_far_from_player(point): + continue + if any(ghost_rect.colliderect(ghost.rect) for ghost in monsta_list): + continue + if any(ghost_rect.colliderect(power.rect) for power in power_list): + continue + candidates.append(point) + return random.choice(candidates) if candidates else None + + while True: + is_inv = inv_timer > 0; is_froz = froz_timer > 0 + if inv_timer > 0: inv_timer -= 1 + if froz_timer > 0: froz_timer -= 1 + + for event in pygame.event.get(): + if event.type == pygame.QUIT: pygame.quit(); sys.exit() + if event.type == ADD_POWER and len(power_list) < 3: + target_block = choose_power_block() + if target_block: + pt = target_block.rect.center + target_block.kill() + p = PowerUp(pt[0], pt[1], random.choice(['star', 'ice'])) + power_list.add(p); all_sprites.add(p) + if event.type == ADD_GHOST and len(monsta_list) < MAX_GHOSTS: + pt = choose_ghost_point() + if pt: + g = Ghost(pt[0]-15, pt[1]-15, random.choice(ghost_imgs)); monsta_list.add(g); all_sprites.add(g) + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_LEFT: + Pacman.changespeed(-30,0) + Pacman.set_direction(-30, 0) + if event.key == pygame.K_RIGHT: + Pacman.changespeed(30,0) + Pacman.set_direction(30, 0) + if event.key == pygame.K_UP: + Pacman.changespeed(0,-30) + Pacman.set_direction(0, -30) + if event.key == pygame.K_DOWN: + Pacman.changespeed(0,30) + Pacman.set_direction(0, 30) + if event.type == pygame.KEYUP: + if event.key == pygame.K_LEFT: Pacman.changespeed(30,0) + if event.key == pygame.K_RIGHT: Pacman.changespeed(-30,0) + if event.key == pygame.K_UP: Pacman.changespeed(0,30) + if event.key == pygame.K_DOWN: Pacman.changespeed(0,-30) + + # 炸彈邏輯 (左 Ctrl) + keys = pygame.key.get_pressed() + if keys[pygame.K_LCTRL] and not space_held and Pacman.bomb_count > 0: + b = Bomb(Pacman.rect.left, Pacman.rect.top); bomb_list.add(b); all_sprites.add(b) + Pacman.bomb_count -= 1; space_held = True + if not keys[pygame.K_LCTRL]: space_held = False + + Pacman.update(wall_list, gate, is_inv) + for g in monsta_list: g.update(wall_list, gate, is_froz) - wall_list.draw(screen) - gate.draw(screen) - all_sprites_list.draw(screen) - monsta_list.draw(screen) - - text=font.render("Score: "+str(score)+"/"+str(bll), True, red) - screen.blit(text, [10, 10]) - - if score == bll: - doNext("Congratulations, you won!",145,all_sprites_list,block_list,monsta_list,pacman_collide,wall_list,gate) + # 炸彈爆炸判定與音效 + for b in list(bomb_list): + b.update(wall_list) + if b.exploded: + if bomb_sound: bomb_sound.play() # 炸彈音效在此播放 + cells = b.get_explosion_cells(wall_list) + active_explosions.append((cells, 5)) + for r, _, _ in cells: + for g in list(monsta_list): + if g.rect.colliderect(r): g.kill(); score += 50 + if Pacman.rect.colliderect(r) and not is_inv: + return finish_game("Game Over") + b.kill() + + # 道具碰撞與音效 + p_hits = pygame.sprite.spritecollide(Pacman, power_list, True) + score += len(p_hits) + for p in p_hits: + if sfx_powerup: sfx_powerup.play() + if p.type == 'star': inv_timer = POWER_DURATION + if p.type == 'ice': + froz_timer = POWER_DURATION + if sfx_freeze: sfx_freeze.play() + + score += len(pygame.sprite.spritecollide(Pacman, block_list, True)) + + # 鬼魂碰撞 + m_hits = pygame.sprite.spritecollide(Pacman, monsta_list, False) + if m_hits: + if is_inv: + for m in m_hits: + m.kill(); score += 100 + if sfx_eat: sfx_eat.play() + else: + return finish_game("Game Over") - monsta_hit_list = pygame.sprite.spritecollide(Pacman, monsta_list, False) + if len(block_list) == 0 and len(power_list) == 0: + return finish_game("Congratulations, you won!") - if monsta_hit_list: - doNext("Game Over",235,all_sprites_list,block_list,monsta_list,pacman_collide,wall_list,gate) + # 繪製 + screen.fill(black) + all_sprites.draw(screen) + + # DEBUG: 顯示碰撞框 (可視化調試) + # pygame.draw.rect(screen, green, Pacman.rect, 2) # Pacman碰撞框 + # for g in monsta_list: pygame.draw.rect(screen, red, g.rect, 2) # Ghost碰撞框 + + # 繪製爆炸效果 + for cells, frames in list(active_explosions): + for r, ct, rot in cells: + if ct == 'center' and _expl_center: screen.blit(_expl_center, r) + elif ct == 'mid' and _expl_mid: screen.blit(pygame.transform.rotate(_expl_mid, rot), r) + else: pygame.draw.rect(screen, (255, 200, 0), r) + idx = active_explosions.index((cells, frames)) + if frames > 1: active_explosions[idx] = (cells, frames - 1) + else: active_explosions.pop(idx) + + # 顯示文字 + pellets_left = len(block_list) + len(power_list) + pellets_eaten = total_blocks - pellets_left + s_text = font.render(f"Score: {score} Pellets: {pellets_eaten}/{total_blocks} Bombs: {Pacman.bomb_count}", True, red) + screen.blit(s_text, [10, 10]) + if is_inv: screen.blit(font.render("INVINCIBLE", True, orange), [280, 10]) + if is_froz: screen.blit(font.render("GHOSTS FROZEN", True, cyan), [410, 10]) + + pygame.display.flip() + clock.tick(10) - # ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT - - pygame.display.flip() - - clock.tick(10) - -def doNext(message,left,all_sprites_list,block_list,monsta_list,pacman_collide,wall_list,gate): - while True: - # ALL EVENT PROCESSING SHOULD GO BELOW THIS COMMENT - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - pygame.quit() - if event.key == pygame.K_RETURN: - del all_sprites_list - del block_list - del monsta_list - del pacman_collide - del wall_list - del gate - startGame() - - #Grey background - w = pygame.Surface((400,200)) # the size of your rect - w.set_alpha(10) # alpha level - w.fill((128,128,128)) # this fills the entire surface - screen.blit(w, (100,200)) # (0,0) are the top-left coordinates - - #Won or lost - text1=font.render(message, True, white) - screen.blit(text1, [left, 233]) - - text2=font.render("To play again, press ENTER.", True, white) - screen.blit(text2, [135, 303]) - text3=font.render("To quit, press ESCAPE.", True, white) - screen.blit(text3, [165, 333]) - - pygame.display.flip() - - clock.tick(10) - -startGame() - -pygame.quit() \ No newline at end of file +if __name__ == "__main__": + while True: startGame() diff --git "a/pacman\344\277\256\346\224\271.py" "b/pacman\344\277\256\346\224\271.py" new file mode 100644 index 0000000..d233625 --- /dev/null +++ "b/pacman\344\277\256\346\224\271.py" @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat May 9 10:30:25 2026 + +@author: linhengan +""" + +import pygame +import sys +import random + +black = (0, 0, 0) +white = (255, 255, 255) +blue = (0, 0, 255) +green = (0, 255, 0) +red = (255, 0, 0) +purple = (255, 0, 255) +yellow = (255, 255, 0) +orange = (255, 165, 0) # 橘色給無敵星星 +cyan = (0, 255, 255) # 青色給冰凍道具 + +# 設定鬼魂數量上限 +MAX_GHOSTS = 8 + +try: + Trollicon = pygame.image.load('images/Trollman.png') + pygame.display.set_icon(Trollicon) +except pygame.error: + pass + +# Add music +pygame.mixer.init() +try: + pygame.mixer.music.load('pacman.mp3') + pygame.mixer.music.play(-1, 0.0) +except pygame.error: + pass + +class Wall(pygame.sprite.Sprite): + def __init__(self, x, y, width, height, color): + pygame.sprite.Sprite.__init__(self) + self.image = pygame.Surface([width, height]) + self.image.fill(color) + self.rect = self.image.get_rect() + self.rect.top = y + self.rect.left = x + +def setupRoomOne(all_sprites_list): + wall_list = pygame.sprite.Group() + walls = [ + [0, 0, 6, 600], + [0, 0, 600, 6], + [0, 600, 606, 6], + [600, 0, 6, 606], + [300, 0, 6, 66], + [60, 60, 186, 6], + [360, 60, 186, 6], + [60, 120, 66, 6], + [60, 120, 6, 126], + [180, 120, 246, 6], + [300, 120, 6, 66], + [480, 120, 66, 6], + [540, 120, 6, 126], + [120, 180, 126, 6], + [120, 180, 6, 126], + [360, 180, 126, 6], + [480, 180, 6, 126], + [180, 240, 6, 126], + [180, 360, 246, 6], + [420, 240, 6, 126], + [240, 240, 42, 6], + [324, 240, 42, 6], + [240, 240, 6, 66], + [240, 300, 126, 6], + [360, 240, 6, 66], + [0, 300, 66, 6], + [540, 300, 66, 6], + [60, 360, 66, 6], + [60, 360, 6, 186], + [480, 360, 66, 6], + [540, 360, 6, 186], + [120, 420, 366, 6], + [120, 420, 6, 66], + [480, 420, 6, 66], + [180, 480, 246, 6], + [300, 480, 6, 66], + [120, 540, 126, 6], + [360, 540, 126, 6] + ] + for item in walls: + wall = Wall(item[0], item[1], item[2], item[3], blue) + wall_list.add(wall) + all_sprites_list.add(wall) + return wall_list + +def setupGate(all_sprites_list): + gate = pygame.sprite.Group() + gate.add(Wall(282, 242, 42, 2, white)) + all_sprites_list.add(gate) + return gate + +class Block(pygame.sprite.Sprite): + def __init__(self, color, width, height): + pygame.sprite.Sprite.__init__(self) + self.image = pygame.Surface([width, height]) + self.image.fill(white) + self.image.set_colorkey(white) + pygame.draw.ellipse(self.image, color, [0, 0, width, height]) + self.rect = self.image.get_rect() + +class PowerUp(pygame.sprite.Sprite): + def __init__(self, color, width, height, power_type): + pygame.sprite.Sprite.__init__(self) + self.image = pygame.Surface([width, height]) + self.image.fill(white) + self.image.set_colorkey(white) + pygame.draw.ellipse(self.image, color, [0, 0, width, height]) + self.rect = self.image.get_rect() + self.type = power_type + +class Player(pygame.sprite.Sprite): + change_x = 0 + change_y = 0 + def __init__(self, x, y, filename): + pygame.sprite.Sprite.__init__(self) + try: + self.image = pygame.image.load(filename).convert() + except pygame.error: + self.image = pygame.Surface([30, 30]) + self.image.fill(yellow) + self.rect = self.image.get_rect() + self.rect.top = y + self.rect.left = x + self.prev_x = x + self.prev_y = y + + def prevdirection(self): + self.prev_x = self.change_x + self.prev_y = self.change_y + + def changespeed(self, x, y): + self.change_x += x + self.change_y += y + + def update(self, walls, gate): + old_x = self.rect.left + new_x = old_x + self.change_x + self.rect.left = new_x + + old_y = self.rect.top + new_y = old_y + self.change_y + + x_collide = pygame.sprite.spritecollide(self, walls, False) + if x_collide: + self.rect.left = old_x + else: + self.rect.top = new_y + y_collide = pygame.sprite.spritecollide(self, walls, False) + if y_collide: + self.rect.top = old_y + + if gate != False: + gate_hit = pygame.sprite.spritecollide(self, gate, False) + if gate_hit: + self.rect.left = old_x + self.rect.top = old_y + +class Ghost(Player): + def __init__(self, x, y, filename): + super().__init__(x, y, filename) + self.steps_taken = 0 + dirs = [(15, 0), (-15, 0), (0, 15), (0, -15)] + self.change_x, self.change_y = random.choice(dirs) + + def update(self, walls, gate=False, is_frozen=False): + if is_frozen: + return + old_x = self.rect.left + old_y = self.rect.top + self.rect.left += self.change_x + self.rect.top += self.change_y + + collide = pygame.sprite.spritecollide(self, walls, False) + if gate: + collide_gate = pygame.sprite.spritecollide(self, gate, False) + if collide_gate: + collide.extend(collide_gate) + + self.steps_taken += 1 + + if collide or self.steps_taken > random.randint(15, 60): + if collide: + self.rect.left = old_x + self.rect.top = old_y + + dirs = [(15, 0), (-15, 0), (0, 15), (0, -15)] + random.shuffle(dirs) + + for dx, dy in dirs: + self.rect.left = old_x + dx + self.rect.top = old_y + dy + if not pygame.sprite.spritecollide(self, walls, False) and not (gate and pygame.sprite.spritecollide(self, gate, False)): + self.change_x = dx + self.change_y = dy + self.steps_taken = 0 + self.rect.left = old_x + self.rect.top = old_y + break + else: + self.rect.left = old_x + self.rect.top = old_y + +pygame.init() +screen = pygame.display.set_mode([606, 606]) +pygame.display.set_caption('Pacman') +background = pygame.Surface(screen.get_size()).convert() +background.fill(black) +clock = pygame.time.Clock() + +pygame.font.init() +try: + font = pygame.font.Font("freesansbold.ttf", 24) +except OSError: + font = pygame.font.Font(None, 24) + +w = 303 - 16 +p_h = (7 * 60) + 19 + +def startGame(): + all_sprites_list = pygame.sprite.Group() + block_list = pygame.sprite.Group() + powerup_list = pygame.sprite.Group() + monsta_list = pygame.sprite.Group() + pacman_collide = pygame.sprite.Group() + + wall_list = setupRoomOne(all_sprites_list) + gate = setupGate(all_sprites_list) + + Pacman = Player(w, p_h, "images/Trollman.png") + all_sprites_list.add(Pacman) + pacman_collide.add(Pacman) + + valid_spawn_points = [] + + for row in range(19): + for column in range(19): + if (row == 7 or row == 8) and (column == 8 or column == 9 or column == 10): + continue + else: + block = Block(yellow, 4, 4) + block.rect.x = (30 * column + 6) + 26 + block.rect.y = (30 * row + 6) + 26 + + b_collide = pygame.sprite.spritecollide(block, wall_list, False) + p_collide = pygame.sprite.spritecollide(block, pacman_collide, False) + if b_collide or p_collide: + continue + else: + block_list.add(block) + all_sprites_list.add(block) + + ghost_x = block.rect.x - 13 + ghost_y = block.rect.y - 13 + if abs(ghost_x - w) > 100 or abs(ghost_y - p_h) > 100: + valid_spawn_points.append((ghost_x, ghost_y)) + + # 開局預設先生成 2 個道具,讓玩家一開始有目標 + if len(block_list) >= 2: + blocks_to_replace = random.sample(list(block_list), 2) + for i, b in enumerate(blocks_to_replace): + block_list.remove(b) + all_sprites_list.remove(b) + ptype = 'star' if i == 0 else 'ice' + pcolor = orange if ptype == 'star' else cyan + powerup = PowerUp(pcolor, 14, 14, ptype) + powerup.rect.center = b.rect.center + powerup_list.add(powerup) + all_sprites_list.add(powerup) + + random.shuffle(valid_spawn_points) + spawns = valid_spawn_points[:4] + + Blinky = Ghost(spawns[0][0], spawns[0][1], "images/Blinky.png") + Pinky = Ghost(spawns[1][0], spawns[1][1], "images/Pinky.png") + Inky = Ghost(spawns[2][0], spawns[2][1], "images/Inky.png") + Clyde = Ghost(spawns[3][0], spawns[3][1], "images/Clyde.png") + + for ghost in [Blinky, Pinky, Inky, Clyde]: + monsta_list.add(ghost) + all_sprites_list.add(ghost) + + score = 0 + done = False + + # 定義事件與計時器 + ADD_GHOST_EVENT = pygame.USEREVENT + 1 + pygame.time.set_timer(ADD_GHOST_EVENT, 15000) # 每 15 秒增加一隻鬼魂 + + ADD_POWERUP_EVENT = pygame.USEREVENT + 2 + pygame.time.set_timer(ADD_POWERUP_EVENT, 8000) # 每 8 秒嘗試生成一個新道具 + + ghost_images = ["images/Blinky.png", "images/Pinky.png", "images/Inky.png", "images/Clyde.png"] + + is_invincible = False + invincible_timer = 0 + is_frozen = False + frozen_timer = 0 + POWER_DURATION = 5000 + + while not done: + current_time = pygame.time.get_ticks() + + if is_invincible and (current_time - invincible_timer > POWER_DURATION): + is_invincible = False + if is_frozen and (current_time - frozen_timer > POWER_DURATION): + is_frozen = False + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + done = True + + if event.type == ADD_GHOST_EVENT: + if len(monsta_list) < MAX_GHOSTS: + safe_points = [] + for pt in valid_spawn_points: + dist_x = abs(pt[0] - Pacman.rect.left) + dist_y = abs(pt[1] - Pacman.rect.top) + if dist_x > 100 or dist_y > 100: + safe_points.append(pt) + + if safe_points: + spawn_pt = random.choice(safe_points) + img = random.choice(ghost_images) + new_ghost = Ghost(spawn_pt[0], spawn_pt[1], img) + monsta_list.add(new_ghost) + all_sprites_list.add(new_ghost) + + # 新增:動態生成道具邏輯 + if event.type == ADD_POWERUP_EVENT: + # 確保畫面上道具不超過 5 個,且還有普通豆子可以替換 + if len(powerup_list) < 5 and len(block_list) > 0: + target_block = random.choice(block_list.sprites()) + + # 隨機決定道具種類 + ptype = random.choice(['star', 'ice']) + pcolor = orange if ptype == 'star' else cyan + + new_powerup = PowerUp(pcolor, 14, 14, ptype) + new_powerup.rect.center = target_block.rect.center + + # 進行替換 + block_list.remove(target_block) + all_sprites_list.remove(target_block) + powerup_list.add(new_powerup) + all_sprites_list.add(new_powerup) + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_LEFT: + Pacman.changespeed(-30, 0) + if event.key == pygame.K_RIGHT: + Pacman.changespeed(30, 0) + if event.key == pygame.K_UP: + Pacman.changespeed(0, -30) + if event.key == pygame.K_DOWN: + Pacman.changespeed(0, 30) + + if event.type == pygame.KEYUP: + if event.key == pygame.K_LEFT: + Pacman.changespeed(30, 0) + if event.key == pygame.K_RIGHT: + Pacman.changespeed(-30, 0) + if event.key == pygame.K_UP: + Pacman.changespeed(0, 30) + if event.key == pygame.K_DOWN: + Pacman.changespeed(0, -30) + + Pacman.update(wall_list, gate) + + for monsta in monsta_list: + monsta.update(wall_list, False, is_frozen) + + blocks_hit_list = pygame.sprite.spritecollide(Pacman, block_list, True) + if len(blocks_hit_list) > 0: + score += len(blocks_hit_list) + + powerups_hit_list = pygame.sprite.spritecollide(Pacman, powerup_list, True) + for powerup in powerups_hit_list: + if powerup.type == 'star': + is_invincible = True + invincible_timer = pygame.time.get_ticks() + elif powerup.type == 'ice': + is_frozen = True + frozen_timer = pygame.time.get_ticks() + + screen.fill(black) + + wall_list.draw(screen) + gate.draw(screen) + all_sprites_list.draw(screen) + monsta_list.draw(screen) + + text = font.render("Score: " + str(score), True, red) + screen.blit(text, [10, 10]) + + status_msg = "" + if is_invincible: + status_msg += "INVINCIBLE! " + if is_frozen: + status_msg += "FROZEN! " + if status_msg: + status_text = font.render(status_msg, True, yellow) + screen.blit(status_text, [300, 10]) + + if len(block_list) == 0 and len(powerup_list) == 0: + pygame.time.set_timer(ADD_GHOST_EVENT, 0) + pygame.time.set_timer(ADD_POWERUP_EVENT, 0) + doNext("Congratulations, you won!", 145, all_sprites_list, block_list, monsta_list, pacman_collide, wall_list, gate) + + monsta_hit_list = pygame.sprite.spritecollide(Pacman, monsta_list, False) + if monsta_hit_list: + if is_invincible: + for m in monsta_hit_list: + m.kill() + score += 50 + else: + pygame.time.set_timer(ADD_GHOST_EVENT, 0) + pygame.time.set_timer(ADD_POWERUP_EVENT, 0) + doNext("Game Over", 235, all_sprites_list, block_list, monsta_list, pacman_collide, wall_list, gate) + + pygame.display.flip() + clock.tick(10) + +def doNext(message, left, all_sprites_list, block_list, monsta_list, pacman_collide, wall_list, gate): + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + pygame.quit() + sys.exit() + if event.key == pygame.K_RETURN: + startGame() + return + + w = pygame.Surface((400, 200)) + w.set_alpha(10) + w.fill((128, 128, 128)) + screen.blit(w, (100, 200)) + + text1 = font.render(message, True, white) + screen.blit(text1, [left, 233]) + + text2 = font.render("To play again, press ENTER.", True, white) + screen.blit(text2, [135, 303]) + text3 = font.render("To quit, press ESCAPE.", True, white) + screen.blit(text3, [165, 333]) + + pygame.display.flip() + clock.tick(10) + +startGame() +pygame.quit() \ No newline at end of file diff --git a/sounds/bomb_sound.mp3 b/sounds/bomb_sound.mp3 new file mode 100644 index 0000000..44cd2a0 Binary files /dev/null and b/sounds/bomb_sound.mp3 differ diff --git a/sounds/eat_ghost.wav b/sounds/eat_ghost.wav new file mode 100644 index 0000000..9f02834 Binary files /dev/null and b/sounds/eat_ghost.wav differ diff --git a/sounds/freeze.wav b/sounds/freeze.wav new file mode 100644 index 0000000..c6f0f4d Binary files /dev/null and b/sounds/freeze.wav differ diff --git a/pacman.mp3 b/sounds/pacman.mp3 similarity index 100% rename from pacman.mp3 rename to sounds/pacman.mp3 diff --git a/sounds/powerup.wav b/sounds/powerup.wav new file mode 100644 index 0000000..644498f Binary files /dev/null and b/sounds/powerup.wav differ diff --git "a/\345\242\236\345\212\240\351\201\223\345\205\267" "b/\345\242\236\345\212\240\351\201\223\345\205\267" new file mode 100644 index 0000000..fd141be --- /dev/null +++ "b/\345\242\236\345\212\240\351\201\223\345\205\267" @@ -0,0 +1,291 @@ +import pygame +import sys +import random + +# --- 顏色設定 --- +black = (0,0,0) +white = (255,255,255) +blue = (0,0,255) +green = (0,255,0) +red = (255,0,0) +purple = (255,0,255) +yellow = (255, 255, 0) +orange = (255, 165, 0) +cyan = (0, 255, 255) + +MAX_GHOSTS = 8 +POWER_DURATION = 50 # 50 幀 (約 5 秒) + +# --- 基礎初始化 --- +pygame.init() +pygame.mixer.init() +screen = pygame.display.set_mode([606, 606]) +pygame.display.set_caption('Pacman - Bomb & Powerups') +clock = pygame.time.Clock() + +# --- 資源載入 (包含原始炸彈音效) --- +try: + bomb_sound = pygame.mixer.Sound('sounds/bomb_sound.mp3') + sfx_powerup = pygame.mixer.Sound('powerup.wav') + sfx_freeze = pygame.mixer.Sound('freeze.wav') + sfx_eat = pygame.mixer.Sound('eat_ghost.wav') + pygame.mixer.music.load('sounds/pacman.mp3') + pygame.mixer.music.play(-1, 0.0) +except: + bomb_sound = sfx_powerup = sfx_freeze = sfx_eat = None + +# --- 爆炸視覺素材 --- +try: + _expl_center = pygame.transform.scale(pygame.image.load('images/explosion_center.png').convert_alpha(), (30, 30)) + _expl_mid = pygame.transform.scale(pygame.image.load('images/explosion_mid.png').convert_alpha(), (30, 30)) + _expl_end = pygame.transform.scale(pygame.image.load('images/explosion_end.png').convert_alpha(), (30, 30)) +except: + _expl_center = _expl_mid = _expl_end = None + +# --- 類別定義 --- + +class Bomb(pygame.sprite.Sprite): + def __init__(self, x, y): + super().__init__() + self.timer = 50 + self.exploded = False + try: + self._img_normal = pygame.transform.scale(pygame.image.load('images/bomb.png').convert_alpha(), (30, 30)) + self._img_flash = pygame.transform.scale(pygame.image.load('images/bomb_flash.png').convert_alpha(), (30, 30)) + except: + self._img_normal = pygame.Surface([30, 30]); self._img_normal.fill((80,80,80)) + self._img_flash = pygame.Surface([30, 30]); self._img_flash.fill(red) + self.image = self._img_normal + self.rect = self.image.get_rect(topleft=(x, y)) + + def update(self, wall_list): + if self.timer > 0: + self.timer -= 1 + if self.timer < 10: + self.image = self._img_flash if (self.timer // 3) % 2 == 0 else self._img_normal + else: + self.exploded = True + + def get_explosion_cells(self, wall_list): + cells = [] + cx, cy, cell = self.rect.left, self.rect.top, 30 + cells.append((pygame.Rect(cx, cy, cell, cell), 'center', 0)) + for rot, dx, dy in [(0, 1, 0), (180, -1, 0), (-90, 0, 1), (90, 0, -1)]: + for step in range(1, 21): + r = pygame.Rect(cx + dx*cell*step, cy + dy*cell*step, cell, cell) + if not screen.get_rect().contains(r) or any(r.colliderect(w.rect) for w in wall_list): + break + cells.append((r, 'mid', rot)) + return cells + +class PowerUp(pygame.sprite.Sprite): + def __init__(self, x, y, p_type): + super().__init__() + self.type = p_type + try: + img_file = 'star.png' if p_type == 'star' else 'ice.png' + self.image = pygame.transform.scale(pygame.image.load(img_file).convert_alpha(), (25, 25)) + except: + self.image = pygame.Surface([20, 20]) + self.image.fill(orange if p_type == 'star' else cyan) + self.rect = self.image.get_rect(center=(x, y)) + +class Wall(pygame.sprite.Sprite): + def __init__(self,x,y,width,height, color): + super().__init__() + self.image = pygame.Surface([width, height]); self.image.fill(color) + self.rect = self.image.get_rect(topleft=(x, y)) + +class Block(pygame.sprite.Sprite): + def __init__(self, color, width, height): + super().__init__() + self.image = pygame.Surface([width, height], pygame.SRCALPHA) + pygame.draw.ellipse(self.image, color, [0,0,width,height]) + self.rect = self.image.get_rect() + +class Player(pygame.sprite.Sprite): + def __init__(self, x, y, filename): + super().__init__() + self.bomb_count = 3 + try: + self.image_normal = pygame.image.load(filename).convert_alpha() + self.image_normal = pygame.transform.scale(self.image_normal, (30, 30)) + except: + self.image_normal = pygame.Surface([30, 30]); self.image_normal.fill(yellow) + self.image = self.image_normal + self.rect = self.image.get_rect(topleft=(x, y)) + self.change_x = self.change_y = 0 + + def changespeed(self, x, y): + self.change_x += x; self.change_y += y + + def update(self, walls, gate, invincible=False): + if invincible and (pygame.time.get_ticks() // 200) % 2 == 0: + self.image = pygame.Surface([30, 30]); self.image.fill(orange) + else: + self.image = self.image_normal + + old_x, old_y = self.rect.topleft + self.rect.left += self.change_x + if pygame.sprite.spritecollide(self, walls, False) or (gate and pygame.sprite.spritecollide(self, gate, False)): + self.rect.left = old_x + self.rect.top += self.change_y + if pygame.sprite.spritecollide(self, walls, False) or (gate and pygame.sprite.spritecollide(self, gate, False)): + self.rect.top = old_y + +class Ghost(Player): + def __init__(self, x, y, filename): + super().__init__(x, y, filename) + self.change_x, self.change_y = 15, 0 + self.steps = 0 + + def update(self, walls, gate, frozen=False): + if frozen: return + old_x, old_y = self.rect.topleft + self.rect.left += self.change_x; self.rect.top += self.change_y + self.steps += 1 + if pygame.sprite.spritecollide(self, walls, False) or self.steps > random.randint(15, 60): + self.rect.topleft = (old_x, old_y) + dirs = [(15,0), (-15,0), (0,15), (0,-15)] + random.shuffle(dirs) + for dx, dy in dirs: + self.rect.left, self.rect.top = old_x + dx, old_y + dy + if not pygame.sprite.spritecollide(self, walls, False): + self.change_x, self.change_y, self.steps = dx, dy, 0 + self.rect.topleft = (old_x, old_y) + break + +# --- 地圖建置 --- +def setupRoomOne(all_sprites): + wall_list = pygame.sprite.Group() + walls = [[0,0,6,600],[0,0,600,6],[0,600,606,6],[600,0,6,606],[300,0,6,66],[60,60,186,6],[360,60,186,6],[60,120,66,6],[60,120,6,126],[180,120,246,6],[300,120,6,66],[480,120,66,6],[540,120,6,126],[120,180,126,6],[120,180,6,126],[360,180,126,6],[480,180,6,126],[180,240,6,126],[180,360,246,6],[420,240,6,126],[240,240,42,6],[324,240,42,6],[240,240,6,66],[240,300,126,6],[360,240,6,66],[0,300,66,6],[540,300,66,6],[60,360,66,6],[60,360,6,186],[480,360,66,6],[540,360,6,186],[120,420,366,6],[120,420,6,66],[480,420,6,66],[180,480,246,6],[300,480,6,66],[120,540,126,6],[360,540,126,6]] + for i in walls: + w = Wall(i[0],i[1],i[2],i[3],blue) + wall_list.add(w); all_sprites.add(w) + return wall_list + +def startGame(): + all_sprites = pygame.sprite.Group() + block_list = pygame.sprite.Group() + monsta_list = pygame.sprite.Group() + bomb_list = pygame.sprite.Group() + power_list = pygame.sprite.Group() + + wall_list = setupRoomOne(all_sprites) + gate = Wall(282,242,42,2,white); all_sprites.add(gate) + Pacman = Player(287, 439, "images/Trollman.png"); all_sprites.add(Pacman) + + valid_points = [] + for r in range(19): + for c in range(19): + if (r == 7 or r == 8) and (8 <= c <= 10): continue + b = Block(yellow, 4, 4); b.rect.x = (30*c+32); b.rect.y = (30*r+32) + if not pygame.sprite.spritecollide(b, wall_list, False): + block_list.add(b); all_sprites.add(b) + valid_points.append(b.rect.center) + + ghost_imgs = ["images/Blinky.png", "images/Pinky.png", "images/Inky.png", "images/Clyde.png"] + for i in range(4): + pt = random.choice(valid_points) + g = Ghost(pt[0]-15, pt[1]-15, ghost_imgs[i]); monsta_list.add(g); all_sprites.add(g) + + ADD_GHOST = pygame.USEREVENT + 1; pygame.time.set_timer(ADD_GHOST, 15000) + ADD_POWER = pygame.USEREVENT + 2; pygame.time.set_timer(ADD_POWER, 8000) + + score = 0; inv_timer = 0; froz_timer = 0 + active_explosions = []; space_held = False + font = pygame.font.Font(None, 24) + + while True: + is_inv = inv_timer > 0; is_froz = froz_timer > 0 + if inv_timer > 0: inv_timer -= 1 + if froz_timer > 0: froz_timer -= 1 + + for event in pygame.event.get(): + if event.type == pygame.QUIT: pygame.quit(); sys.exit() + if event.type == ADD_POWER and len(power_list) < 3: + pt = random.choice(valid_points) + p = PowerUp(pt[0], pt[1], random.choice(['star', 'ice'])) + power_list.add(p); all_sprites.add(p) + if event.type == ADD_GHOST and len(monsta_list) < MAX_GHOSTS: + pt = random.choice(valid_points) + g = Ghost(pt[0]-15, pt[1]-15, random.choice(ghost_imgs)); monsta_list.add(g); all_sprites.add(g) + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_LEFT: Pacman.changespeed(-30,0) + if event.key == pygame.K_RIGHT: Pacman.changespeed(30,0) + if event.key == pygame.K_UP: Pacman.changespeed(0,-30) + if event.key == pygame.K_DOWN: Pacman.changespeed(0,30) + if event.type == pygame.KEYUP: + if event.key == pygame.K_LEFT: Pacman.changespeed(30,0) + if event.key == pygame.K_RIGHT: Pacman.changespeed(-30,0) + if event.key == pygame.K_UP: Pacman.changespeed(0,30) + if event.key == pygame.K_DOWN: Pacman.changespeed(0,-30) + + # 炸彈邏輯 (左 Ctrl) + keys = pygame.key.get_pressed() + if keys[pygame.K_LCTRL] and not space_held and Pacman.bomb_count > 0: + b = Bomb(Pacman.rect.left, Pacman.rect.top); bomb_list.add(b); all_sprites.add(b) + Pacman.bomb_count -= 1; space_held = True + if not keys[pygame.K_LCTRL]: space_held = False + + Pacman.update(wall_list, gate, is_inv) + for g in monsta_list: g.update(wall_list, gate, is_froz) + + # 炸彈爆炸判定與音效 + for b in list(bomb_list): + b.update(wall_list) + if b.exploded: + if bomb_sound: bomb_sound.play() # 炸彈音效在此播放 + cells = b.get_explosion_cells(wall_list) + active_explosions.append((cells, 5)) + for r, _, _ in cells: + for g in list(monsta_list): + if g.rect.colliderect(r): g.kill(); score += 50 + if Pacman.rect.colliderect(r) and not is_inv: return # 炸死 + b.kill() + + # 道具碰撞與音效 + p_hits = pygame.sprite.spritecollide(Pacman, power_list, True) + for p in p_hits: + if sfx_powerup: sfx_powerup.play() + if p.type == 'star': inv_timer = POWER_DURATION + if p.type == 'ice': + froz_timer = POWER_DURATION + if sfx_freeze: sfx_freeze.play() + + score += len(pygame.sprite.spritecollide(Pacman, block_list, True)) + + # 鬼魂碰撞 + m_hits = pygame.sprite.spritecollide(Pacman, monsta_list, False) + if m_hits: + if is_inv: + for m in m_hits: + m.kill(); score += 100 + if sfx_eat: sfx_eat.play() + else: return + + # 繪製 + screen.fill(black) + all_sprites.draw(screen) + # 繪製爆炸效果 + for cells, frames in list(active_explosions): + for r, ct, rot in cells: + if ct == 'center' and _expl_center: screen.blit(_expl_center, r) + elif ct == 'mid' and _expl_mid: screen.blit(pygame.transform.rotate(_expl_mid, rot), r) + else: pygame.draw.rect(screen, (255, 200, 0), r) + idx = active_explosions.index((cells, frames)) + if frames > 1: active_explosions[idx] = (cells, frames - 1) + else: active_explosions.pop(idx) + + # 顯示文字 + s_text = font.render(f"Score: {score} Bombs: {Pacman.bomb_count}", True, red) + screen.blit(s_text, [10, 10]) + if is_inv: screen.blit(font.render("INVINCIBLE!", True, orange), [250, 10]) + if is_froz: screen.blit(font.render("GHOSTS FROZEN!", True, cyan), [400, 10]) + + pygame.display.flip() + clock.tick(10) + +if __name__ == "__main__": + while True: startGame()