化学配对记忆游戏:用Python和Pygame打造趣味化学学习工具

一款使用Python和Pygame开发的化学配对教育游戏,通过创新的记忆配对机制帮助学生学习化学知识。游戏包含元素和化合物两种模式,分别涵盖28种化学元素和32种常见化合物,玩家需要在网格中点击匹配中文名称与对应化学式。游戏设计了三个难度级别(4×4、6×6、8×8网格),具有自适应界面布局、智能字体调整、震动反馈效果和完整的游戏流程管理,既能让学生在轻松互动中掌握化学术语,又展示了如何将编程技术应用于教育领域,是一款兼具教育价值和编程学习意义的开源项目,因为化合物中的下标弄不出来,所以H₂O这种是H2O,也可以自己增加元素和化合物的种类。

python 复制代码
import pygame
import random
import sys
import math
import os
from pygame.locals import *

# 初始化pygame
pygame.init()

# 获取屏幕尺寸并设置窗口居中
screen_width = 1000
screen_height = 800
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("化学配对游戏")

# 颜色定义
BACKGROUND = (240, 245, 249)
GRID_BG = (230, 240, 255)
TEXT_COLOR = (30, 45, 70)
SELECTED_COLOR = (100, 180, 255)
CORRECT_COLOR = (100, 220, 150)
WRONG_COLOR = (255, 150, 150)
EMPTY_COLOR = (220, 230, 240)
BUTTON_COLOR = (70, 130, 180)
BUTTON_HOVER = (90, 150, 200)
TITLE_COLOR = (20, 60, 120)

# 单元格边距
CELL_MARGIN = 8


# 字体加载函数
def load_font(font_size=32):
    """安全加载字体,返回一个可用的字体对象"""
    try:
        # 尝试加载系统字体
        font_names = [
            'Microsoft YaHei',  # Windows 中文
            'SimHei',  # Windows 黑体
            'SimSun',  # Windows 宋体
            'Arial Unicode MS',  # 通用Unicode字体
            'DejaVu Sans',  # Linux通用字体
            'Arial'  # 通用字体
        ]

        for font_name in font_names:
            try:
                font = pygame.font.SysFont(font_name, font_size)
                # 测试字体是否能渲染
                test_surface = font.render("测试", True, (0, 0, 0))
                if test_surface.get_width() > 0:
                    return font
            except:
                continue

        # 如果都失败,使用默认字体
        return pygame.font.Font(None, font_size)
    except:
        # 终极备用方案
        return pygame.font.Font(None, font_size)


# 加载字体
try:
    chinese_font = load_font(32)
    chinese_font_small = load_font(24)
    font_medium = load_font(36)
    font_small = load_font(28)
except:
    # 如果字体加载失败,使用默认字体
    chinese_font = pygame.font.Font(None, 32)
    chinese_font_small = pygame.font.Font(None, 24)
    font_medium = pygame.font.Font(None, 36)
    font_small = pygame.font.Font(None, 28)

# 化学元素数据 - 中文名称和对应的化学式
elements = [
    {"chinese": "氢", "formula": "H"},
    {"chinese": "氦", "formula": "He"},
    {"chinese": "锂", "formula": "Li"},
    {"chinese": "铍", "formula": "Be"},
    {"chinese": "硼", "formula": "B"},
    {"chinese": "碳", "formula": "C"},
    {"chinese": "氮", "formula": "N"},
    {"chinese": "氧", "formula": "O"},
    {"chinese": "氟", "formula": "F"},
    {"chinese": "氖", "formula": "Ne"},
    {"chinese": "钠", "formula": "Na"},
    {"chinese": "镁", "formula": "Mg"},
    {"chinese": "铝", "formula": "Al"},
    {"chinese": "硅", "formula": "Si"},
    {"chinese": "磷", "formula": "P"},
    {"chinese": "硫", "formula": "S"},
    {"chinese": "氯", "formula": "Cl"},
    {"chinese": "氩", "formula": "Ar"},
    {"chinese": "钾", "formula": "K"},
    {"chinese": "钙", "formula": "Ca"},
    {"chinese": "铁", "formula": "Fe"},
    {"chinese": "铜", "formula": "Cu"},
    {"chinese": "银", "formula": "Ag"},
    {"chinese": "金", "formula": "Au"},
    {"chinese": "汞", "formula": "Hg"},
    {"chinese": "铅", "formula": "Pb"},
    {"chinese": "锡", "formula": "Sn"},
    {"chinese": "锌", "formula": "Zn"},
]

