Python汉诺塔动画代码中坐标计算与圆盘堆叠逻辑

小岛来信
小岛来信
发布时间:2026-11-02 | 9人浏览过
原创

深入解析Python汉诺塔动画实现中的三柱坐标映射、圆盘几何尺寸与栈堆叠逻辑,以及三段式移动动画轨迹插值算法。

在编写 Python 汉诺塔动画程序(无论是基于 Pygame、Turtle 还是 Tkinter Canvas)时,许多开发者能够轻松写出递归求解序列,却在图形渲染与平滑移动环节遇到瓶颈。其本质原因在于:经典算法只关心抽象的“盘号从柱子 A 转移到柱子 C”,而图形系统必须精确掌握每一个圆盘在每一帧上的像素位置。本文直接拆解汉诺塔动画的核心技术点——屏幕物理坐标系与三柱布局计算、基于栈(Stack)的垂直堆叠高度映射,以及圆盘移动时的三段式插值轨迹,帮助你构建结构严密的动画引擎。

一、三柱体系与屏幕坐标系映射

图形渲染的第一步,是将逻辑上的“A、B、C 三根柱子”映射为窗口画布上的确切几何坐标。在主流图形引擎中,屏幕坐标系的原点 (0, 0) 均位于画布的左上角,水平向右为 X 轴正方向,垂直向下为 Y 轴正方向。

为了让画面均衡美观,我们需要在总宽度 width 和总高度 height 内对三根柱子进行等距居中排列:

  • 底座基准线(Base Y): 所有柱子立于同一水平基线上,通常设定在距离底部一定边距的位置,例如 base_y = height - 80。
  • 柱子水平中心(Column X): 将可用宽度划分为 4 个均等区间,3 根柱子的 X 坐标分别为:
    peg_x = [width * 0.25, width * 0.50, width * 0.75],对应索引 0: A 柱, 1: B 柱, 2: C 柱。
  • 柱体高度与安全高度(Safe Y): 柱高必须大于所有圆盘累加的总厚度。在柱顶上方还需预留一道水平缓冲区,称为“安全升空线” safe_y,用于圆盘在水平平移时避开所有柱顶,防止动画穿模。
Python汉诺塔动画代码中坐标计算与圆盘堆叠逻辑
图1:三柱体系在屏幕坐标系中的几何定位与安全空域划分

二、圆盘几何尺寸映射与数据栈堆叠逻辑

汉诺塔的核心物理约束是“大盘在下,小盘在上”,这与计算机数据结构中的后进先出栈(LIFO Stack)完全吻合。在 Python 中,我们使用列表 list 模拟每根柱子上的圆盘堆叠状态。

1. 圆盘尺寸的线性插值计算

设圆盘总数为 n,盘号从 1(最小)至 n(最大)。如果每个盘的厚度固定为 disk_h,则圆盘宽度 disk_w 应当根据盘号线性递增:

# 根据盘号 disk_id (1 到 n) 计算宽度
disk_w = min_width + (disk_id - 1) * (max_width - min_width) / max(1, (total_disks - 1))

其中 max_width 必须严格小于相邻两柱间距(即 span),以防止左右柱子上的圆盘在画面中重叠穿插。

2. 基于栈索引的垂直堆叠定位公式

若柱子 A 当前存储的圆盘列表为 pegs['A'] = [4, 3, 1](列表首项为栈底,末项为栈顶):

  • 列表索引 stack_idx = 0 对应最底层的圆盘(盘号 4);
  • 列表索引 stack_idx = 2 对应顶层的圆盘(盘号 1)。

由于屏幕 Y 轴向下递增,第 stack_idx 层圆盘的绘制顶部 Y 坐标为:

top_y = base_y - (stack_idx + 1) * disk_h

结合柱子水平中心 center_x,该圆盘在画布上的完整矩形包围盒为:

rect_x = center_x - disk_w / 2
rect_y = top_y
# 绘制矩形: (rect_x, rect_y, disk_w, disk_h)
Python汉诺塔动画代码中坐标计算与圆盘堆叠逻辑
图2:数据结构中的列表栈索引与屏幕像素高度的对应关系

三、移动动画的三段式坐标插值与轨迹平滑

