目录

汉诺塔游戏

7. 汉诺塔类

8. 移动圆盘

9. 移动演示

10. 递归问题

11. 任意展示

12. 鼠标操作


汉诺塔游戏

汉诺塔(Tower of Hanoi),是一个源于印度古老传说的益智玩具。这个传说讲述了大梵天创造世界的时候,他做了三根金刚石柱子,并在其中一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门将这些圆盘从下面开始按大小顺序重新摆放在另一根柱子上,并规定在小圆盘上不能放大圆盘,同时在三根柱子之间一次只能移动一个圆盘。当盘子的数量增加时,移动步骤的数量会呈指数级增长,圆盘数为n时,总步骤数steps为2^n – 1。

n = 64, steps = 2^64 – 1 = 18446744073709551616 ≈ 1.845 x 10^19

汉诺塔问题是一个递归问题,也可以使用非递归法来解决,例如使用栈来模拟递归过程。这个问题不仅是一个数学和逻辑问题,也是一个很好的教学工具,可以用来教授递归、算法和逻辑思考等概念。同时,汉诺塔游戏也具有一定的娱乐性,人们可以通过解决不同规模的汉诺塔问题来挑战自己的智力和耐心。

本篇接着上期讲下去,这次争取把圆盘移动起来,前篇的链接地址:

Python 一步一步教你用pyglet制作汉诺塔游戏-CSDN博客

7. 汉诺塔类

前篇的最后已经初步把汉诺塔的架子搭好了,可以显示但移动的描述很复杂。所以得改造一下,把三个塔架设计到一个类中,方便圆盘的移动,运行后效果不变:

代码:

import pyglet window = pyglet.window.Window(800, 500, caption='汉诺塔')pyglet.gl.glClearColor(1, 1, 1, 1)batch = pyglet.graphics.Batch()Color = (182,128,18),(25,65,160),(56,170,210),(16,188,78),(20,240,20),(240,240,20),(255,128,20),(240,20,20),(245,60,138)class Disk:def __init__(self, x, y, color=(0,0,0), width=200, height=20):self.cir1 = pyglet.shapes.Circle(x+width/2-height/2, y, radius=height/2, color=color, batch=batch)self.cir2 = pyglet.shapes.Circle(x-width/2+height/2, y, radius=height/2, color=color, batch=batch)self.rect = pyglet.shapes.Rectangle(x-width/2+height/2, y-height/2, width-height, height, color=color, batch=batch)class Hann:def __init__(self, x, y, order=2, space=250, thickness=20, width=200, height=300):assert(order>1)self.pole = [pyglet.shapes.Line(x-i*space, y, x-i*space, y+height, width=thickness, color=Color[0], batch=batch) for i in range(-1,2)]self.disk = [Disk(x+i*space, y, color=Color[0], width=width+thickness, height=thickness) for i in range(-1,2)]self.x, self.y = x, yself.order = orderself.space = spaceself.thickness = thicknessself.width = widthself.height = (height-thickness*2)/orderself.step = (width-thickness)/(order+1)coordinates = [(self.x-space, self.y+(i+1)*self.height-(self.height-thickness)/2) for i in range(order)]self.beads = [Disk(*xy, Color[i%8+1], width=self.width-i*self.step, height=self.height) for i,xy in enumerate(coordinates)]@window.eventdef on_draw():window.clear()batch.draw()hann = Hann(window.width/2, 120, 8)pyglet.app.run()

8. 移动圆盘

给圆盘类和汉诺塔类各设计一个.move()方法,主旨思想是圆盘的相对位置移动,不需要在调用时计算各种控件的具体坐标值。

代码:

