Python应用开发学习: Pygame 中实现数字水平靠右对齐和垂直靠底对齐
一、前言
最近一段时间手头没有应用开发需求,但我也没有闲着,而是学习用pygame开发小游戏,相继尝试了贪吃蛇、五子棋和数独游戏,积累了一点游戏开发经验。
平日小憩时,我会玩玩手机上的小游戏。其中一款特殊的数独游戏我经常玩。这款游戏,并不是往格子里填数字,而是根据游戏提供的数字提示,在格子里做标记。有了之前积累的开发经验,我觉得可以用Pygame模仿这个手游也做一个趣味数独游戏。本篇日志记录了关于游戏界面开发的相关内容。


二、开发需求
这个游戏在界面上与普通数独游戏的不同之处在于,数字不显示在格子内,而是在格子的左侧和上侧,且分别是水平靠右对齐和垂直靠底对齐。画格子和在格子内做标记的技能,我已经在之前的开发中学到了,这个特殊的数字对齐方式我还是第一次碰到。在游戏界面设计上,这是要优先解决的问题。
三、借助AI开发软件
我之前搞软件开发,会经常借助于DeepSeek,但它偶尔会出问题,无法正常使用。除了DeepSeek,我偶尔也会用文心快码,其他的AI没怎么用过。我之所以爱用DeepSeek,是因为,它可以通过开启新对话,生成不同的对话记录,这样方便以后查询。而文心快码是将所有的对话都放在一起的,不方便查阅历史信息。
这次遇到了DeepSeek不能正常使用,就用文心快码来解决问题了。
我提出了自己的问题:“pygame中 我要实现多行水平排列的文字靠右对齐,多列垂直排列的文字靠下对齐怎么办”
文心快码提供了两个函数用于处理水平靠右对齐和垂直靠下对齐,但我测试后发现其提供的代码存在问题,只实现了多行水平文字靠右对齐,并未实现多列垂直文字靠下对齐。


(测试效果,垂直方向靠下对齐没有实现)
我接着提出第二问:“你提供的代码,只实现了多行水平文字靠右对齐,多列垂直文字并不是靠下对齐(还是水平方向显示的,且也没有对齐)请修改错误”

测试一下,之前发现的错误已得到修正。

不过AI提供的示例中基本是针对文字而非纯数字,我将示例中的文字替换成数字后测试,就发现了问题。我把多列垂直文字的示例修改成 ["2 3", "4 5 6 7", "8 9 10"] 的纯数字后,再测试,结果数字10被拆分成了1和0,这显然不是我想要的结果。

(将文字改成纯数字测试就发现了问题,10被拆分了)
我将新的需求发给AI:“我要显示的是1-15的数字,这种方法对数值大于9的数值会造成错误显示,该怎么解决”

这次,AI只给了一个水平排列的示例,且与我的要求有出入。这不是我要的答案,看样子,文心快码的上下文关联存在问题。需要一次把需求提足才行。

整理下思绪后,我重新提问:“我需要分别水平靠右对齐输出多行数字和垂直靠下对齐输出多行数字,都要控制行数字之间的行间距和列间距,每行的数字个数不同,数字范围是1-15,请帮我实现”

这次AI给的代码不会再将大于9的数字拆分了,但垂直排列的数字实现确是靠顶对齐,而非靠下对齐。这和我第一次提问后得到的结果有相似之处。另外,水平靠右对齐的字符间距并没有控制参数,无法调整。这让我怀疑其文字理解能力了。

(大于10的数字不会被拆分,但垂直对齐不符合要求)
我只好针对实现对齐的两个函数提出具体的修改要求:“上面的render_right_aligned_numbers中还需要控制字符之间的间距。render_bottom_aligned_numbers没有实现靠下对齐,请修改这两个问题”

得到修改后的代码后,再次进行测试,终于符合要求了。但仔细看测试结果,发现了一个新问题,就是render_bottom_aligned_numbers函数输出的同一列数字在水平方向是居左而没有居中,垂直的第三列数字是[8, 9, 10],就很明显。不过这个问题并没有在之前的需求中提到过,不能算是AI的问题。

(垂直数列的数字水平方向是靠左对齐的,不符合我的预期)
为了追求更好的效果,只得继续向AI提出需求了:“测试中又发现一个新问题render_bottom_aligned_numbers输出的同一列没有在水平方向居中(而是居左),比如vertical_nums中有一列数字是[8, 9, 10],显示出来就居左,显得不美观,请帮我设置成水平方向居中”

虽然render_bottom_aligned_numbers函数修改了,但最终的显示效果却和修改前一样,AI改了个寂寞。
不得已,第七次提问:“我测试你修改后的render_bottom_aligned_numbers函数,仍然没有实现每列的数字水平居中,请检查代码,看看是哪里不对”

