-
Notifications
You must be signed in to change notification settings - Fork 0
/
hanoi_tower.py
149 lines (132 loc) · 6.05 KB
/
hanoi_tower.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3
# Piotr Beling, 2024
from pyglet import shapes, clock, app, window, text
from pyglet.window import key
AUTO_PLAY_MOVE_TIME = 1 # in sec.
class HanoiWindow(window.Window):
def __init__(self):
super().__init__(caption="Tower of Hanoi", resizable=True)
self.reset_game(2)
clock.schedule(self.auto_play)
def reset_game(self, max_disk):
'''Set the game state to the initial position with the largest disk of the given size (counting from 0)'''
self.max_disk = max_disk
self.position = [d for d in range(self.max_disk, -1, -1)], [], []
self.plan = [(self.max_disk, 0, 1)]
self.auto_play_time = None
self.selected_rod = None
def auto_play(self, dt):
'''Play optimal move each 1 second or do nothing if auto_play_time is None'''
if self.auto_play_time is None: return
self.auto_play_time += dt
if self.auto_play_time > AUTO_PLAY_MOVE_TIME:
self.auto_play_time = 0
self.make_optimal_move()
def make_move(self, src, dst):
'''Move one disk from src to dst.'''
self.position[dst].append(self.position[src].pop())
def push_to_plan(self, src, dst):
'''Plan moving one disk from src to dst.'''
self.plan.append((0, src, dst))
def make_user_move(self, src, dst):
'''Move one disk from src to dst and plan corrective moves.'''
optimal_move = self.optimal_move() # optimal move
if optimal_move is None: # if the game is already solved
self.push_to_plan(dst, src) # unmaking the move solves it again
else:
optim_src, optim_dst = optimal_move
if src == optim_src: # disk to play is right
if dst != optim_dst: # but the destination is wong
self.push_to_plan(dst, optim_dst) # move from wrong to right destination to fix
else: # disk to play is wrong, to fix:
self.push_to_plan(optim_src, optim_dst) # make correct move
self.push_to_plan(dst, src) # after unmake the wrong one
self.make_move(src, dst)
def select(self, rod):
'''Try to select the rod or try to make a move if another rod is already selected.'''
if self.selected_rod is None:
if self.position[rod]:
self.selected_rod = rod
self.auto_play_time = None
else:
if self.selected_rod == rod:
self.selected_rod = None
return
d = self.position[self.selected_rod][-1]
if not self.position[rod] or self.position[rod][-1] > d:
self.make_user_move(self.selected_rod, rod)
self.selected_rod = None
def make_optimal_move(self):
'''Make optimal move is the game is not already solved.'''
try:
src, dst = self.optimal_move()
except TypeError:
pass
else:
self.make_move(src, dst)
def optimal_move(self):
'''Get optimal move or None if the game is already solved.'''
if not self.plan: return None
n, src, dst = self.plan.pop()
while n > 0:
third = 3 - src - dst
n -= 1
self.plan.append((n, third, dst))
self.plan.append((0, src, dst))
dst = third
return src, dst
def on_key_press(self, symbol, modifiers):
if symbol == key._1: self.select(0)
if symbol == key._2: self.select(1)
if symbol == key._3: self.select(2)
if symbol in (key.ENTER, key.NUM_ENTER):
self.make_optimal_move()
self.auto_play_time = None
self.selected_rod = None
if symbol in (key.PLUS, key.NUM_ADD) and self.max_disk < 8: self.reset_game(self.max_disk+1)
if symbol in (key.MINUS, key.NUM_SUBTRACT) and self.max_disk > 1: self.reset_game(self.max_disk-1)
if symbol in (key.P, key.SPACE): self.auto_play_time = AUTO_PLAY_MOVE_TIME if self.auto_play_time is None else None
def on_mouse_press(self, x, y, button, modifiers):
if y > self.height-60:
self.make_optimal_move()
else:
self.select(x * 3 // self.width)
def on_draw(self):
rod_x_center = self.width / 4
rod_width = self.width / 5
rod_y = self.height / 5
rod_height = 3 * self.height / 5
disk_height = rod_height / 10
min_disk_width = rod_width / 2
disk_width_delta = rod_width * 9 / 10 - min_disk_width
self.clear()
for rod_i in range(3):
rod_x = rod_x_center*(rod_i+1)
rod_color = color=(123, 123, 255) if rod_i == self.selected_rod else (55, 55, 255)
base = shapes.Rectangle(
rod_x, rod_y,
rod_width, disk_height,
rod_color)
base.anchor_position = rod_width/2, disk_height
base.draw()
rod = shapes.Rectangle(
rod_x, rod_y,
rod_width/10, rod_height,
rod_color)
rod.anchor_x = rod_width/20
rod.draw()
for d_i, d in enumerate(self.position[rod_i]):
disk_w = min_disk_width + disk_width_delta * d / self.max_disk
disk = shapes.Rectangle(rod_x, rod_y+d_i*disk_height+d_i+1,
disk_w, disk_height,
(255 * d // self.max_disk,
255 * (self.max_disk-d) // self.max_disk,
0))
disk.anchor_x = disk_w/2
disk.draw()
text.Label('Make optimal move (ENTER)',
font_size=36,
x=window.width//2, y=self.height-30,
anchor_x='center', anchor_y='center').draw()
window = HanoiWindow()
app.run()