在动画过程中,如果直接用两点直线插值(从原柱位置斜向连线到目标柱位置),圆盘会在半空中斜穿其他柱子和已有圆盘,产生严重的视觉穿模。符合真实物理逻辑的平滑动画应当拆解为三段正交折线轨迹:

  1. 阶段一:垂直提升(Lift): 保持 X 坐标不变,圆盘从源柱顶部的当前物理 Y 坐标垂直匀速上升,直至达到顶部的安全飞行高度 safe_y。
  2. 阶段二:水平巡航(Horizontal Shift): 保持 Y 坐标等于 safe_y 不变,圆盘沿水平方向从源柱中心 X 坐标平移到目标柱中心 X 坐标。
  3. 阶段三:垂直降落(Drop): 保持 X 坐标在目标柱中心,圆盘从 safe_y 垂直向下匀速降落,直至精确抵达目标柱根据其当前已有盘数算出的目标堆叠 Y 坐标。

对于每一阶段,我们引入进度归一化参数 t ∈ [0.0, 1.0] 进行线性插值(Linear Interpolation,简写为 LERP):

def lerp(start_pos, end_pos, t):
    return start_pos + (end_pos - start_pos) * t
Python汉诺塔动画代码中坐标计算与圆盘堆叠逻辑
图3:圆盘移动的三段式正交折线插值轨迹与空间防穿模机制

四、完整 Python 动画引擎代码示例

下面给出一段基于 Pygame 架构的完整汉诺塔动画实现。代码清晰地展示了:递归指令队列生成、逻辑栈与渲染解耦、以及在三段式动画完成时才正式修改目标栈数据的严谨状态机机制。

import pygame
import sys

# 1. 窗口与画布常量定义
SCREEN_WIDTH = 900
SCREEN_HEIGHT = 500
BASE_Y = 400
SAFE_Y = 100
DISK_HEIGHT = 24
NUM_DISKS = 4

# 三根柱子的水平中心 X 坐标
PEG_X = {
    'A': int(SCREEN_WIDTH * 0.25),
    'B': int(SCREEN_WIDTH * 0.50),
    'C': int(SCREEN_WIDTH * 0.75)
}

# 调色板定义 (云白、雾蓝、灰绿风格)
COLOR_BG = (243, 246, 250)
COLOR_PEG = (107, 136, 166)
COLOR_BASE = (180, 199, 217)
COLOR_DISK_NORMAL = (113, 163, 151)
COLOR_DISK_MOVING = (71, 106, 138)
COLOR_TEXT = (44, 62, 80)

# 2. 生成汉诺塔移动序列队列
def generate_hanoi_moves(n, source, target, auxiliary, move_list):
    if n > 0:
        generate_hanoi_moves(n - 1, source, auxiliary, target, move_list)
        move_list.append((source, target))
        generate_hanoi_moves(n - 1, auxiliary, target, source, move_list)

def get_disk_width(disk_id, total_disks):
    min_w, max_w = 50, 180
    if total_disks == 1:
        return max_w
    return min_w + (disk_id - 1) * (max_w - min_w) // (total_disks - 1)