这次,没有掉链子,实现了垂直排列的数字水平居中了。
(最终的效果)
这次使用文心快码的体验,感觉它还差点火候,输出的结果与提的需求有可能会存在偏差,需要不断的提示,才能得到修正。所幸,最终得到了我们想要的结果,比起自己去写代码还是省事多了。
第七次提问后,AI给出的代码如下:
import pygamepygame.init()def render_right_aligned_numbers(numbers_2d, font, right_x, start_y, line_spacing, char_spacing=0):"""多行数字水平靠右对齐,支持字符间距"""for row in numbers_2d:# 计算整行宽度(含字符间距)total_width = sum(font.size(str(num))[0] for num in row) + max(0, len(row) - 1) * char_spacingx = right_x - total_widthy = start_yfor num in row:text = font.render(str(num), True, (255, 255, 255))screen.blit(text, (x, y))x += text.get_width() + char_spacingstart_y += line_spacingdef render_bottom_aligned_numbers(numbers_2d, font, start_x, bottom_y, col_spacing, row_spacing):"""多列数字垂直靠下对齐,且每列严格水平居中"""# 预计算每列最大宽度col_widths = [max(font.size(str(num))[0] for num in col) for col in numbers_2d]for col_idx, col in enumerate(numbers_2d):# 计算当前列的水平中心点col_center_x = start_x + col_idx * col_spacing + col_spacing // 2# 从底部向上渲染y = bottom_y - len(col) * row_spacingfor num in col:text_surface = font.render(str(num), True, (255, 255, 255))# 数字水平居中:中心点 - 数字宽度/2x = col_center_x - text_surface.get_width() // 2screen.blit(text_surface, (x, y))y += row_spacing# 使用示例
screen = pygame.display.set_mode((800, 600))
font = pygame.font.SysFont('Arial', 24)horizontal_nums = [[1, 2, 3],[4, 5, 6, 7],[8, 9, 10, 11, 12],[13, 14, 15]
]vertical_nums = [[1, 2, 3],[4, 5, 6, 7],[8, 9, 10],[11, 12, 13, 14, 15]
]running = True
while running:for event in pygame.event.get():if event.type == pygame.QUIT:running = Falsescreen.fill((0, 0, 0))# 水平靠右对齐(右侧x=700,起始y=50,行距40,字符间距15)render_right_aligned_numbers(horizontal_nums, font, 700, 50, 40, 15)# 垂直靠下对齐(起始x=100,底部y=500,列距80,行距30)render_bottom_aligned_numbers(vertical_nums, font, 100, 500, 80, 30)pygame.display.flip()pygame.quit()四、实现效果
有了AI提供的代码,搞定游戏界面中数字水平靠右对齐和垂直靠底对齐的问题就很轻松了。
由于我还需要为每行和每列数字添加一个带颜色的背景框,因此,AI的代码还不能直接使用,需要做一些修改。这一块不算复杂,我就没有求助于AI了,自己开动脑筋即可实现。最终呈现的效果如下图,比较接近我常玩的那个手游了。

注:当前界面中的数字是一组固定数据,待完成了生成数字的相关代码开发后,即可在每次新开游戏的时候,自动更新了。
五、代码展示
最后放上我制作的示例代码,可直接运行,运行效果就如上图,供大家参考。
import pygame
import sys
import time# 初始化 pygame
pygame.init()# 屏幕大小
SCREEN_WIDTH, SCREEN_HEIGHT = 610, 670
# 棋盘规格(行、列数)
BOARD_SIZE = 15
# 网格单元格大小(单位:像素)
CELL_SIZE = 30
# 边距
MARGIN_X = 140 # 左边距
MARGIN_Y = 200 # 顶边距
# 帧率
FPS = 10
# 文字对齐参数
RIGHT_BOUNDARY = 130 # 水平排布,靠右对齐的边界
BOTTOM_BOUNDARY = 195 # 垂直排布,靠下对齐的边界
LINE_SPACING = 30 # 水平排布,行间距
CHAR_SPACING = 10 # 水平排布,行内字符间距
COL_SPACING = 30 # 垂直排布,列之间的水平间距
ROW_SPACING = 25 # 垂直排布,列内垂直间距# 颜色定义常量
BACKGROUND = (230, 230, 250)
GRID_COLOR = (120, 80, 40) # 棋盘网格颜色
NUM_BACKGROUND = (190, 190, 250) # 特殊模式下侧边数字条的底色
DARK_GRAY = (100, 100, 100)# 创建屏幕
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('趣味数独') # 窗口标题# 字体
number_size = 20
number_font = pygame.font.SysFont("simhei", number_size) # 棋盘左侧和上侧的数字
number_font_info = {"font": number_font,"size": number_size,
}
info_font = pygame.font.SysFont("simhei", 20)# 计时器类
class Timer:def __init__(self):self.start_time = Noneself.elapsed_time = 0self.is_running = Falseself.is_finished = Falsedef start(self):if not self.is_running and not self.is_finished:self.start_time = time.time()self.is_running = Truedef restart(self):self.start_time = time.time()self.is_running = Truedef stop(self):if self.is_running:self.elapsed_time = self.get_elapsed_time()self.is_running = Falseself.is_finished = Truedef get_elapsed_time(self):if self.is_running:return time.time() - self.start_timeelse:return self.elapsed_timedef 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 = Noneself.elapsed_time = 0self.is_running = Falseself.is_finished = Falsedef 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))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 + 5for 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_widthy = 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)screen.blit(text, (x, y))x += text.get_width() + char_spacingstart_y += line_spacingdef 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"] + 2for col_idx, col in enumerate(numbers_2d):# 计算当前列的水平、垂直起始坐标col_x = start_x + col_idx * col_spacingcol_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)# 数字水平居中:中心点 - 数字宽度/2x = col_center_x - text_surface.get_width() // 2screen.blit(text_surface, (x, y))y += row_spacingdef quit_game():print("退出游戏")pygame.quit()sys.exit()def main():"""主程序"""# 数据初始化timer = Timer()error_count = 0 # 当前错误数error_limit = 5 # 最大错误数# 行、列数字(演示用)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 = Truewhile running:for event in pygame.event.get():if event.type == pygame.QUIT:running = False# 绘制背景screen.fill(BACKGROUND)# 绘制难度等级level_text = info_font.render(f"趣味数独-填格子", True, DARK_GRAY)screen.blit(level_text, (20, 20))# 绘制错误次数error_text = info_font.render(f"错误次数:{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)# 测试数据# 水平靠右对齐(后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()