import pyglet window = pyglet.window.Window(800, 500, caption='汉诺塔')pyglet.gl.glClearColor(1, 1, 1, 1)batch = pyglet.graphics.Batch()Color = (182,128,18),(25,65,160),(56,170,210),(16,188,78),(20,240,20),(240,240,20),(255,128,20),(240,20,20),(245,60,138)class Disk:def __init__(self, x, y, color=(0,0,0), width=200, height=20):self.cir1 = pyglet.shapes.Circle(x+width/2-height/2, y, radius=height/2, color=color, batch=batch)self.cir2 = pyglet.shapes.Circle(x-width/2+height/2, y, radius=height/2, color=color, batch=batch)self.rect = pyglet.shapes.Rectangle(x-width/2+height/2, y-height/2, width-height, height, color=color, batch=batch)def move(self, dx, dy):self.cir1.x += dx; self.cir1.y += dyself.cir2.x += dx; self.cir2.y += dyself.rect.x += dx; self.rect.y += dyclass Hann:def __init__(self, x, y, order=2, space=250, thickness=20, width=200, height=300):assert(order>1)self.pole = [pyglet.shapes.Line(x-i*space, y, x-i*space, y+height, width=thickness, color=Color[0], batch=batch) for i in range(-1,2)]self.disk = [Disk(x+i*space, y, color=Color[0], width=width+thickness, height=thickness) for i in range(-1,2)]self.x, self.y = x, yself.order = orderself.space = spaceself.thickness = thicknessself.width = widthself.height = (height-thickness*2)/orderself.step = (width-thickness)/(order+1)coordinates = [(self.x-space, self.y+(i+1)*self.height-(self.height-thickness)/2) for i in range(order)]self.beads = [Disk(*xy, Color[i%8+1], width=self.width-i*self.step, height=self.height) for i,xy in enumerate(coordinates)]self.array = [[*range(order)], [], []]def move(self, pole1, pole2):if self.array[pole1]:bead = self.array[pole1].pop()if self.array[pole2] and bead<self.array[pole2][-1]:print('大盘不能搬到小盘上')return Falseelse:print('所选择的塔架为空!')return Noneself.beads[bead].move((pole2-pole1)*self.space, (len(self.array[pole2])-len(self.array[pole1]))*self.height)self.array[pole2].append(bead)return True@window.eventdef on_draw():window.clear()batch.draw()hann = Hann(window.width/2, 100, 7)hann.move(0,1)hann.move(0,2)hann.move(1,2)hann.move(0,1)hann.move(2,0)hann.move(2,1)hann.move(0,1)hann.move(0,2)pyglet.app.run()

9. 移动演示

pyglet.clock.schedule_interval(function, seconds) 是 Pyglet 库中的一个函数调用,用于定期调度另一个函数function在指定的时间间隔seconds内执行。

pyglet.clock.unschedule(function) 任务完成后使用此函数来取消调度,以避免不必要的资源消耗。

以下展示一个四层汉诺塔的移动演示:

代码:

