Reputation: 3804
I want to be able to create some turtles which display values by subclassing turtle.Turtle
.
These turtles should display their value as text centered in their own shape. I also want to be able to position the turtles with accuracy, so setting/determining their width and height relative to a given font size is important.
This is my attempt so far:
I think this answer is relevant: How to know the pixel size of a specific text on turtle graphics in python? but it is quite old, and the bounding box it draws around the text is not in the correct position using python 3.8.
import turtle
FONT_SIZE = 32
class Tile(turtle.Turtle):
def __init__(self):
super().__init__(shape="square")
self.penup()
def show_value(self, val):
self.write(val, font=("Arial", FONT_SIZE, "bold"), align="center")
screen = turtle.Screen()
vals = [5, 7, 8, 2]
for i in range(len(vals)):
tile = Tile()
tile_size = (FONT_SIZE / 20)
tile.shapesize(tile_size)
tile.fillcolor("red" if i % 2 == 0 else "blue")
tile.setx(i * FONT_SIZE)
tile.show_value(vals[i])
turtle.done()
Upvotes: 6
Views: 2918
Reputation: 41905
It would be very helpful to have Turtle Objects containing text such as integer values, which can be used to display a variety of puzzles and games, and can have their own click handlers attached.
Here's the rub, and the (two) reason(s) that approaches using stamp() as suggested in other answers won't work. First, you can't click on a hidden turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
# comment out the following line if you want `onlick()` to work
yertle.hideturtle()
yertle.shape('square')
yertle.stamp()
yertle.onclick(doit)
done()
Stamps are not clickable entities. Second, you can't even click on a turtle that's behind ink left by this, or another, turtle:
from turtle import *
def doit(x, y):
print("Just do it!")
yertle = Turtle()
yertle.shape('square')
yertle.fillcolor('white')
yertle.onclick(doit)
myrtle = Turtle()
myrtle.shape('turtle')
myrtle.penup()
myrtle.sety(-16)
# comment out the following line if you want `onlick()` to work
myrtle.write('X', align='center', font=('Courier', 32, 'bold'))
myrtle.goto(100, 100) # move myrtle out of the way of clicking
done()
If you click on the letter 'X', nothing happens unless you manage to hit a portion of the square just beyond the letter. My belief is that although we think of the 'X' as dead ink over our live turtle, at the tkinter level they are both similar, possibly both capable of receiving events, so one obscures the click on the other.
So how can we do this? The approach I'm going to use is make a tile a turtle with an image where the images are generate by writing onto bitmaps:
tileset.py
from turtle import Screen, Turtle, Shape
from PIL import Image, ImageDraw, ImageFont, ImageTk
DEFAULT_FONT_FILE = "/Library/Fonts/Courier New Bold.ttf" # adjust for your system
DEFAULT_POINT_SIZE = 32
DEFAULT_OUTLINE_SIZE = 1
DEFAULT_OUTLINE_COLOR = 'black'
DEFAULT_BACKGROUND_COLOR = 'white'
class Tile(Turtle):
def __init__(self, shape, size):
super().__init__(shape)
self.penup()
self.size = size
def tile_size(self):
return self.size
class TileSet():
def __init__(self, font_file=DEFAULT_FONT_FILE, point_size=DEFAULT_POINT_SIZE, background_color=DEFAULT_BACKGROUND_COLOR, outline_size=DEFAULT_OUTLINE_SIZE, outline_color=DEFAULT_OUTLINE_COLOR):
self.font = ImageFont.truetype(font_file, point_size)
self.image = Image.new("RGB", (point_size, point_size))
self.draw = ImageDraw.Draw(self.image)
self.background_color = background_color
self.outline_size = outline_size
self.outline_color = outline_color
def register_image(self, string):
width, height = self.draw.textsize(string, font=self.font)
image = Image.new("RGB", (width + self.outline_size*2, height + self.outline_size*2), self.background_color)
draw = ImageDraw.Draw(image)
tile_size = (width + self.outline_size, height + self.outline_size)
draw.rectangle([(0, 0), tile_size], outline=self.outline_color)
draw.text((0, 0), string, font=self.font, fill="#000000")
photo_image = ImageTk.PhotoImage(image)
shape = Shape("image", photo_image)
Screen()._shapes[string] = shape # underpinning, not published API
return tile_size
def make_tile(self, string):
tile_size = self.register_image(string)
return Tile(string, tile_size)
Other than its image, the only differences a Tile instance has from a Turtle instance is an extra method tile_size()
to return its width and height as generic turtles can't do this in the case of images. And a tile's pen is up at the start, instead of down.
I've drawn on a couple of SO questions and answers:
And while I'm at it, this answer has been updated to be more system independent:
To demonstrate how my tile sets work, here's the well-know 15 puzzle implemented using them. It creates two tile sets, one with white backgrounds and one with red (pink) backgrounds:
from tileset import TileSet
from turtle import Screen
from functools import partial
from random import shuffle
SIZE = 4
OFFSETS = [(-1, 0), (0, -1), (1, 0), (0, 1)]
def slide(tile, row, col, x, y):
tile.onclick(None) # disable handler inside handler
for dy, dx in OFFSETS:
try:
if row + dy >= 0 <= col + dx and matrix[row + dy][col + dx] == None:
matrix[row][col] = None
row, col = row + dy, col + dx
matrix[row][col] = tile
width, height = tile.tile_size()
x, y = tile.position()
tile.setposition(x + dx * width, y - dy * height)
break
except IndexError:
pass
tile.onclick(partial(slide, tile, row, col))
screen = Screen()
matrix = [[None for _ in range(SIZE)] for _ in range(SIZE)]
white_tiles = TileSet(background_color='white')
red_tiles = TileSet(background_color='pink')
tiles = []
parity = True
for number in range(1, SIZE * SIZE):
string = str(number).rjust(2)
tiles.append(white_tiles.make_tile(string) if parity else red_tiles.make_tile(string))
parity = not parity
if number % SIZE == 0:
parity = not parity
shuffle(tiles)
width, height = tiles[0].tile_size()
offset_width, offset_height = width * 1.5, height * 1.5
for row in range(SIZE):
for col in range(SIZE):
if row == SIZE - 1 == col:
break
tile = tiles.pop(0)
width, height = tile.tile_size()
tile.goto(col * width - offset_width, offset_height - row * height)
tile.onclick(partial(slide, tile, row, col))
matrix[row][col] = tile
screen.mainloop()
If you click on a number tile that's next to the blank space, it will move into the blank space, otherwise nothing happens. This code doesn't guarantee a solvable puzzle -- half won't be solvable due to the random shuffle. It's just a demonstration, the fine details of it, and the tiles themselves, are left to you.
Upvotes: 5