Python应用开发学习:Pygame中实现切换开关及鼠标拖动连续填充功能

一、前言

我的上一篇日志《Python应用开发学习: Pygame 中实现数字水平靠右对齐和垂直靠底对齐》,记录了我参照自己经常玩的一个小游戏,做一个趣味数独游戏时,如何解决游戏界面中数字对齐的过程。

今天放上这个学习系列的第二篇日志,讲的是如何在这个趣味数独游戏中实现切换开关及鼠标拖动连续填充的功能。

二、开发需求

我要模仿的这款游戏的玩法是根据数字提示在棋盘中填充叉或方块。在棋盘的下方有一个切换开关,可以在叉和方块直接进行切换。切换到左边的叉,就能在棋盘中填充叉(代表空格),切换到右边的方块,就能在棋盘中填充方块(代表实心格)。另外,该游戏是款手游,是支持手指滑动进行连续填充的。

我希望自己做的这个仿品也具备这两个功能,其中连续填充功能是通过按下鼠标左键后拖动来实现。

三、借助AI开发软件

对于这两个功能的实现,我是没有任何的头绪的,必须要借助AI了。上一篇日志讲到,因为碰到DeepSeek不能正常工作,我使用了文心快码来帮助我解决问题,感觉还是差点火候。DeepSeek的异常只是短暂的,恢复正常后,我还是愿意用它的。

在用AI之前,我也没有把自己的需求想那么全面的。一开始只是想实现切换开关,于是,我向DeepSeek提出了如下的需求:"我用pygame开发小游戏,游戏规则是对10*10的棋盘中的格子进行标记,现在需要实现一个状态切换开关,这个开关修改变量fill_cell。当开关拨向左侧时,fill_cell = 0,此时鼠标点中格子,表示向格子内填充字符x;当开关拨向右侧时,fill_cell = 1,此时鼠标点中格子,表示向格子内填充字符■。已经填充过的格子不能再填充,请帮我实现这个功能"

我始终觉得DeepSeek的理解能力很强,对我提出的需求能够准确把握关键点,给出的方案切合需求,有时还能进行扩展(如叉用红色,方块用蓝色)

将DeepSeek提供的代码复制到pycharm中运行,实现了切换开关功能,不过,游戏界面中的切换开关上的方块显示存在bug。对于这个情况,我并不意外,毕竟我提出需求时是提到了字符■的,这个特殊字符容易造成显示不正常。

(切换开关-左,填叉,开关上的方块显示不正常)

(切换开关-右,填方块)

这不是个大问题,让AI改改:"上述代码经过测试,发现 draw_switch()方法中 绘制开关标签的部分,"■"不能在正常显示,那么"x"和"■"可否用其它形式显示(如图形),并不局限于使用字符"

DeeSeek也认为我提到的问题很常见,并按我的需求给出了用图形替代字符的解决方案

再次进行测试,切换开关上的方块能正常显示了。

(切换开关正常显示了,不过上方的文字不正常)

不过DeepSeek在开关上方添加的"标记类型:"几个字不能正常显示,这是因为字体设置代码中没有指定字体导致的,将相关代码( font = pygame.font.SysFont(None, 24) )做一下修改,指定字体(将None改为"simhei")就能够正常显示了。

(设置好字体,就可以正常显示中文了)

切换开关功能实现后,我又希望AI能帮我实现鼠标拖动连续填充功能:"你给出的示例非常好,不过,还存在一个不足,那就是,要往棋盘中的单元格填充选择的图形(交叉线或实心方块)只能一个个的点选单元格,我希望能够实现按下鼠标左键后移动鼠标,凡是被鼠标滑过的格子都能被选中,并填充图形,请帮我实现该功能。"

DeepSeek的上下文关联能力是很强的,没有出现新的方案与之前的方案出现冲突(上一篇日志中文心快码就出现过类似的问题),在这个实现鼠标拖动连续填充功能的方案中,仍然保持了已填充的单元格不会重复填充的限制。测试还发现,不仅可以支线拖动填充,还能斜线拖动填充,挺好的。

(实现了鼠标拖动连续填充了)

另外,在Pycharm中出现了一个警告提示:

意外类型:(int | Any, str)可能的类型:(SupportsIndex, None)(slice, Iterable[None])

是针对grid[row][col]二维列表的,这个二维列表是用来存储每个格子填充的内容的。虽然这个问题不影响程序的运行,我还是问了一下DeepSeek:"你提供的代码中语句 grid[row][col] = 'cross' 和 grid[row][col] = 'square' 被pycharm警告,警告信息是:意外类型:(int | Any, str)可能的类型:(SupportsIndex, None)(slice, Iterable[None]) ,这是怎么回事?"