import pyglet window = pyglet.window.Window(800, 500, caption='汉诺塔')pyglet.gl.glClearColor(1, 1, 1, 1)batch = pyglet.graphics.Batch()Color = (182,128,18),(25,65,160),(56,170,210),(16,188,78),(20,240,20),(240,240,20),(255,128,20),(240,20,20),(245,60,138)class Disk:def __init__(self, x, y, color=(0,0,0), width=200, height=20):self.cir1 = pyglet.shapes.Circle(x+width/2-height/2, y, radius=height/2, color=color, batch=batch)self.cir2 = pyglet.shapes.Circle(x-width/2+height/2, y, radius=height/2, color=color, batch=batch)self.rect = pyglet.shapes.Rectangle(x-width/2+height/2, y-height/2, width-height, height, color=color, batch=batch)def move(self, dx, dy):self.cir1.x += dx; self.cir1.y += dyself.cir2.x += dx; self.cir2.y += dyself.rect.x += dx; self.rect.y += dyclass Hann:def __init__(self, x, y, order=2, space=250, thickness=20, width=200, height=300):assert(order>1)self.pole = [pyglet.shapes.Line(x-i*space, y, x-i*space, y+height, width=thickness, color=Color[0], batch=batch) for i in range(-1,2)]self.disk = [Disk(x+i*space, y, color=Color[0], width=width+thickness, height=thickness) for i in range(-1,2)]self.x, self.y = x, yself.order = orderself.space = spaceself.thickness = thicknessself.width = widthself.height = (height-thickness*2)/orderself.step = (width-thickness)/(order+1)coordinates = [(self.x-space, self.y+(i+1)*self.height-(self.height-thickness)/2) for i in range(order)]self.beads = [Disk(*xy, Color[i%8+1], width=self.width-i*self.step, height=self.height) for i,xy in enumerate(coordinates)]self.array = [[*range(order)], [], []]def move(self, pole1, pole2):if self.array[pole1]:bead = self.array[pole1].pop()if self.array[pole2] and bead<self.array[pole2][-1]:print('大盘不能搬到小盘上')return Falseelse:print('所选择的塔架为空!')return Noneself.beads[bead].move((pole2-pole1)*self.space, (len(self.array[pole2])-len(self.array[pole1]))*self.height)self.array[pole2].append(bead)return True@window.eventdef on_draw():window.clear()batch.draw()def on_move(event):global movesif moves:x, y = moves.pop(0)hann.move(x, y)label.text = f'盘架{x+1}的圆盘移到盘架{y+1}上'else:label.text = '演示完毕!'pyglet.clock.unschedule(on_move)hann = Hann(window.width/2, 120, 4)moves = [(0,1), (0,2), (1,2), (0,1), (2,0), (2,1), (0,1), (0,2), (1,2), (1,0), (2,0), (1,2), (0,1), (0,2), (1,2)]label = pyglet.text.Label('汉诺塔圆盘自动移动演示', font_size=24, color=(0,0,0,255), x=window.width/2, y=50, anchor_x='center', batch=batch)pyglet.clock.schedule_interval(on_move, 1)pyglet.app.run()

10. 递归问题

虽然解决了自动演示的功能,但继续增加层数怎么办?还是要自己解决这个问题,本篇前言里就说了汉诺塔问题就是一个递归问题,所以就用递归函数来解决,通常写法:

def hanoi(n, start, mid, end):if n == 1:print('Move disk 1 from', start, 'to', end)else:hanoi(n-1, start, end, mid)print('Move disk', n, 'from', start, 'to', end)hanoi(n-1, mid, start, end)hanoi(4, 0, 1, 2)

运行的打印结果为:

Move disk 1 from 0 to 1
Move disk 2 from 0 to 2
Move disk 1 from 1 to 2
Move disk 3 from 0 to 1
Move disk 1 from 2 to 0
Move disk 2 from 2 to 1
Move disk 1 from 0 to 1
Move disk 4 from 0 to 2
Move disk 1 from 1 to 2
Move disk 2 from 1 to 0
Move disk 1 from 2 to 0
Move disk 3 from 1 to 2
Move disk 1 from 0 to 1
Move disk 2 from 0 to 2
Move disk 1 from 1 to 2

移动的坐标刚好与上一步指定的相同,但要改造这个函数,我们需要的是坐标返回值不是打印信息,所以指定一列表用于存放这些坐标,改造后的函数:

def hanoi(n, start, mid, end, moves=None):if moves is None:moves = []if n == 1:moves.append((start, end))else:hanoi(n-1, start, end, mid, moves)moves.append((start, end))hanoi(n-1, mid, start, end, moves)return movesfor order in (4,7,8):moves = hanoi(order, 0, 1, 2)print(len(moves)==2**order-1)print(moves)

运行结果:

True
[(0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2)]
True
[(0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (2, 0), (1, 0), (2, 1), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2), (1, 0), (2, 1), (2, 0), (1, 0), (1, 2), (0, 2), (0, 1), (2, 1), (0, 2), (1, 0), (1, 2), (0, 2)]
True
[(0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (2, 1), (0, 1), (2, 0), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2), (0, 1), (2, 0), (2, 1), (0, 1), (0, 2), (1, 2), (1, 0), (2, 0), (1, 2), (0, 1), (0, 2), (1, 2)]

由此,也验证了总步骤数 steps = 2^n- 1,n为层数。

11. 任意展示