def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("Python汉诺塔动画坐标计算演示")
    clock = pygame.time.Clock()

    # 初始化三根柱子的数据栈
    pegs = {
        'A': list(range(NUM_DISKS, 0, -1)),  # [4, 3, 2, 1]
        'B': [],
        'C': []
    }

    # 预计算所有移动指令
    move_queue = []
    generate_hanoi_moves(NUM_DISKS, 'A', 'C', 'B', move_queue)

    # 动画状态机变量
    current_moving_disk = None
    move_phase = 0  # 1: 上升, 2: 水平移动, 3: 下降
    anim_progress = 0.0  # 阶段内进度 0.0 -> 1.0
    anim_speed = 0.04    # 每一帧的插值步长

    start_x, start_y = 0, 0
    target_x, target_y = 0, 0
    curr_x, curr_y = 0, 0
    source_peg, target_peg = None, None

    running = True
    while running:
        clock.tick(60)

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

        # 状态机:如果当前没有处于动画状态,且队列中仍有动作,则启动下一步
        if current_moving_disk is None and move_queue:
            source_peg, target_peg = move_queue.pop(0)
            # 逻辑栈中弹出盘号
            current_moving_disk = pegs[source_peg].pop()
            
            # 计算起始坐标与目标坐标
            start_x = PEG_X[source_peg]
            start_y = BASE_Y - (len(pegs[source_peg]) + 1) * DISK_HEIGHT
            
            target_x = PEG_X[target_peg]
            target_y = BASE_Y - (len(pegs[target_peg]) + 1) * DISK_HEIGHT

            curr_x, curr_y = start_x, start_y
            move_phase = 1
            anim_progress = 0.0

        # 更新当前动画盘的坐标
        if current_moving_disk is not None:
            anim_progress += anim_speed
            if move_phase == 1:
                # 阶段 1: 垂直上升到 SAFE_Y
                curr_x = start_x
                curr_y = start_y + (SAFE_Y - start_y) * anim_progress
                if anim_progress >= 1.0:
                    curr_y = SAFE_Y
                    move_phase = 2
                    anim_progress = 0.0

            elif move_phase == 2:
                # 阶段 2: 水平平移到目标柱上方
                curr_y = SAFE_Y
                curr_x = start_x + (target_x - start_x) * anim_progress
                if anim_progress >= 1.0:
                    curr_x = target_x
                    move_phase = 3
                    anim_progress = 0.0

            elif move_phase == 3:
                # 阶段 3: 垂直下降到目标堆叠高度
                curr_x = target_x
                curr_y = SAFE_Y + (target_y - SAFE_Y) * anim_progress
                if anim_progress >= 1.0:
                    curr_y = target_y
                    # 动画结束,正式将盘号压入目标柱数据栈
                    pegs[target_peg].append(current_moving_disk)
                    current_moving_disk = None
                    move_phase = 0

        # 渲染主画布
        screen.fill(COLOR_BG)

        # 绘制底部基座
        pygame.draw.rect(screen, COLOR_BASE, (80, BASE_Y, SCREEN_WIDTH - 160, 16))

        # 绘制三根柱子
        for p_name, x in PEG_X.items():
            pygame.draw.rect(screen, COLOR_PEG, (x - 6, SAFE_Y + 20, 12, BASE_Y - (SAFE_Y + 20)))

        # 绘制静止在柱子上的圆盘
        for p_name, disk_list in pegs.items():
            cx = PEG_X[p_name]
            for idx, disk_id in enumerate(disk_list):
                dw = get_disk_width(disk_id, NUM_DISKS)
                top_y = BASE_Y - (idx + 1) * DISK_HEIGHT
                rect = (cx - dw // 2, top_y, dw, DISK_HEIGHT - 2)
                pygame.draw.rect(screen, COLOR_DISK_NORMAL, rect, border_radius=4)

        # 绘制正在处于飞行动画中的圆盘
        if current_moving_disk is not None:
            dw = get_disk_width(current_moving_disk, NUM_DISKS)
            rect = (curr_x - dw // 2, curr_y, dw, DISK_HEIGHT - 2)
            pygame.draw.rect(screen, COLOR_DISK_MOVING, rect, border_radius=4)

        pygame.display.flip()

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

五、状态维护中的常见问题与避坑指南

常见失误一:过早修改目标柱数据栈
很多初学者在动画刚触发时,就执行了 pegs[target].append(disk)。这会导致目标柱上的既有堆叠高度计算立即增加;若后续有其他逻辑读取目标高度,或者界面重新渲染,盘子就会在空中飞行时,地面上已经凭空出现了一个占位盘,造成逻辑混乱。正确的做法是在阶段三完全落地完成时才将元素放入目标列表。
优化建议二:浮点数插值与整型像素转换
在每一帧中计算的 curr_x 和 curr_y 是浮点数,直接丢给渲染器时建议使用 int(round(curr_x)) 转换为像素整型。浮点插值可以保证在不同帧率下平滑移动,而整型取整可避免边缘出现亚像素抖动或模糊。

六、汉诺塔动画系统整体执行流程总结

综上所述,一个健壮的汉诺塔动画系统由四个紧密协作的子模块构成:离散递归生成器、状态调度中心、连续插值引擎与物理渲染器。各模块权责分明,将算法的“步骤”转化为画面的“流动”。

Python汉诺塔动画代码中坐标计算与圆盘堆叠逻辑
图4:Python 汉诺塔动画系统架构与数据流闭环全景

通过将离散的递归调用转换为显式指令队列,并结合严格的“三段式折线插值”与“栈顶高度偏移公式”,即可彻底解决汉诺塔动画中的穿模、重叠与坐标漂移问题。这套坐标映射与状态机解耦模式同样适用于华容道、八皇后、排序算法可视化等各类算法动画开发。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表