DeepSeek给出解释是,这个警告是因为 PyCharm 的类型检查器无法确定 grid 列表的确切类型。在 Python 中,列表可以包含任何类型的元素,但 PyCharm 的类型检查器需要更明确的类型信息以避免潜在的错误。它给了我们三种解决方案:

方案1:使用类型注释(推荐)

python 复制代码
# 存储每个格子的内容
grid: List[List[Optional[str]]] = [[None for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]

方案2:使用枚举类型(更清晰)

python 复制代码
class CellState(Enum):

    EMPTY = None

    CROSS = "cross"

SQUARE = "square"



# 然后在初始化网格时使用

grid: List[List[CellState]] = [[CellState.EMPTY for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]



# 在填充格子时if fill_cell == 0:

    grid[row][col] = CellState.CROSSelse:

grid[row][col] = CellState.SQUARE



# 在绘制时if grid[row][col] == CellState.CROSS:

    # 绘制红色交叉

    draw_cross(x+5, y+5, CELL_SIZE-10, RED)elif grid[row][col] == CellState.SQUARE:

    # 绘制蓝色方块

    draw_square(x+5, y+5, CELL_SIZE-10, BLUE)

方案3:禁用特定警告

如果你不想修改代码结构,可以在 PyCharm 中禁用这个特定警告:

1.将光标放在警告上

2.按 Alt+Enter 打开快速修复菜单

3.选择 "Suppress for statement" 或 "Suppress for function"

对之前的代码适当修改,消除警告并设置字体,确保中文正常显示后的代码如下:

python 复制代码
import pygame
import sys
from typing import List, Optional

# 初始化Pygame
pygame.init()

# 常量定义
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
GRID_SIZE = 10
CELL_SIZE = 40
GRID_OFFSET_X = 50
GRID_OFFSET_Y = 50
SWITCH_X = 500
SWITCH_Y = 50
SWITCH_WIDTH = 120
SWITCH_HEIGHT = 40
SWITCH_KNOB_WIDTH = 50

# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
LIGHT_GRAY = (230, 230, 230)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 180, 0)

# 创建窗口
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("棋盘标记游戏")

# 游戏状态
fill_cell = 0  # 0表示填充交叉图形,1表示填充方块
grid: List[List[Optional[str]]] = [[None for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]  # 存储每个格子的内容
font = pygame.font.SysFont(None, 24)
dragging = False  # 跟踪是否正在拖动
last_cell = None  # 记录上一个处理的格子,避免重复处理


def draw_cross(x, y, size, color):
    """绘制交叉图形"""
    pygame.draw.line(screen, color, (x, y), (x + size, y + size), 3)
    pygame.draw.line(screen, color, (x + size, y), (x, y + size), 3)


def draw_square(x, y, size, color):
    """绘制实心方块"""
    pygame.draw.rect(screen, color, (x, y, size, size))


def draw_grid():
    """绘制棋盘"""
    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):
            x = GRID_OFFSET_X + col * CELL_SIZE
            y = GRID_OFFSET_Y + row * CELL_SIZE

            # 绘制格子
            pygame.draw.rect(screen, WHITE, (x, y, CELL_SIZE, CELL_SIZE))
            pygame.draw.rect(screen, BLACK, (x, y, CELL_SIZE, CELL_SIZE), 1)

            # 如果格子有内容,绘制内容
            if grid[row][col] is not None:
                if grid[row][col] == 'cross':
                    # 绘制红色交叉
                    draw_cross(x + 5, y + 5, CELL_SIZE - 10, RED)
                else:  # 绘制蓝色方块
                    draw_square(x + 5, y + 5, CELL_SIZE - 10, BLUE)


def draw_switch():
    """绘制状态切换开关"""
    # 绘制开关背景
    pygame.draw.rect(screen, LIGHT_GRAY, (SWITCH_X, SWITCH_Y, SWITCH_WIDTH, SWITCH_HEIGHT))
    pygame.draw.rect(screen, BLACK, (SWITCH_X, SWITCH_Y, SWITCH_WIDTH, SWITCH_HEIGHT), 2)

    # 绘制开关标签 - 使用图形替代字符
    # 左侧标签:红色交叉
    draw_cross(SWITCH_X + 15, SWITCH_Y + SWITCH_HEIGHT // 2 - 10, 20, RED)

    # 右侧标签:蓝色方块
    draw_square(SWITCH_X + SWITCH_WIDTH - 35, SWITCH_Y + SWITCH_HEIGHT // 2 - 10, 20, BLUE)

    # 绘制开关滑块
    if fill_cell == 0:
        knob_x = SWITCH_X + 5
    else:
        knob_x = SWITCH_X + SWITCH_WIDTH - SWITCH_KNOB_WIDTH - 5

    pygame.draw.rect(screen, GRAY, (knob_x, SWITCH_Y + 5, SWITCH_KNOB_WIDTH, SWITCH_HEIGHT - 10))
    pygame.draw.rect(screen, BLACK, (knob_x, SWITCH_Y + 5, SWITCH_KNOB_WIDTH, SWITCH_HEIGHT - 10), 2)

    # 在滑块上绘制当前状态的图形
    if fill_cell == 0:
        draw_cross(knob_x + SWITCH_KNOB_WIDTH // 2 - 10, SWITCH_Y + SWITCH_HEIGHT // 2 - 10, 20, RED)
    else:
        draw_square(knob_x + SWITCH_KNOB_WIDTH // 2 - 10, SWITCH_Y + SWITCH_HEIGHT // 2 - 10, 20, BLUE)

    # 添加说明文字
    label = font.render("标记类型:", True, BLACK)
    screen.blit(label, (SWITCH_X, SWITCH_Y - 25))

    # 添加操作提示
    hint = font.render("提示: 按住鼠标左键拖动可连续标记", True, BLACK)
    screen.blit(hint, (SWITCH_X, SWITCH_Y + SWITCH_HEIGHT + 10))


def is_point_in_grid(pos):
    """检查点击位置是否在棋盘内"""
    x, y = pos
    return (GRID_OFFSET_X <= x < GRID_OFFSET_X + GRID_SIZE * CELL_SIZE and
            GRID_OFFSET_Y <= y < GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE)


def get_grid_cell(pos):
    """获取点击位置对应的格子坐标"""
    x, y = pos
    col = (x - GRID_OFFSET_X) // CELL_SIZE
    row = (y - GRID_OFFSET_Y) // CELL_SIZE
    return row, col


def is_point_in_switch(pos):
    """检查点击位置是否在开关内"""
    x, y = pos
    return (SWITCH_X <= x < SWITCH_X + SWITCH_WIDTH and
            SWITCH_Y <= y < SWITCH_Y + SWITCH_HEIGHT)


def toggle_switch():
    """切换开关状态"""
    global fill_cell
    fill_cell = 1 - fill_cell  # 在0和1之间切换


def fill_cell_at_pos(pos):
    """在指定位置填充格子"""
    global last_cell

    if is_point_in_grid(pos):
        row, col = get_grid_cell(pos)

        # 检查是否是新的格子
        if (row, col) == last_cell:
            return

        if 0 <= row < GRID_SIZE and 0 <= col < GRID_SIZE and grid[row][col] is None:
            if fill_cell == 0:
                grid[row][col] = 'cross'
            else:
                grid[row][col] = 'square'

            # 更新最后处理的格子
            last_cell = (row, col)


def handle_mouse_down(pos):
    """处理鼠标按下事件"""
    global dragging, last_cell

    if is_point_in_switch(pos):
        toggle_switch()
    elif is_point_in_grid(pos):
        dragging = True
        last_cell = None  # 重置最后处理的格子
        fill_cell_at_pos(pos)  # 填充当前格子


def handle_mouse_up():
    """处理鼠标释放事件"""
    global dragging, last_cell
    dragging = False
    last_cell = None  # 重置最后处理的格子


def handle_mouse_motion(pos):
    """处理鼠标移动事件"""
    if dragging and is_point_in_grid(pos):
        fill_cell_at_pos(pos)


# 主游戏循环
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:  # 左键按下
                handle_mouse_down(event.pos)
        elif event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:  # 左键释放
                handle_mouse_up()
        elif event.type == pygame.MOUSEMOTION:
            handle_mouse_motion(event.pos)

    # 绘制背景
    screen.fill(GRAY)

    # 绘制棋盘和开关
    draw_grid()
    draw_switch()

    # 更新显示
    pygame.display.flip()

# 退出游戏
pygame.quit()
sys.exit()

四、实现效果

我并没有将所有工作都交给,通过AI获得的代码还不能直接使用,还需要我对其进行修改,并融入到自己的代码中,这样的过程也可以学到不少的知识,同时也比全部由自己编写代码要高效。融入新的代码后,我的程序运行效果如下:

五、代码展示

最后放上我制作的示例代码,可直接运行,运行效果就如上图,供大家参考。

python 复制代码
import pygame
import sys
import time
from typing import List, Optional

# 初始化 pygame
pygame.init()

# 屏幕大小
SCREEN_WIDTH, SCREEN_HEIGHT = 610, 740
# 棋盘规格(行、列数)
BOARD_SIZE = 15
# 网格单元格大小(单位:像素)
CELL_SIZE = 30
# 边距
MARGIN_X = 140  # 棋盘左边距
MARGIN_Y = 200  # 棋盘顶边距
# 切换按钮参数
SWITCH_WIDTH = 120
SWITCH_HEIGHT = 40
SWITCH_KNOB_WIDTH = 50  # 滑块宽度
SWITCH_X = SCREEN_WIDTH // 2 - SWITCH_WIDTH // 2  # 切换按钮起始坐标X
SWITCH_Y = MARGIN_Y + CELL_SIZE * BOARD_SIZE + 30  # 切换按钮起始坐标Y
# 帧率
FPS = 10
# 文字对齐参数
RIGHT_BOUNDARY = 130  # 水平排布,靠右对齐的边界
BOTTOM_BOUNDARY = 195  # 垂直排布,靠下对齐的边界
LINE_SPACING = 30  # 水平排布,行间距
CHAR_SPACING = 10  # 水平排布,行内字符间距
COL_SPACING = 30  # 垂直排布,列之间的水平间距
ROW_SPACING = 25  # 垂直排布,列内垂直间距

# 颜色定义常量
BACKGROUND = (230, 230, 250)
# TITLE_COLOR = (255, 215, 0)
# BUTTON_COLOR = (70, 130, 180)  # 正常按钮颜色
# BUTTON_HOVER_COLOR = (100, 160, 210)  # 正常按钮悬停颜色
# BUTTON_TEXT_COLOR = (255, 255, 255)  # 按钮文本颜色
# TOGGLE_ACTIVE_COLOR = (80, 155, 55)  # 切换按钮激活状态的颜色
# TOGGLE_ACTIVE_HOVER_COLOR = (100, 175, 75)  # 切换按钮激活状态的悬停颜色
GRID_COLOR = (120, 80, 40)  # 棋盘网格颜色
# CELL_COLOR = (105, 150, 255)  # 单元格颜色
# TIP_COLOR = (80, 155, 55)  # 单元格提示状态颜色
# INPUT_NUM_COLOR = (30, 60, 225)  # 玩家输入的文字颜色
NUM_BACKGROUND = (190, 190, 250)  # 特别模式下侧边数字条的底色
FILL_COLOR = (30, 60, 225)  # 特别模式,正确填充结果的颜色
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
LIGHT_GRAY = (220, 220, 220)
DARK_GRAY = (100, 100, 100)
RED = (255, 0, 0)
# GREEN = (80, 155, 55)
# BLUE = (105, 150, 255)

# 创建屏幕
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('趣味数独')  # 窗口标题

# 字体
# button_font = pygame.font.SysFont("simhei", 32)  # 按钮字体
number_size = 20
number_font = pygame.font.SysFont("simhei", number_size)  # 棋盘左侧和上侧的数字
number_font_info = {
    "font": number_font,
    "size": number_size,
}
# mark_font = pygame.font.SysFont("simhei", 12)  # 用于标记状态下输入的数字
info_font = pygame.font.SysFont("simhei", 20)  # 信息字体


# 游戏状态类
class GameState:
    def __init__(self):
        self.fill_cell = 1  # 0表示填充交叉图形,1表示填充方块
        self.dragging = False  # 跟踪是否正在拖动
        self.last_cell = None  # 记录上一个处理的格子,避免重复处理
        self.error_count = 0  # 当前错误数


# 计时器类
class Timer:
    def __init__(self):
        self.start_time = None
        self.elapsed_time = 0
        self.is_running = False
        self.is_finished = False

    def start(self):
        if not self.is_running and not self.is_finished:
            self.start_time = time.time()
            self.is_running = True

    def restart(self):
        self.start_time = time.time()
        self.is_running = True

    def stop(self):
        if self.is_running:
            self.elapsed_time = self.get_elapsed_time()
            self.is_running = False
            self.is_finished = True

    def get_elapsed_time(self):
        if self.is_running:
            return time.time() - self.start_time
        else:
            return self.elapsed_time

    def get_formatted_time(self):
        elapsed = self.get_elapsed_time()
        minutes = int(elapsed // 60)
        seconds = int(elapsed % 60)
        return f"{minutes:02d}:{seconds:02d}"

    def reset(self):
        self.start_time = None
        self.elapsed_time = 0
        self.is_running = False
        self.is_finished = False


def draw_board(surface, grid: int, size: int, margin_x: int, margin_y: int, color):
    """
    绘制棋盘
    :param surface: 游戏屏幕对象
    :param grid:  棋盘网格数
    :param size:  棋盘格子大小
    :param margin_x: 棋盘与左侧间距
    :param margin_y: 棋盘与顶部间距
    :param color: 网格颜色
    :return :
    """
    for i in range(grid + 1):
        # 线条数比网格数多1
        # 绘制垂直网格线
        pygame.draw.line(surface, color,
                         (margin_x + i * size, margin_y),
                         (margin_x + i * size, margin_y + grid * size))
        # 绘制水平网格线
        pygame.draw.line(surface, color,
                         (margin_x, margin_y + i * size),
                         (margin_x + grid * size, margin_y + i * size))
    # 边框加粗
    pygame.draw.line(surface, color,
                     (margin_x - 1, margin_y - 1),
                     (margin_x - 1, margin_y + grid * size + 1))  # 左边框1
    pygame.draw.line(surface, color,
                     (margin_x - 2, margin_y - 2),
                     (margin_x - 2, margin_y + grid * size + 2))  # 左边框2
    pygame.draw.line(surface, color,
                     (margin_x + grid * size + 1, margin_y - 1),
                     (margin_x + grid * size + 1, margin_y + grid * size + 1))  # 右边框1
    pygame.draw.line(surface, color,
                     (margin_x + grid * size + 2, margin_y - 2),
                     (margin_x + grid * size + 2, margin_y + grid * size + 2))  # 右边框2
    pygame.draw.line(surface, color,
                     (margin_x, margin_y - 1),
                     (margin_x + grid * size, margin_y - 1))  # 上边框1
    pygame.draw.line(surface, color,
                     (margin_x - 1, margin_y - 2),
                     (margin_x + grid * size + 1, margin_y - 2))  # 上边框2
    pygame.draw.line(surface, color,
                     (margin_x - 1, margin_y + grid * size + 1),
                     (margin_x + grid * size + 1, margin_y + grid * size + 1))  # 下边框1
    pygame.draw.line(surface, color,
                     (margin_x - 2, margin_y + grid * size + 2),
                     (margin_x + grid * size + 2, margin_y + grid * size + 2))  # 下边框2
    # 中间加粗
    pygame.draw.line(surface, color,
                     (margin_x + 5 * size - 1, margin_y - 1),
                     (margin_x + 5 * size - 1, margin_y + grid * size + 1))  # 垂直线1-1
    pygame.draw.line(surface, color,
                     (margin_x + 5 * size + 1, margin_y - 1),
                     (margin_x + 5 * size + 1, margin_y + grid * size + 1))  # 垂直线1-2
    pygame.draw.line(surface, color,
                     (margin_x + 10 * size - 1, margin_y - 1),
                     (margin_x + 10 * size - 1, margin_y + grid * size + 1))  # 垂直线2-1
    pygame.draw.line(surface, color,
                     (margin_x + 10 * size + 1, margin_y - 1),
                     (margin_x + 10 * size + 1, margin_y + grid * size + 1))  # 垂直线2-2
    pygame.draw.line(surface, color,
                     (margin_x, margin_y + 5 * size - 1),
                     (margin_x + grid * size, margin_y + 5 * size - 1))  # 水平线1-1
    pygame.draw.line(surface, color,
                     (margin_x, margin_y + 5 * size + 1),
                     (margin_x + grid * size, margin_y + 5 * size + 1))  # 水平线1-2
    pygame.draw.line(surface, color,
                     (margin_x, margin_y + 10 * size - 1),
                     (margin_x + grid * size, margin_y + 10 * size - 1))  # 水平线2-1
    pygame.draw.line(surface, color,
                     (margin_x, margin_y + 10 * size + 1),
                     (margin_x + grid * size, margin_y + 10 * size + 1))  # 水平线2-2


def render_right_aligned_numbers(surface, numbers_2d: list, font_dict: dict, text_color: tuple, background: tuple,
                                 start_x: int, right_x: int, start_y: int, line_spacing: int, char_spacing: int = 0):
    """
    多行数字水平靠右对齐,支持字符间距
    :param surface:  游戏画面对象
    :param numbers_2d:  包含数字的二维列表
    :param font_dict:  pygame.font字体,字典形式
    :param text_color:  文字颜色(元组)
    :param background:  文字条底色(元组)
    :param start_x:  水平起始坐标点
    :param right_x:  水平右对齐坐标点
    :param start_y:  垂直方向起始坐标点
    :param line_spacing:  行间距
    :param char_spacing:  数字间距
    :return:
    """
    # 计算行宽(用于底色的绘制)
    row_width = right_x - start_x + 5
    for row in numbers_2d:
        # 计算整行宽度(含字符间距),num是row子列表(当前行)中的元素
        total_width = sum(font_dict["font"].size(str(num))[0] for num in row) + max(0, len(row) - 1) * char_spacing
        # 计算本行起始坐标
        x = right_x - total_width
        y = start_y
        # 绘制当前行的底色
        rect = pygame.Rect(start_x, y, row_width, font_dict["size"] + 2)
        pygame.draw.rect(surface, background, rect, border_radius=0)

        for num in row:
            # 逐一显示当前行的数字
            text = font_dict["font"].render(str(num), True, text_color)
            surface.blit(text, (x, y))
            x += text.get_width() + char_spacing

        start_y += line_spacing


def render_bottom_aligned_numbers(surface, numbers_2d: list, font_dict: dict, text_color: tuple, background: tuple,
                                  start_x: int, start_y: int, bottom_y: int, col_spacing: int, row_spacing: int = 0):
    """
    多列数字垂直靠下对齐,且每列水平居中
    :param surface:  游戏画面对象
    :param numbers_2d:  包含数字的二维列表
    :param font_dict:  pygame.font字体,字典形式
    :param text_color:  文字颜色(元组)
    :param background:  文字条底色(元组)
    :param start_x:  水平方向起始坐标
    :param start_y:  垂直方向起始坐标点
    :param bottom_y:  底对齐坐标
    :param col_spacing:  列间距
    :param row_spacing:  行间距
    :return:
    """
    # 计算列宽(用于底色的绘制)
    col_width = font_dict["size"] + 2
    for col_idx, col in enumerate(numbers_2d):
        # 计算当前列的水平、垂直起始坐标
        col_x = start_x + col_idx * col_spacing
        col_y = bottom_y - start_y
        # 计算当前列的水平中心点
        col_center_x = col_x + col_spacing // 2
        # 以底部坐标为基准,计算当前列的起始纵坐标
        y = bottom_y - len(col) * row_spacing
        # 绘制当前列的底色
        rect = pygame.Rect(col_x + 3, start_y, col_width, col_y - 2)
        pygame.draw.rect(surface, background, rect, border_radius=0)

        for num in col:
            # 逐一显示当前列的数字
            text_surface = font_dict["font"].render(str(num), True, text_color)
            # 数字水平居中:中心点 - 数字宽度/2
            x = col_center_x - text_surface.get_width() // 2
            surface.blit(text_surface, (x, y))
            y += row_spacing


def draw_cross(surface, x, y, size, color):
    """
    绘制交叉图形
    :param surface: 游戏屏幕对象
    :param x:  起始坐标x
    :param y:  起始坐标y
    :param size:  图形尺寸
    :param color:  图形颜色
    :return:
    """
    pygame.draw.line(surface, color, (x, y), (x + size, y + size), 3)
    pygame.draw.line(surface, color, (x + size, y), (x, y + size), 3)


def draw_square(surface, x, y, size, color):
    """
    绘制实心方块
    :param surface:  游戏屏幕对象
    :param x:  起始坐标x
    :param y:  起始坐标y
    :param size:  图形尺寸
    :param color:  图形颜色
    :return:
    """
    pygame.draw.rect(surface, color, (x, y, size, size))


def draw_switch(surface, switch_x: int, switch_y: int, switch_width: int, switch_height: int,
                switch_knob_width: int, fill_cell: int):
    """
    绘制状态切换开关
    :param surface:  游戏屏幕对象
    :param switch_x:  起始坐标x
    :param switch_y:  起始坐标y
    :param switch_width:  切换开关宽
    :param switch_height:   切换开关高
    :param switch_knob_width:  滑块宽
    :param fill_cell:  填充标记
    :return:
    """
    # 绘制开关背景
    pygame.draw.rect(surface, LIGHT_GRAY, (switch_x, switch_y, switch_width, switch_height))
    pygame.draw.rect(surface, BLACK, (switch_x, switch_y, switch_width, switch_height), 2)

    # 绘制开关标签
    # 左侧标签:交叉图形
    draw_cross(surface, switch_x + 15, switch_y + switch_height // 2 - 10, 20, FILL_COLOR)

    # 右侧标签:方块图形
    draw_square(surface, switch_x + switch_width - 35, switch_y + switch_height // 2 - 10, 20, FILL_COLOR)

    # 绘制开关滑块
    if fill_cell == 0:
        knob_x = switch_x + 5
    else:
        knob_x = switch_x + switch_width - switch_knob_width - 5

    pygame.draw.rect(surface, BACKGROUND, (knob_x, switch_y + 5, switch_knob_width, switch_height - 10))
    pygame.draw.rect(surface, BLACK, (knob_x, switch_y + 5, switch_knob_width, switch_height - 10), 2)

    # 在滑块上绘制当前状态的图形
    if fill_cell == 0:
        draw_cross(surface, knob_x + switch_knob_width // 2 - 10, switch_y + switch_height // 2 - 10, 20, FILL_COLOR)
    else:
        draw_square(surface, knob_x + switch_knob_width // 2 - 10, switch_y + switch_height // 2 - 10, 20, FILL_COLOR)


def display_graphics_within_cells(surface, grid: list, grid_offset_x: int, grid_offset_y: int, grid_size: int,
                                  cell_size: int, color):
    """
    在游戏屏幕上显示单元格内图形
    :param surface:  游戏屏幕对象
    :param grid:  记录玩家填充图形的列表
    :param grid_offset_x:  棋盘左侧边界偏移量X
    :param grid_offset_y:  棋盘顶侧边界偏移量Y
    :param grid_size:  棋盘大小,长(宽)格子数
    :param cell_size:  单元格边长
    :param color:  图形颜色
    :return:
    """
    for row in range(grid_size):
        for col in range(grid_size):
            x = grid_offset_x + col * cell_size
            y = grid_offset_y + row * cell_size
            if grid[row][col] is not None:
                # 正确答案与错误答案的颜色有区别,待后续完善。
                if grid[row][col] == 0:
                    # 绘制交叉图形
                    draw_cross(surface, x + 5, y + 5, cell_size - 10, color)
                else:  # 绘制方块图形
                    draw_square(surface, x + 5, y + 5, cell_size - 10, color)


def is_point_in_switch(pos: tuple, switch_x: int, switch_y: int, switch_width: int, switch_height: int):
    """检查点击位置是否在开关内"""
    x, y = pos
    return (switch_x <= x < switch_x + switch_width and
            switch_y <= y < switch_y + switch_height)


def is_point_in_grid(pos: tuple, grid_offset_x: int, grid_offset_y: int, grid_size: int, cell_size: int):
    """
    检查点击位置是否在棋盘内
    :param pos:   鼠标点击处坐标
    :param grid_offset_x:  棋盘左侧边界偏移量X
    :param grid_offset_y:   棋盘顶侧边界偏移量Y
    :param grid_size:  棋盘大小,长(宽)格子数
    :param cell_size:  单元格边长
    :return:  判定结果,True或False
    """
    x, y = pos
    return (grid_offset_x <= x < grid_offset_x + grid_size * cell_size and
            grid_offset_y <= y < grid_offset_y + grid_size * cell_size)


def get_grid_cell(pos: tuple, grid_offset_x: int, grid_offset_y: int, cell_size: int):
    """
    获取点击位置对应的格子坐标
    :param pos:  鼠标点击处坐标
    :param grid_offset_x:  棋盘左侧边界偏移量X
    :param grid_offset_y:  棋盘顶侧边界偏移量Y
    :param cell_size:  单元格边长
    :return row, col:  坐标点在棋盘内的行列号
    """
    x, y = pos
    col = (x - grid_offset_x) // cell_size
    row = (y - grid_offset_y) // cell_size
    return row, col


def fill_cell_at_pos(pos: tuple, grid: list, grid_offset_x: int, grid_offset_y: int, grid_size: int, cell_size: int, game_state):
    """
    在指定位置填充格子
    :param pos:  鼠标点击处坐标
    :param grid:  记录玩家填充图形的列表
    :param grid_offset_x:  棋盘左侧边界偏移量X
    :param grid_offset_y:  棋盘顶侧边界偏移量Y
    :param grid_size:  棋盘大小,长(宽)格子数
    :param cell_size:  单元格边长
    :param game_state:  游戏状态类对象
    :return:
    """
    if is_point_in_grid(pos, grid_offset_x, grid_offset_y, grid_size, cell_size):
        row, col = get_grid_cell(pos, grid_offset_x, grid_offset_y, cell_size)
        # 当前格子是上一个处理的格子
        if (row, col) == game_state.last_cell:
            return
        # 当前格子是新格子
        if 0 <= row < grid_size and 0 <= col < grid_size and grid[row][col] is None:
            if game_state.fill_cell == 0:
                grid[row][col] = 0
            else:
                grid[row][col] = 1

            # 更新最后处理的格子
            game_state.last_cell = (row, col)


def toggle_switch(game_state):
    """
    切换开关状态
    :param game_state:  游戏状态类对象
    :return:
    """
    game_state.fill_cell = 1 - game_state.fill_cell  # 在0和1之间切换


def handle_mouse_down(pos: tuple, grid: list, game_state):
    """
    处理鼠标按下事件
    :param pos:   鼠标左键点击位置,元组
    :param grid:  记录玩家填充图形的列表
    :param game_state:  游戏状态类对象
    :return:
    """
    if is_point_in_switch(pos, SWITCH_X, SWITCH_Y, SWITCH_WIDTH, SWITCH_HEIGHT):
        # 切换开关
        toggle_switch(game_state)
    elif is_point_in_grid(pos, MARGIN_X, MARGIN_Y, BOARD_SIZE, CELL_SIZE):
        game_state.dragging = True
        game_state.last_cell = None  # 重置最后处理的格子
        # 填充当前格子
        fill_cell_at_pos(pos, grid, MARGIN_X, MARGIN_Y, BOARD_SIZE, CELL_SIZE, game_state)
        # 检查对错。。。待补充


def handle_mouse_up(game_state):
    """
    处理鼠标释放事件
    :param game_state:  游戏状态类对象
    :return:
    """
    game_state.dragging = False
    game_state.last_cell = None  # 重置最后处理的格子


def handle_mouse_motion(pos: tuple, grid: list, game_state):
    """处理鼠标移动事件"""
    if game_state.dragging and is_point_in_grid(pos, MARGIN_X, MARGIN_Y, BOARD_SIZE, CELL_SIZE):
        fill_cell_at_pos(pos, grid, MARGIN_X, MARGIN_Y, BOARD_SIZE, CELL_SIZE, game_state)


def quit_game():
    print("退出游戏")
    pygame.quit()
    sys.exit()


def main():
    """主程序"""
    # 创建游戏状态
    game_state = GameState()
    # 数据初始化
    timer = Timer()
    error_limit = 5  # 最大错误数
    # 存储每个格子的填充内容
    board: List[List[Optional[int]]] = [[None for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]

    # 行、列数字(演示用)
    horizontal_nums = [[3, 8], [2, 3, 2], [4, 1, 1], [3, 1, 2, 2, 1], [1, 2, 2, 2, 1],
                       [3, 1, 2, 2, 1], [1, 1, 1], [1, 3, 1, 3], [3, 10], [2, 11],
                       [1, 6, 3], [7, 3], [1, 4, 4], [7, 5], [15]]
    vertical_nums = [[2, 4, 3], [4, 1, 2, 1, 2], [4, 1, 1, 4], [3, 1, 5], [6],
                     [7], [10, 2], [2, 5, 1], [2, 3, 4, 1], [1, 3, 2, 1],
                     [1, 4, 2], [1, 3, 2, 3], [2, 3, 8], [2, 8], [15]]

    # 开始计时
    timer.start()

    # 主循环
    clock = pygame.time.Clock()
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                # 处理鼠标左键按下事件
                if event.button == 1:
                    handle_mouse_down(event.pos, board, game_state)
            elif event.type == pygame.MOUSEBUTTONUP:
                # 处理鼠标左键释放实践
                if event.button == 1:
                    handle_mouse_up(game_state)
            elif event.type == pygame.MOUSEMOTION:
                # 处理鼠标移动事件
                handle_mouse_motion(event.pos, board, game_state)

        # 绘制背景
        screen.fill(BACKGROUND)
        # 绘制难度等级
        level_text = info_font.render(f"趣味数独-填格子", True, DARK_GRAY)
        screen.blit(level_text, (20, 20))
        # 绘制错误次数
        error_text = info_font.render(f"错误次数:{game_state.error_count}/{error_limit}", True, DARK_GRAY)
        screen.blit(error_text, (SCREEN_WIDTH // 2 - error_text.get_width() // 2, 20))
        # 显示计时器
        time_text = info_font.render(f"{timer.get_formatted_time()}", True, DARK_GRAY)
        screen.blit(time_text, (SCREEN_WIDTH - time_text.get_width() - 20, 20))

        # 绘制棋盘网格线
        draw_board(screen, BOARD_SIZE, CELL_SIZE, MARGIN_X, MARGIN_Y, GRID_COLOR)
        # 绘制棋盘内填充图形
        display_graphics_within_cells(screen, board, MARGIN_X, MARGIN_Y, BOARD_SIZE, CELL_SIZE, FILL_COLOR)
        # 绘制切换开关
        draw_switch(screen, SWITCH_X, SWITCH_Y, SWITCH_WIDTH, SWITCH_HEIGHT, SWITCH_KNOB_WIDTH,
                    game_state.fill_cell)

        # 测试数据
        # 水平靠右对齐(后5个参数,左侧x,右侧x,起始y,行距,字符间距)
        render_right_aligned_numbers(screen, horizontal_nums, number_font_info, DARK_GRAY, NUM_BACKGROUND,
                                     20, RIGHT_BOUNDARY, MARGIN_Y, LINE_SPACING, CHAR_SPACING)

        # 垂直靠下对齐(后5个参数,起始x=100,底部y=500,列距80,行距30)
        render_bottom_aligned_numbers(screen, vertical_nums, number_font_info, DARK_GRAY, NUM_BACKGROUND,
                                      MARGIN_X, 60, BOTTOM_BOUNDARY, COL_SPACING, ROW_SPACING)

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

        # 退出游戏
    pygame.quit()
    sys.exit()


if __name__ == "__main__":
    main()
相关推荐
inside8022 小时前
11月10日学习总结--numpy的基本操作
学习·numpy
JELEE.2 小时前
Django中的clean()方法和full_clean()方法
后端·python·django
想暴富,学技术2 小时前
AI提示词学习基础(一)
人工智能·学习
2401_841495642 小时前
【LeetCode刷题】移动零
数据结构·python·算法·leetcode·数组·双指针法·移动零
quant_19862 小时前
全面解析美股行情API
经验分享·后端·python·websocket·程序人生·区块链
Danceful_YJ2 小时前
32.Bahdanau 注意力
pytorch·python·深度学习
曲鸟2 小时前
用Python和MediaPipe实现实时手指识别
开发语言·python
创业之路&下一个五年3 小时前
系统分析师,我又考后总结了。
学习·总结
Highcharts.js3 小时前
时间序列图的“性能陷阱”:Highcharts “金融级”优化方案
前端·python·金融