解决递归问题后,就可以展示任意层数的移动演示了,就用上一步得到的7层汉诺塔的移动坐标来展示一下它的移动过程:

完整代码:

import pyglet window = pyglet.window.Window(800, 500, caption='汉诺塔')pyglet.gl.glClearColor(1, 1, 1, 1)batch = pyglet.graphics.Batch()Color = (182,128,18),(25,65,160),(56,170,210),(16,188,78),(20,240,20),(240,240,20),(255,128,20),(240,20,20),(245,60,138)class Disk:def __init__(self, x, y, color=(0,0,0), width=200, height=20):self.cir1 = pyglet.shapes.Circle(x+width/2-height/2, y, radius=height/2, color=color, batch=batch)self.cir2 = pyglet.shapes.Circle(x-width/2+height/2, y, radius=height/2, color=color, batch=batch)self.rect = pyglet.shapes.Rectangle(x-width/2+height/2, y-height/2, width-height, height, color=color, batch=batch)def move(self, dx, dy):self.cir1.x += dx; self.cir1.y += dyself.cir2.x += dx; self.cir2.y += dyself.rect.x += dx; self.rect.y += dyclass Hann:def __init__(self, x, y, order=2, space=250, thickness=20, width=200, height=300):assert(order>1)self.pole = [pyglet.shapes.Line(x-i*space, y, x-i*space, y+height, width=thickness, color=Color[0], batch=batch) for i in range(-1,2)]self.disk = [Disk(x+i*space, y, color=Color[0], width=width+thickness, height=thickness) for i in range(-1,2)]self.x, self.y = x, yself.order = orderself.space = spaceself.thickness = thicknessself.width = widthself.height = (height-thickness*2)/orderself.step = (width-thickness)/(order+1)coordinates = [(self.x-space, self.y+(i+1)*self.height-(self.height-thickness)/2) for i in range(order)]self.beads = [Disk(*xy, Color[i%8+1], width=self.width-i*self.step, height=self.height) for i,xy in enumerate(coordinates)]self.array = [[*range(order)], [], []]def move(self, pole1, pole2):if self.array[pole1]:bead = self.array[pole1].pop()if self.array[pole2] and bead<self.array[pole2][-1]:self.array[pole1].append(bead)print('大盘不能搬到小盘上')return Falseelse:print('所选择的塔架为空!')return Noneself.beads[bead].move((pole2-pole1)*self.space, (len(self.array[pole2])-len(self.array[pole1]))*self.height)self.array[pole2].append(bead)return True@window.eventdef on_draw():window.clear()batch.draw()@window.eventdef on_key_press(symbol, modifiers):global startif not start:start = Truepyglet.clock.schedule_interval(on_move, 0.3)@window.eventdef on_mouse_press(x, y, button, modifier):global startif not start:start = Truepyglet.clock.schedule_interval(on_move, 0.3)def hanoi(n, start, mid, end, moves=None):if moves is None:moves = []if n == 1:moves.append((start, end))else:hanoi(n-1, start, end, mid, moves)moves.append((start, end))hanoi(n-1, mid, start, end, moves)return movesdef on_move(event):global moves,stepsif moves:x, y = moves.pop(0)hanns.move(x, y)label.text = f'盘架{x+1}的圆盘移到盘架{y+1}上'message.text = f'总步骤数:{steps}\t当前步数:{steps-len(moves)}'else:label.text = '演示完毕!'pyglet.clock.unschedule(on_move)order = 7start = Falsehanns = Hann(window.width/2, 120, order)label = pyglet.text.Label('汉诺塔圆盘自动移动演示,任意按键开始......', font_size=24, color=(0,0,0,255), x=window.width/2, y=50, anchor_x='center', batch=batch)moves = hanoi(order, 0, 1, 2)steps = len(moves)message = pyglet.text.Label(f'总步骤数:{steps}\t当前步数:{steps-len(moves)}', font_size=24, color=(0,0,0,255), x=100, y=450, batch=batch)pyglet.app.run()

12. 鼠标操作

增加一个新的属性.poleheight,把原来的.height属性改为.beadheight以示区别;再给塔类增加两个方法判断鼠标点击和是否成功:

def on_mouse_over(self, x, y):
for i in range(-1,2):
if hanns.x-hanns.width/2 < x-i*hanns.space < hanns.x+hanns.width/2 and hanns.y-hanns.thickness/2 < y < hanns.y+hanns.poleheight:
return i+1
def success(self):
return len(self.array[2]) == self.order

最后配合鼠标点击事件,就能开始玩游戏了!

@window.event
def on_mouse_press(x, y, dx, dy):
global moves
if not hanns.success():
pole = hanns.on_mouse_over(x, y)
if pole is not None:
print(pole)
moves.append(pole)
if len(moves)==2:
hanns.move(*moves)
moves.clear()
if hanns.success():
print(‘Success!’)

完整代码:

import pyglet window = pyglet.window.Window(800, 500, caption='汉诺塔')pyglet.gl.glClearColor(1, 1, 1, 1)batch = pyglet.graphics.Batch()Color = (182,128,18),(25,65,160),(56,170,210),(16,188,78),(20,240,20),(240,240,20),(255,128,20),(240,20,20),(245,60,138)class Disk:def __init__(self, x, y, color=(0,0,0), width=200, height=20):self.cir1 = pyglet.shapes.Circle(x+width/2-height/2, y, radius=height/2, color=color, batch=batch)self.cir2 = pyglet.shapes.Circle(x-width/2+height/2, y, radius=height/2, color=color, batch=batch)self.rect = pyglet.shapes.Rectangle(x-width/2+height/2, y-height/2, width-height, height, color=color, batch=batch)def move(self, dx, dy):self.cir1.x += dx; self.cir1.y += dyself.cir2.x += dx; self.cir2.y += dyself.rect.x += dx; self.rect.y += dyclass Hann:def __init__(self, x, y, order=2, space=250, thickness=20, width=200, height=300):assert(order>1)self.pole = [pyglet.shapes.Line(x-i*space, y, x-i*space, y+height, width=thickness, color=Color[0], batch=batch) for i in range(-1,2)]self.disk = [Disk(x+i*space, y, color=Color[0], width=width+thickness, height=thickness) for i in range(-1,2)]self.x, self.y = x, yself.order = orderself.space = spaceself.thickness = thicknessself.width = widthself.poleheight = heightself.beadheight = (height-thickness*2)/orderself.step = (width-thickness)/(order+1)coordinates = [(self.x-space, self.y+(i+1)*self.beadheight-(self.beadheight-thickness)/2) for i in range(order)]self.beads = [Disk(*xy, Color[i%8+1], width=self.width-i*self.step, height=self.beadheight) for i,xy in enumerate(coordinates)]self.array = [[*range(order)], [], []]def move(self, pole1, pole2):if self.array[pole1]:bead = self.array[pole1].pop()if self.array[pole2] and bead<self.array[pole2][-1]:self.array[pole1].append(bead)print('大盘不能搬到小盘上')return Falseelse:print('所选择的塔架为空!')return Noneself.beads[bead].move((pole2-pole1)*self.space, (len(self.array[pole2])-len(self.array[pole1]))*self.beadheight)self.array[pole2].append(bead)return Truedef on_mouse_over(self, x, y):for i in range(-1,2):if hanns.x-hanns.width/2 < x-i*hanns.space < hanns.x+hanns.width/2 and hanns.y-hanns.thickness/2 < y < hanns.y+hanns.poleheight:return i+1def success(self):return len(self.array[2]) == self.order@window.eventdef on_draw():window.clear()batch.draw()@window.eventdef on_mouse_press(x, y, dx, dy):global movesif not hanns.success():pole = hanns.on_mouse_over(x, y)if pole is not None:print(pole)moves.append(pole)if len(moves)==2:hanns.move(*moves)moves.clear()if hanns.success():print('Success!')moves = []hanns = Hann(window.width/2, 120, 4)pyglet.app.run()

本篇至此暂时告一段落,下期会继续给游戏增加信息提示等等功能,敬请期待……