# 化合物数据 - 使用标准字符,避免特殊字符
compounds = [
    {"chinese": "水", "formula": "H2O"},
    {"chinese": "二氧化碳", "formula": "CO2"},
    {"chinese": "食盐", "formula": "NaCl"},
    {"chinese": "氨气", "formula": "NH3"},
    {"chinese": "甲烷", "formula": "CH4"},
    {"chinese": "硫酸", "formula": "H2SO4"},
    {"chinese": "葡萄糖", "formula": "C6H12O6"},
    {"chinese": "乙醇", "formula": "C2H5OH"},
    {"chinese": "碳酸钙", "formula": "CaCO3"},
    {"chinese": "氢氧化钠", "formula": "NaOH"},
    {"chinese": "盐酸", "formula": "HCl"},
    {"chinese": "硝酸", "formula": "HNO3"},
    {"chinese": "醋酸", "formula": "CH3COOH"},
    {"chinese": "氧化铁", "formula": "Fe2O3"},
    {"chinese": "硫酸铜", "formula": "CuSO4"},
    {"chinese": "氯化银", "formula": "AgCl"},
    {"chinese": "氧化铝", "formula": "Al2O3"},
    {"chinese": "硫化氢", "formula": "H2S"},
    {"chinese": "氢氧化钙", "formula": "Ca(OH)2"},
    {"chinese": "碳酸钠", "formula": "Na2CO3"},
    {"chinese": "氯化钾", "formula": "KCl"},
    {"chinese": "硫酸钠", "formula": "Na2SO4"},
    {"chinese": "硝酸银", "formula": "AgNO3"},
    {"chinese": "硫酸亚铁", "formula": "FeSO4"},
    {"chinese": "氧化铜", "formula": "CuO"},
    {"chinese": "二氧化硅", "formula": "SiO2"},
    {"chinese": "氯化钙", "formula": "CaCl2"},
    {"chinese": "氯化镁", "formula": "MgCl2"},
    {"chinese": "硫酸镁", "formula": "MgSO4"},
    {"chinese": "氯化锌", "formula": "ZnCl2"},
    {"chinese": "硫酸锌", "formula": "ZnSO4"},
    {"chinese": "硝酸钾", "formula": "KNO3"},
]


class GameCell:
    def __init__(self, x, y, width, height, text, cell_type, pair_id):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.cell_type = cell_type  # 'chinese' 或 'formula'
        self.pair_id = pair_id  # 配对ID,相同ID的可以配对
        self.selected = False
        self.matched = False

    def draw(self, screen):
        if self.matched:
            color = EMPTY_COLOR
        elif self.selected:
            color = SELECTED_COLOR
        else:
            color = GRID_BG

        pygame.draw.rect(screen, color, self.rect, border_radius=8)
        pygame.draw.rect(screen, TEXT_COLOR, self.rect, 2, border_radius=8)

        if self.text and not self.matched:
            # 根据单元格大小选择合适的字体大小
            cell_size = min(self.rect.width, self.rect.height)

            if cell_size > 100:  # 大单元格
                font_size = 32
            elif cell_size > 70:  # 中等单元格
                font_size = 24
            else:  # 小单元格
                font_size = 18

            # 使用安全的字体加载
            try:
                font = load_font(font_size)
                text_surface = font.render(self.text, True, TEXT_COLOR)
            except:
                # 如果字体渲染失败,使用默认字体
                font = pygame.font.Font(None, font_size)
                text_surface = font.render(self.text, True, TEXT_COLOR)

            # 如果文本太长,缩小字体
            max_width = self.rect.width - 10
            while text_surface.get_width() > max_width and font_size > 12:
                font_size -= 2
                try:
                    font = load_font(font_size)
                    text_surface = font.render(self.text, True, TEXT_COLOR)
                except:
                    font = pygame.font.Font(None, font_size)
                    text_surface = font.render(self.text, True, TEXT_COLOR)

            text_rect = text_surface.get_rect(center=self.rect.center)
            screen.blit(text_surface, text_rect)

    def is_clicked(self, pos):
        return self.rect.collidepoint(pos) and not self.matched


class Button:
    def __init__(self, x, y, width, height, text):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.hovered = False

    def draw(self, screen):
        color = BUTTON_HOVER if self.hovered else BUTTON_COLOR
        pygame.draw.rect(screen, color, self.rect, border_radius=8)
        pygame.draw.rect(screen, (255, 255, 255), self.rect, 2, border_radius=8)

        # 渲染按钮文本
        try:
            text_surface = chinese_font_small.render(self.text, True, (255, 255, 255))
        except:
            font = pygame.font.Font(None, 24)
            text_surface = font.render(self.text, True, (255, 255, 255))

        text_rect = text_surface.get_rect(center=self.rect.center)
        screen.blit(text_surface, text_rect)

    def is_clicked(self, pos):
        return self.rect.collidepoint(pos)


