Skip to content

Instantly share code, notes, and snippets.

@lispandfound
Created April 16, 2018 10:47
Show Gist options
  • Save lispandfound/afa89003e63e55cfc6d3c9857b48a42c to your computer and use it in GitHub Desktop.
Save lispandfound/afa89003e63e55cfc6d3c9857b48a42c to your computer and use it in GitHub Desktop.
Towers of Hanoi Cairo Background
import itertools
import cairo
def interpret_state(disks, counter):
''' Return the position of the towers of hanoi after `counter` moves.
Efficient algorithm, runs in O(k) time, k being the number of disks.
This method abuses the binary relationship between Towers of Hanoi and moves.
See https://en.wikipedia.org/wiki/Tower_of_Hanoi#Binary_solution
for more info. Although their explanation of how the disks move is
rather counter intuitive, I recommend running though a few
solutions yourself to try and observe these patterns.
Args:
disks (int): The number of tower of hanoi disks
Returns:
list of int: The position of the disks (on peg 0..2)
Examples:
>>> interpret_state(3, 7)
[2, 2, 2]
>>> interpret_state(3, 4)
[1, 1, 2] '''
# The general rule to be observed (took some time to figure out) is
# that a disk k has moved (m + 2**k) // (2**(k + 1)) times after m moves.
move_map = [(counter + 2**i) // (2**(i + 1)) for i in range(disks)]
# We then map the number of times the disks have moved to their
# current positions.
# The pattern to observe (again this takes some time to work out)
# is that for an even number of disks, the even disks move left
# and the odd disks move right.
# The opposite is true for odd disk counts, the odd disks move
# left and the even disks move right.
# m moves right corresponds to m % 3, whilst m moves left
# corresponds to -m % 3.
peg_positions = [
-move_map[k] % 3 if (k % 2 != 0 and disks % 2 == 0)
or (k % 2 == 0 and disks % 2 != 0) else move_map[k] % 3
for k in range(disks)
]
return peg_positions
def draw_hanoi(width,
height,
state,
disk_widths,
peg_width,
peg_height=None,
disk_height=None,
background=(1, 1, 1),
peg=(0, 0, 0),
disk_colour=(0.207, 0.361, 0.231)):
''' Draw a tower of state of the tower of hanoi on a canvas.
Draw a tower of state of the tower of hanoi on a canvas of a
specified width, and height, and customizable peg/disk
heights/widths/colours.
Args:
width (int): The width of the canvas.
height (int): The height of the canvas.
state (list of int): The state (current positions) of the disks in the puzzle.
disk_widths (list of int): The widths of the individual disks.
peg_width (int): The widths of the pegs.
peg_height (int, optional): The height of the pegs. Defaults to 2/3rds of the height of the canvas.
disk_height (int, optional): The height of the disks. Defaults to `peg_height // len(state)`
background ((float, float, float), optional): The colour of the background. Defaults to #FFFFFF
peg ((float, float, float), optional): The colour of the pegs. Defaults to #000000.
disk_colour ((float, float, float), optional): The colour of the disks. Defaults to green.
Returns:
cairo.Surface: A cairo surface representing the state of the
towers of hanoi puzzle.
'''
peg_height = peg_height or int(height * 0.67)
disk_height = disk_height or peg_height // len(state)
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height)
context = cairo.Context(surface)
context.set_source_rgb(*background)
context.rectangle(0, 0, width, height)
context.fill()
# Split the width into 5 roughly equal parts, the first and
# last used for padding, the middle three as disk positions.
positions = [i * (width // 4) + min(i, width % 4) for i in range(4)]
for position in itertools.islice(positions, 1, len(positions)):
peg_rect = cairo.Rectangle(position - peg_width // 2,
height - peg_height, peg_width, peg_height)
context.set_source_rgb(*peg)
context.rectangle(*peg_rect)
context.fill()
# Now we draw the disks
# Disks are drawn in reverse order (largest to smallest) and
# on top of each other.
counts = [1, 1, 1]
for disk, peg in enumerate(reversed(state)):
disk_rect = cairo.Rectangle(
positions[peg + 1] - disk_widths[disk] // 2,
height - counts[peg] * disk_height, disk_widths[disk], disk_height)
counts[peg] += 1
context.set_source_rgb(*disk_colour)
context.rectangle(*disk_rect)
context.fill()
return surface
def rgb_to_cairo(rgb_string, prefixed_hash=True):
''' Convert a RGB hex colour to a cairo colour tuple.
Args:
rgb_string (string): An RGB hex colour, e.g #FFFFFF
prefixed_hash (bool): If True string will be parsed from an
offset of 1 (thus skipping '#').
Returns:
(float, float, float): A cairo compatible colour tuple, e.g (1.0, 1.0, 1.0)
Examples:
>>> rgb_to_cairo('#FFFFFF')
(1.0, 1.0, 1.0)
>>> rgb_to_cairo('000000', prefixed_hash=False)
(0.0, 0.0, 0.0) '''
start = 1 if prefixed_hash else 0
red = int(rgb_string[start:start + 2], 16)
blue = int(rgb_string[start + 2:start + 4], 16)
green = int(rgb_string[start + 4:start + 6], 16)
return (red / 255, blue / 255, green / 255)
WIDTH = 1920
HEIGHT = 1080
PEG_COLOUR = rgb_to_cairo('#272d2d')
BACKGROUND_COLOUR = rgb_to_cairo('#f8fff7')
DISK_COLOUR = rgb_to_cairo('#8d99ae')
PEG_HEIGHT = 360
PEG_WIDTH = 40
DISKS = 8
DISK_WIDTHS = list(range(360, 40, -40))
DISK_HEIGHT = 40
BACKGROUND_LOCATION = '/home/jake/.config/i3/desktop.png'
COUNTER_LOCATION = '/home/jake/.config/i3/counter'
if __name__ == '__main__':
with open(COUNTER_LOCATION, 'r+') as f:
counter = int(f.readline())
state = interpret_state(DISKS, counter)
surface = draw_hanoi(WIDTH, HEIGHT, state, DISK_WIDTHS, PEG_WIDTH,
PEG_HEIGHT, DISK_HEIGHT, BACKGROUND_COLOUR,
PEG_COLOUR, DISK_COLOUR)
surface.write_to_png(BACKGROUND_LOCATION)
f.seek(0)
f.write(str(counter + 1))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment