import os
import sys
import random
import math
import librosa
import numpy as np
from pygame import *
import time as systime
# ========================
# 游戏核心类定义
# ========================
class Ghost:
def __init__(self):
self.x = 200
self.y = 450
self.state = "neutral" # neutral, left, right, hit
self.hit_timer = 0
self.speed = 5
def move(self, direction):
"""移动幽灵"""
if direction == "left":
self.x = max(50, self.x - self.speed)
self.state = "left"
elif direction == "right":
self.x = min(1150, self.x + self.speed)
self.state = "right"
else:
self.state = "neutral"
def hit_effect(self):
"""击中效果"""
self.state = "hit"
self.hit_timer = 10 # 10帧的击中效果
def update(self):
"""更新幽灵状态"""
if self.hit_timer > 0:
self.hit_timer -= 1
if self.hit_timer == 0:
self.state = "neutral"
def get_rect(self):
"""获取幽灵碰撞矩形"""
return Rect(self.x, self.y, 100, 100)
class Note:
def __init__(self, track, note_type, time_position, speed=5):
"""
音符类
:param track: 轨道 (0=左轨, 1=右轨)
:param note_type: 音符类型 (0=红/奇数, 1=蓝/偶数, 2=绿/质数, 3=黄/特殊)
:param time_position: 音符出现的时间点(毫秒)
:param speed: 移动速度(像素/帧)
"""
self.track = track
self.note_type = note_type
self.time_position = time_position
self.speed = speed
self.x = 1280 # 从屏幕右侧开始
self.y = 400 if track == 0 else 500 # 上下轨道位置
self.width = 50
self.height = 50
self.hit = False
self.missed = False
self.rect = Rect(self.x, self.y, self.width, self.height)
def update(self):
"""更新音符位置"""
self.x -= self.speed
self.rect.x = self.x
def draw(self, screen, note_images):
"""绘制音符"""
if not self.hit and not self.missed:
screen.blit(note_images[self.note_type], (self.x, self.y))
class MathProblem:
def __init__(self, difficulty=1):
"""
数学题目类
:param difficulty: 题目难度 (1-3)
"""
self.difficulty = difficulty
self.generate_problem()
self.time_limit = 10000 # 10秒答题时间
self.start_time = systime.time() * 1000 # 毫秒
def generate_problem(self):
"""生成数学题目"""
problem_types = [
self.generate_addition,
self.generate_subtraction,
self.generate_multiplication,
self.generate_division
]
# 根据难度选择题目类型
if self.difficulty == 1:
func = random.choice(problem_types[:2])
elif self.difficulty == 2:
func = random.choice(problem_types)
else:
func = random.choice(problem_types[2:])
self.problem_text, self.correct_answer = func()
def generate_addition(self):
"""生成加法题"""
a = random.randint(1, 20)
b = random.randint(1, 20)
return f"{a} + {b} = ?", a + b
def generate_subtraction(self):
"""生成减法题"""
a = random.randint(10, 30)
b = random.randint(1, a)
return f"{a} - {b} = ?", a - b
def generate_multiplication(self):
"""生成乘法题"""
a = random.randint(1, 12)
b = random.randint(1, 12)
return f"{a} × {b} = ?", a * b
def generate_division(self):
"""生成除法题"""
b = random.randint(1, 10)
a = b * random.randint(1, 10)
return f"{a} ÷ {b} = ?", a // b
def check_answer(self, answer):
"""检查答案是否正确"""
return answer == self.correct_answer
def is_prime(self, n):
"""检查数字是否为质数"""
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
def time_remaining(self):
"""返回剩余时间(毫秒)"""
elapsed = systime.time() * 1000 - self.start_time
return max(0, self.time_limit - elapsed)
class RhythmGame:
def __init__(self, screen, font_path=None):
self.screen = screen
self.font_path = font_path
self.load_resources()
self.reset_game()
def load_resources(self):
"""加载游戏资源"""
script_dir = os.path.dirname(os.path.abspath(__file__))
images_dir = os.path.join(script_dir, 'images')
audio_dir = os.path.join(script_dir, 'audio')
# 确保音频目录存在
if not os.path.exists(audio_dir):
os.makedirs(audio_dir)
print(f"已创建音频目录: {audio_dir}")
# 加载图片
self.note_images = [
self.load_image(os.path.join(images_dir, "note_red.png")),
self.load_image(os.path.join(images_dir, "note_blue.png")),
self.load_image(os.path.join(images_dir, "note_green.png")),
self.load_image(os.path.join(images_dir, "note_yellow.png"))
]
self.pokey_images = {
"left": self.load_image(os.path.join(images_dir, "pokey_left.png")),
"right": self.load_image(os.path.join(images_dir, "pokey_right.png")),
"neutral": self.load_image(os.path.join(images_dir, "pokey_neutral.png")),
"hit": self.load_image(os.path.join(images_dir, "pokey_hit.png"))
}
self.track_bg = self.load_image(os.path.join(images_dir, "track_background.png"))
self.judgment_line_img = self.load_image(os.path.join(images_dir, "judgment_line.png"))
self.blackboard_img = self.load_image(os.path.join(images_dir, "blackboard.png"))
self.combo_display_img = self.load_image(os.path.join(images_dir, "combo_display.png"))
self.score_display_img = self.load_image(os.path.join(images_dir, "score_display.png"))
self.health_bar_img = self.load_image(os.path.join(images_dir, "health_bar.png"))
self.pause_button_img = self.load_image(os.path.join(images_dir, "pause_button.png"))
# 加载字体 - 使用相对路径
try:
# 使用指定的字体路径
if self.font_path and os.path.exists(self.font_path):
self.font_large = font.Font(self.font_path, 48)
self.font_medium = font.Font(self.font_path, 36)
self.font_small = font.Font(self.font_path, 24)
print(f"成功加载自定义字体: {self.font_path}")
else:
# 尝试加载默认字体
self.font_large = font.Font(None, 48)
self.font_medium = font.Font(None, 36)
self.font_small = font.Font(None, 24)
print("使用系统默认字体")
except:
# 字体加载失败时使用回退方案
self.font_large = font.Font(None, 48)
self.font_medium = font.Font(None, 36)
self.font_small = font.Font(None, 24)
print("字体加载失败,使用系统默认字体")
# 加载音效 - 使用安全加载方法
self.sound_hit_perfect = self.load_sound(os.path.join(audio_dir, "hit_perfect.wav"))
self.sound_hit_good = self.load_sound(os.path.join(audio_dir, "hit_good.wav"))
self.sound_hit_miss = self.load_sound(os.path.join(audio_dir, "hit_miss.wav"))
self.sound_combo_break = self.load_sound(os.path.join(audio_dir, "combo_break.wav"))
self.sound_question = self.load_sound(os.path.join(audio_dir, "question_appear.wav"))
self.sound_level_up = self.load_sound(os.path.join(audio_dir, "level_up.wav"))
# 加载音乐
self.music_files = [
os.path.join(audio_dir, "song1.mp3"),
os.path.join(audio_dir, "song2.mp3"),
os.path.join(audio_dir, "song3.mp3")
]
def load_image(self, path):
"""安全加载图片,如果失败则创建空表面"""
try:
if os.path.exists(path):
return image.load(path).convert_alpha()
else:
print(f"图片文件不存在: {path}")
except Exception as e:
print(f"图片加载失败: {path}, 错误: {e}")
# 创建空表面作为替代
return Surface((50, 50), SRCALPHA)
def load_sound(self, path):
"""安全加载音效,如果失败则创建空音效"""
try:
if os.path.exists(path):
return mixer.Sound(path)
else:
print(f"音效文件不存在: {path}")
except Exception as e:
print(f"音效加载失败: {path}, 错误: {e}")
# 创建空音效作为替代
return mixer.Sound(buffer=bytes())
def reset_game(self):
"""重置游戏状态"""
self.notes = []
self.current_problem = None
self.score = 0
self.combo = 0
self.max_combo = 0
self.health = 100
self.song_index = 0
self.difficulty = 1
self.game_state = "playing" # playing, paused, game_over
self.pokey = Ghost()
self.load_song(self.music_files[self.song_index])
def load_song(self, file_path):
"""加载并分析歌曲"""
# 检查文件是否存在
if not os.path.exists(file_path):
print(f"音乐文件不存在: {file_path}")
self.beat_times = []
return
try:
# 加载音频文件
mixer.music.load(file_path)
# 使用librosa分析节拍
y, sr = librosa.load(file_path)
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
# 将节拍点转换为毫秒
self.beat_times = librosa.frames_to_time(beat_frames, sr=sr) * 1000
# 生成音符
self.generate_notes()
except Exception as e:
print(f"歌曲加载失败: {e}")
self.beat_times = []
def generate_notes(self):
"""根据节拍生成音符"""
self.notes = []
for beat_time in self.beat_times:
track = random.randint(0, 1) # 随机选择轨道
note_type = random.randint(0, 2) # 红、蓝、绿
# 20%几率生成特殊音符
if random.random() < 0.2:
note_type = 3 # 黄色特殊音符
self.notes.append(Note(track, note_type, beat_time))
def start_math_problem(self):
"""开始一个新的数学问题"""
self.current_problem = MathProblem(self.difficulty)
try:
self.sound_question.play()
except:
pass # 忽略音效播放错误
def check_note_hit(self, track, current_time):
"""
检查音符是否击中
:param track: 玩家按下的轨道
:param current_time: 当前游戏时间(毫秒)
"""
perfect_threshold = 50 # 完美判定阈值(毫秒)
good_threshold = 150 # 良好判定阈值(毫秒)
for note in self.notes:
if note.hit or note.missed:
continue
# 计算时间差
time_diff = abs(current_time - note.time_position)
# 检查是否在判定范围内
if time_diff <= good_threshold and note.track == track:
# 设置Pokey击中状态
self.pokey.hit_effect()
# 检查是否与数学问题相关
if self.current_problem:
answer_type = self.get_answer_type(self.current_problem.correct_answer)
# 特殊音符总是正确的
if note.note_type == 3: # 黄色特殊音符
accuracy = "perfect"
# 检查音符类型是否匹配答案类型
elif (note.note_type == 0 and answer_type == "odd") or \
(note.note_type == 1 and answer_type == "even") or \
(note.note_type == 2 and answer_type == "prime"):
accuracy = "perfect" if time_diff <= perfect_threshold else "good"
else:
accuracy = "bad"
else:
accuracy = "perfect" if time_diff <= perfect_threshold else "good"
# 处理击中结果
self.handle_hit_result(note, accuracy)
return True
return False
def get_answer_type(self, answer):
"""获取答案的类型(奇数、偶数、质数)"""
if answer % 2 == 0:
return "even"
elif self.current_problem.is_prime(answer):
return "prime"
else:
return "odd"
def handle_hit_result(self, note, accuracy):
"""处理击中结果"""
note.hit = True
if accuracy == "perfect":
self.score += 100 * self.combo_multiplier()
self.combo += 1
try:
self.sound_hit_perfect.play()
except:
pass
elif accuracy == "good":
self.score += 50 * self.combo_multiplier()
self.combo += 1
try:
self.sound_hit_good.play()
except:
pass
else: # bad
self.health -= 5
self.break_combo()
try:
self.sound_hit_miss.play()
except:
pass
self.max_combo = max(self.max_combo, self.combo)
def break_combo(self):
"""中断连击"""
if self.combo >= 5:
try:
self.sound_combo_break.play()
except:
pass
self.combo = 0
def combo_multiplier(self):
"""计算连击倍数"""
if self.combo < 5:
return 1
elif self.combo < 10:
return 1.2
elif self.combo < 20:
return 1.5
else:
return 2.0
def update(self, current_time):
"""更新游戏状态"""
if self.game_state != "playing":
return
# 更新Pokey状态
self.pokey.update()
# 更新音符
for note in self.notes:
note.update()
# 检查是否错过音符
if not note.hit and not note.missed and note.x < 200:
note.missed = True
self.health -= 2
self.break_combo()
try:
self.sound_hit_miss.play()
except:
pass
# 检查是否需要新题目
if not self.current_problem or self.current_problem.time_remaining() <= 0:
self.start_math_problem()
# 检查游戏结束
if self.health <= 0:
self.game_state = "game_over"
def draw(self):
"""绘制游戏界面"""
# 绘制背景
self.screen.blit(self.track_bg, (0, 0))
# 绘制判定线
self.screen.blit(self.judgment_line_img, (250, 300))
# 绘制音符
for note in self.notes:
note.draw(self.screen, self.note_images)
# 绘制Pokey
pokey_img = self.pokey_images[self.pokey.state]
self.screen.blit(pokey_img, (self.pokey.x, self.pokey.y))
# 绘制黑板和题目
if self.current_problem:
self.screen.blit(self.blackboard_img, (400, 100))
problem_text = self.font_large.render(self.current_problem.problem_text, True, (255, 255, 255))
self.screen.blit(problem_text, (450, 150))
# 绘制剩余时间条
time_remaining = self.current_problem.time_remaining() / self.current_problem.time_limit
time_bar_width = int(300 * time_remaining)
draw.rect(self.screen, (0, 255, 0), (450, 200, time_bar_width, 20))
# 绘制UI元素
self.screen.blit(self.score_display_img, (20, 20))
score_text = self.font_medium.render(f"Score: {self.score}", True, (255, 255, 255))
self.screen.blit(score_text, (40, 30))
self.screen.blit(self.combo_display_img, (20, 70))
combo_text = self.font_medium.render(f"Combo: {self.combo}x", True, (255, 255, 255))
self.screen.blit(combo_text, (40, 80))
self.screen.blit(self.health_bar_img, (1100, 20))
health_width = int(150 * (self.health / 100))
draw.rect(self.screen, (255, 0, 0), (1105, 25, health_width, 20))
self.screen.blit(self.pause_button_img, (1200, 650))
# 游戏结束画面
if self.game_state == "game_over":
overlay = Surface((1280, 720), SRCALPHA)
overlay.fill((0, 0, 0, 180))
self.screen.blit(overlay, (0, 0))
game_over_text = self.font_large.render("Game Over", True, (255, 0, 0))
score_text = self.font_medium.render(f"Final Score: {self.score}", True, (255, 255, 255))
combo_text = self.font_medium.render(f"Max Combo: {self.max_combo}", True, (255, 255, 255))
restart_text = self.font_medium.render("Press R to restart", True, (255, 255, 255))
self.screen.blit(game_over_text, (540, 300))
self.screen.blit(score_text, (540, 370))
self.screen.blit(combo_text, (540, 410))
self.screen.blit(restart_text, (540, 470))
# 暂停画面
elif self.game_state == "paused":
overlay = Surface((1280, 720), SRCALPHA)
overlay.fill((0, 0, 0, 180))
self.screen.blit(overlay, (0, 0))
pause_text = self.font_large.render("Game Paused", True, (255, 255, 0))
continue_text = self.font_medium.render("Press P to continue", True, (255, 255, 255))
self.screen.blit(pause_text, (520, 300))
self.screen.blit(continue_text, (520, 370))
# ========================
# 游戏主程序
# ========================
def Mam_draw_map(screen, show_buttons, bar_height, background_offset, conversation_mode,
current_conversation_img, text_line1, text_line2, text_progress, font):
"""绘制游戏场景(优化显示更新)"""
color = (192,192,192)
screen.fill(color)
if not conversation_mode:
# 滚动模式:绘制背景和对话图片
screen.blit(background, (background_offset, -200))
screen.blit(conversation_1, (background_offset + background.get_width(), -200))
# 如果需要,在更右侧绘制第二张背景以保持连续性
if background_offset < 0:
screen.blit(background, (background_offset + 2 * background.get_width(), -200))
else:
# 对话模式:绘制当前对话图片
screen.blit(current_conversation_img, (0, -200))
# 根据show_buttons标志决定是否绘制标题和按钮
if show_buttons:
screen.blit(title_img, (0, 0))
screen.blit(button_start_img, (500, 500))
screen.blit(button_quit_img, (500, 570))
# 绘制上下黑条(始终绘制)
if bar_height > 0:
# 上黑条
draw.rect(screen, (0, 0, 0), (0, 0, screen.get_width(), bar_height))
# 下黑条
draw.rect(screen, (0, 0, 0), (0, screen.get_height() - bar_height,
screen.get_width(), bar_height))
# 在对话模式下绘制文本
if conversation_mode and text_progress > 0 and font:
# 创建半透明背景
text_bg = Surface((screen.get_width(), bar_height * 2), SRCALPHA)
text_bg.fill((0, 0, 0, 200)) # 半透明黑色
# 绘制文本背景
screen.blit(text_bg, (0, (screen.get_height() - bar_height * 2) // 2))
# 绘制第一行文本
if text_progress >= 1:
text_surface1 = font.render(text_line1, True, (255, 255, 255))
text_rect1 = text_surface1.get_rect(center=(screen.get_width() // 2,
screen.get_height() // 2 - 30))
screen.blit(text_surface1, text_rect1)
# 绘制第二行文本
if text_progress >= 2:
text_surface2 = font.render(text_line2, True, (255, 255, 255))
text_rect2 = text_surface2.get_rect(center=(screen.get_width() // 2,
screen.get_height() // 2 + 30))
screen.blit(text_surface2, text_rect2)
# 使用更高效的显示更新方式
display.flip()
def main():
# 初始化pygame
init()
screen = display.set_mode((1280, 720))
display.set_caption("数学节奏大师: Pokey的冒险")
clock = time.Clock()
# 获取当前脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
images_dir = os.path.join(script_dir, 'images')
fonts_dir = os.path.join(script_dir, 'fonts') # 字体文件夹路径
audio_dir = os.path.join(script_dir, 'audio') # 音频文件夹路径
# 确保图片目录存在
if not os.path.exists(images_dir):
os.makedirs(images_dir)
print(f"已创建图片目录: {images_dir}")
# 确保字体目录存在
if not os.path.exists(fonts_dir):
os.makedirs(fonts_dir)
print(f"已创建字体目录: {fonts_dir}")
# 确保音频目录存在
if not os.path.exists(audio_dir):
os.makedirs(audio_dir)
print(f"已创建音频目录: {audio_dir}")
# 字体文件路径
font_file = "HYPixel11pxU-2.ttf"
font_path = os.path.join(fonts_dir, font_file)
# 加载游戏字体
try:
# 尝试加载指定字体
game_font = font.Font(font_path, 48)
print(f"成功加载字体: {font_path}")
except:
# 字体加载失败时使用系统默认字体
try:
game_font = font.Font(None, 48)
print("使用系统默认字体")
except:
print("警告:无法加载任何字体,文本将无法显示")
game_font = None
# 加载游戏图片资源
def safe_load_image(path, default_size=(50, 50)):
"""安全加载图片,如果失败则创建空表面"""
try:
if os.path.exists(path):
return image.load(path).convert_alpha()
else:
print(f"图片文件不存在: {path}")
except Exception as e:
print(f"图片加载失败: {path}, 错误: {e}")
# 创建空表面作为替代
return Surface(default_size, SRCALPHA)
try:
background = safe_load_image(os.path.join(images_dir, 'background.png'), (1280, 720))
button_start_img = safe_load_image(os.path.join(images_dir, 'button_start.png'))
button_quit_img = safe_load_image(os.path.join(images_dir, 'button_quit.png'))
title_img = safe_load_image(os.path.join(images_dir, 'title.png'))
conversation_1 = safe_load_image(os.path.join(images_dir, 'conversation_1.png'))
conversation_2 = safe_load_image(os.path.join(images_dir, 'conversation_2.png'))
print("图片加载完成(部分可能使用替代图像)")
except Exception as e:
print(f"图片加载过程中发生错误: {e}")
quit()
sys.exit(1)
# 游戏状态变量
game_state = "menu" # menu, story, rhythm_game
show_buttons = True
bar_height = 0
target_bar_height = screen.get_height() // 6
background_offset = 0
target_background_offset = -background.get_width()
animation_speed = 5
animation_active = False
conversation_mode = False
current_conversation_img = conversation_1
last_switch_time = systime.time()
switch_interval = 0.5
text_line1 = "『你不该来这里的。』"
text_line2 = "『抱歉,朋友。我也没得选择。』"
text_progress = 0
last_text_time = 0
# 创建按钮矩形区域
start_button_rect = button_start_img.get_rect(topleft=(500, 500))
quit_button_rect = button_quit_img.get_rect(topleft=(500, 570))
# 初始化节奏游戏,传入字体路径
rhythm_game = RhythmGame(screen, font_path)
running = True
while running:
current_time = systime.time()
clock.tick(60)
for event in pygame.event.get():
if event.type == QUIT:
running = False
if game_state == "menu":
if event.type == MOUSEBUTTONDOWN:
mouse_pos = mouse.get_pos()
if show_buttons:
if start_button_rect.collidepoint(mouse_pos):
show_buttons = False
animation_active = True
print("开始游戏!")
elif quit_button_rect.collidepoint(mouse_pos):
running = False
print("退出游戏")
elif game_state == "story":
if event.type == MOUSEBUTTONDOWN:
if conversation_mode and text_progress < 2:
text_progress += 1
elif game_state == "rhythm_game":
if event.type == KEYDOWN:
if event.key == K_LEFT:
rhythm_game.check_note_hit(0, current_time * 1000)
elif event.key == K_RIGHT:
rhythm_game.check_note_hit(1, current_time * 1000)
elif event.key == K_p:
if rhythm_game.game_state == "playing":
rhythm_game.game_state = "paused"
mixer.music.pause()
elif rhythm_game.game_state == "paused":
rhythm_game.game_state = "playing"
mixer.music.unpause()
elif event.key == K_r and rhythm_game.game_state == "game_over":
rhythm_game.reset_game()
mixer.music.play()
elif event.key == K_ESCAPE:
game_state = "menu"
rhythm_game.reset_game()
if event.type == MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
if rhythm_game.game_state == "playing" and 1200 <= mouse_pos[0] <= 1260 and 650 <= mouse_pos[1] <= 710:
rhythm_game.game_state = "paused"
mixer.music.pause()
# 更新游戏状态
if game_state == "menu":
# 更新动画
if animation_active:
# 更新黑条高度
if bar_height < target_bar_height:
bar_height += animation_speed
if bar_height > target_bar_height:
bar_height = target_bar_height
# 更新背景滚动位置
if background_offset > target_background_offset:
background_offset -= animation_speed
if background_offset < target_background_offset:
background_offset = target_background_offset
# 检查动画是否完成
if bar_height == target_bar_height and background_offset == target_background_offset:
animation_active = False
conversation_mode = True
last_text_time = current_time
print("进入对话模式")
# 对话模式
if conversation_mode:
# 图片切换
if current_time - last_switch_time > switch_interval:
if current_conversation_img == conversation_1:
current_conversation_img = conversation_2
else:
current_conversation_img = conversation_1
last_switch_time = current_time
# 文本显示进度
if text_progress == 0 and current_time - last_text_time > 1.0:
text_progress = 1
last_text_time = current_time
elif text_progress == 1 and current_time - last_text_time > 3.0:
text_progress = 2
last_text_time = current_time
# 文本显示完成后进入节奏游戏
game_state = "rhythm_game"
try:
mixer.music.play()
except:
print("音乐播放失败,游戏继续运行")
# 绘制菜单/故事场景
Mam_draw_map(screen, show_buttons, bar_height, background_offset,
conversation_mode, current_conversation_img,
text_line1, text_line2, text_progress, game_font)
elif game_state == "rhythm_game":
# 更新节奏游戏
rhythm_game.update(current_time * 1000)
rhythm_game.draw()
display.flip()
quit()
if __name__ == "__main__":
main() 分析这段代码所有可能导致游戏无法运行的bug
最新发布