class DifficultyButton:
    def __init__(self, x, y, width, height, text, difficulty):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.difficulty = difficulty
        self.hovered = False
        self.selected = False

    def draw(self, screen):
        if self.selected:
            color = (150, 200, 100)
        elif self.hovered:
            color = BUTTON_HOVER
        else:
            color = BUTTON_COLOR

        pygame.draw.rect(screen, color, self.rect, border_radius=8)
        pygame.draw.rect(screen, (255, 255, 255), self.rect, 2, border_radius=8)

        # 渲染按钮文本
        try:
            text_surface = chinese_font_small.render(self.text, True, (255, 255, 255))
        except:
            font = pygame.font.Font(None, 24)
            text_surface = font.render(self.text, True, (255, 255, 255))

        text_rect = text_surface.get_rect(center=self.rect.center)
        screen.blit(text_surface, text_rect)

    def is_clicked(self, pos):
        return self.rect.collidepoint(pos)


class Game:
    def __init__(self):
        self.mode = "elements"  # "elements" 或 "compounds"
        self.difficulty = "easy"  # "easy", "medium", "hard"
        self.cells = []
        self.selected_cells = []
        self.matched_pairs = 0
        self.total_pairs = 0
        self.game_over = False
        self.shake_time = 0
        self.shake_offset = (0, 0)

        # 创建按钮
        self.create_buttons()
        self.init_game()

    def calculate_cell_size(self):
        """根据难度计算单元格大小和网格大小"""
        if self.difficulty == "easy":
            self.grid_size = 4
            available_width = screen_width - 100
            available_height = screen_height - 250
            cell_size = min(available_width // self.grid_size, available_height // self.grid_size)
            self.cell_width = cell_size - CELL_MARGIN
            self.cell_height = self.cell_width

        elif self.difficulty == "medium":
            self.grid_size = 6
            available_width = screen_width - 100
            available_height = screen_height - 250
            cell_size = min(available_width // self.grid_size, available_height // self.grid_size)
            self.cell_width = cell_size - CELL_MARGIN
            self.cell_height = self.cell_width

        else:  # hard
            self.grid_size = 8
            available_width = screen_width - 100
            available_height = screen_height - 250
            cell_size = min(available_width // self.grid_size, available_height // self.grid_size)
            self.cell_width = cell_size - CELL_MARGIN
            self.cell_height = self.cell_width

    def create_buttons(self):
        """创建游戏按钮"""
        button_width = 150
        button_height = 40
        button_margin = 20
        button_y = screen_height - 70

        self.restart_button = Button(
            screen_width // 2 - button_width - button_margin // 2,
            button_y,
            button_width,
            button_height,
            "重新开始"
        )

        self.mode_button = Button(
            screen_width // 2 + button_margin // 2,
            button_y,
            button_width,
            button_height,
            "切换模式"
        )

        # 创建难度按钮
        diff_button_width = 100
        diff_button_margin = 10
        diff_button_y = 90

        self.easy_button = DifficultyButton(
            screen_width // 2 - diff_button_width - diff_button_margin,
            diff_button_y,
            diff_button_width,
            button_height,
            "简单",
            "easy"
        )

        self.medium_button = DifficultyButton(
            screen_width // 2,
            diff_button_y,
            diff_button_width,
            button_height,
            "中等",
            "medium"
        )

        self.hard_button = DifficultyButton(
            screen_width // 2 + diff_button_width + diff_button_margin,
            diff_button_y,
            diff_button_width,
            button_height,
            "困难",
            "hard"
        )

        # 设置默认难度按钮选中状态
        self.easy_button.selected = True

    def init_game(self):
        self.cells = []
        self.selected_cells = []
        self.matched_pairs = 0
        self.game_over = False
        self.shake_time = 0
        self.shake_offset = (0, 0)

        # 根据模式和难度确定使用的数据
        if self.mode == "elements":
            data = elements
        else:
            data = compounds

        # 计算单元格大小
        self.calculate_cell_size()

        # 根据网格大小确定需要的配对数量
        total_cells = self.grid_size * self.grid_size
        num_pairs_needed = total_cells // 2

        # 确保有足够的数据
        if len(data) < num_pairs_needed:
            # 如果数据不足,重复使用数据
            selected_data = []
            while len(selected_data) < num_pairs_needed:
                remaining = num_pairs_needed - len(selected_data)
                selected_data.extend(random.sample(data, min(remaining, len(data))))
        else:
            selected_data = random.sample(data, num_pairs_needed)

        # 创建配对列表
        pairs = []
        for i, item in enumerate(selected_data):
            pairs.append((item["chinese"], "chinese", i))
            pairs.append((item["formula"], "formula", i))

        # 随机打乱顺序
        random.shuffle(pairs)

        # 计算网格位置使其居中
        grid_width = self.grid_size * (self.cell_width + CELL_MARGIN) + CELL_MARGIN
        grid_height = self.grid_size * (self.cell_height + CELL_MARGIN) + CELL_MARGIN
        start_x = max(50, (screen_width - grid_width) // 2)  # 确保不超出左边界
        start_y = 150

        # 创建单元格
        self.cells = []
        for i in range(self.grid_size):
            for j in range(self.grid_size):
                index = i * self.grid_size + j
                if index < len(pairs):
                    text, cell_type, pair_id = pairs[index]
                    x = start_x + j * (self.cell_width + CELL_MARGIN) + CELL_MARGIN
                    y = start_y + i * (self.cell_height + CELL_MARGIN) + CELL_MARGIN

                    # 确保不超出屏幕边界
                    if x + self.cell_width > screen_width - 50:
                        self.cell_width = (screen_width - 50 - x) - CELL_MARGIN

                    cell = GameCell(x, y, self.cell_width, self.cell_height, text, cell_type, pair_id)
                    self.cells.append(cell)

        self.total_pairs = len(selected_data)

    def handle_click(self, pos):
        # 游戏结束后仍然可以点击按钮重新开始
        # 检查按钮点击(放在最前面,这样游戏结束后也能点击)
        if self.restart_button.is_clicked(pos):
            self.init_game()
            return

        if self.mode_button.is_clicked(pos):
            self.mode = "compounds" if self.mode == "elements" else "elements"
            self.init_game()
            return

        # 检查难度按钮点击
        if self.easy_button.is_clicked(pos) and self.difficulty != "easy":
            self.difficulty = "easy"
            self.easy_button.selected = True
            self.medium_button.selected = False
            self.hard_button.selected = False
            self.init_game()
            return

        if self.medium_button.is_clicked(pos) and self.difficulty != "medium":
            self.difficulty = "medium"
            self.easy_button.selected = False
            self.medium_button.selected = True
            self.hard_button.selected = False
            self.init_game()
            return

        if self.hard_button.is_clicked(pos) and self.difficulty != "hard":
            self.difficulty = "hard"
            self.easy_button.selected = False
            self.medium_button.selected = False
            self.hard_button.selected = True
            self.init_game()
            return

        # 如果游戏结束,不再处理单元格点击
        if self.game_over:
            return

        # 检查单元格点击
        for cell in self.cells:
            if cell.is_clicked(pos):
                if cell.selected or cell.matched:
                    return

                cell.selected = True
                self.selected_cells.append(cell)

                # 如果选中了两个单元格
                if len(self.selected_cells) == 2:
                    cell1, cell2 = self.selected_cells

                    # 检查是否配对成功
                    if cell1.pair_id == cell2.pair_id and cell1.cell_type != cell2.cell_type:
                        # 配对成功
                        cell1.matched = True
                        cell2.matched = True
                        self.selected_cells.clear()
                        self.matched_pairs += 1

                        # 检查游戏是否结束
                        if self.matched_pairs == self.total_pairs:
                            self.game_over = True
                    else:
                        # 配对失败,触发震动效果
                        self.shake_time = pygame.time.get_ticks()
                        self.shake_offset = (random.randint(-10, 10), random.randint(-10, 10))

                        # 短暂显示后取消选择
                        pygame.time.set_timer(pygame.USEREVENT, 800)

                return

    def cancel_selection(self):
        for cell in self.selected_cells:
            cell.selected = False
        self.selected_cells.clear()

    def update(self):
        current_time = pygame.time.get_ticks()

        # 处理震动效果
        if self.shake_time > 0:
            shake_duration = 300
            if current_time - self.shake_time < shake_duration:
                elapsed = current_time - self.shake_time
                progress = elapsed / shake_duration
                intensity = 10 * (1 - progress)
                angle = elapsed * 0.1
                self.shake_offset = (
                    intensity * math.sin(angle * 2),
                    intensity * math.cos(angle * 3)
                )
            else:
                self.shake_time = 0
                self.shake_offset = (0, 0)

        # 更新按钮悬停状态
        mouse_pos = pygame.mouse.get_pos()
        self.restart_button.hovered = self.restart_button.rect.collidepoint(mouse_pos)
        self.mode_button.hovered = self.mode_button.rect.collidepoint(mouse_pos)
        self.easy_button.hovered = self.easy_button.rect.collidepoint(mouse_pos)
        self.medium_button.hovered = self.medium_button.rect.collidepoint(mouse_pos)
        self.hard_button.hovered = self.hard_button.rect.collidepoint(mouse_pos)

    def draw(self, screen):
        # 应用震动偏移
        if self.shake_time > 0:
            screen_scroll = screen.copy()
            screen.fill(BACKGROUND)
            screen.blit(screen_scroll, self.shake_offset)
        else:
            screen.fill(BACKGROUND)

        # 绘制标题 - 将模式和标题合并显示
        if self.mode == "elements":
            title = "化学元素配对游戏(模式:元素)"
        else:
            title = "化学化合物配对游戏(模式:化合物)"

        try:
            title_surface = chinese_font.render(title, True, TITLE_COLOR)
        except:
            font = pygame.font.Font(None, 32)
            title_surface = font.render(title, True, TITLE_COLOR)

        screen.blit(title_surface, (screen_width // 2 - title_surface.get_width() // 2, 20))

        # 绘制进度
        progress_text = f"配对进度: {self.matched_pairs}/{self.total_pairs}"
        try:
            progress_surface = chinese_font.render(progress_text, True, TEXT_COLOR)
        except:
            font = pygame.font.Font(None, 24)
            progress_surface = font.render(progress_text, True, TEXT_COLOR)

        screen.blit(progress_surface, (20, 20))

        # 绘制难度按钮
        self.easy_button.draw(screen)
        self.medium_button.draw(screen)
        self.hard_button.draw(screen)

        # 绘制所有单元格
        for cell in self.cells:
            cell.draw(screen)

        # 绘制按钮(在遮罩之前绘制,这样按钮不会被遮住)
        self.restart_button.draw(screen)
        self.mode_button.draw(screen)

        # 如果游戏结束,显示胜利消息
        if self.game_over:
            # 创建一个透明遮罩,但不要太暗,让玩家能看到下面的按钮
            overlay = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 100))  # 降低透明度,让按钮可见
            screen.blit(overlay, (0, 0))

            win_text = "恭喜!游戏胜利!"
            try:
                win_surface = chinese_font.render(win_text, True, (255, 255, 255))
            except:
                font = pygame.font.Font(None, 32)
                win_surface = font.render(win_text, True, (255, 255, 255))

            screen.blit(win_surface, (screen_width // 2 - win_surface.get_width() // 2, screen_height // 2 - 50))

            restart_text = "点击重新开始按钮或按Enter键再来一局"
            try:
                restart_surface = chinese_font.render(restart_text, True, (255, 255, 255))
            except:
                font = pygame.font.Font(None, 24)
                restart_surface = font.render(restart_text, True, (255, 255, 255))

            screen.blit(restart_surface,
                        (screen_width // 2 - restart_surface.get_width() // 2, screen_height // 2 + 30))


def main():
    game = Game()
    clock = pygame.time.Clock()
    running = True

    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False

            elif event.type == MOUSEBUTTONDOWN:
                if event.button == 1:
                    game.handle_click(event.pos)

            elif event.type == KEYDOWN:
                # 游戏结束后按任意键重新开始
                if game.game_over:
                    game.init_game()

            elif event.type == pygame.USEREVENT:
                # 计时器事件,用于取消错误选择
                game.cancel_selection()
                pygame.time.set_timer(pygame.USEREVENT, 0)

        game.update()
        game.draw(screen)

        pygame.display.flip()
        clock.tick(60)

    pygame.quit()
    sys.exit()


if __name__ == "__main__":
    main()
相关推荐
梦幻精灵_cq2 小时前
问题切入『视角很重要』——ansi-color有效编码序列“单背景判定”小部件的“简洁精妙”
python
有代理ip2 小时前
成功请求的密码:HTTP 2 开头响应码深度解析
java·大数据·python·算法·php
0思必得02 小时前
[Web自动化] Selenium截图
前端·爬虫·python·selenium·自动化
放飞自我的Coder3 小时前
【PDF拆分 Python拆分左右并排PDF】
python·pdf
nimadan123 小时前
**AI漫剧爆款生成器2025推荐,解锁高互动率与平台适配的
人工智能·python
2401_857683543 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python
luoluoal3 小时前
基于opencv的疲劳检测系统(源码+文档)
python·mysql·django·毕业设计·源码