Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Turtle Write Value in Containing Box

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:

enter image description here

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()
like image 915
Robin Andrews Avatar asked Aug 02 '20 10:08

Robin Andrews


People also ask

How do you draw a filled square in Python turtle?

You can do so using the begin_fill() and end_fill() functions. These two functions are used to enclose a set of Python Turtle commands that will draw a filled shape using the current fill color. So if the current pen color is blue, then any shape you draw will be filled with the color blue.

What does Penup () do in turtle?

penup or pu means pick pen up, so you can move turtle without leaving tracks. pendown or pd means pick pen down, so you can move the turtle and leave tracks. hideturtle or ht means hide the turtle, so you can admire your drawing.

How do you write text in turtle graphics in Python?

write() This function is used to write text at the current turtle position.

What is SETY in turtle Python?

sety() This method is used to set the turtle's second coordinate to y, leaving the first coordinate unchanged. Here, whatever the position of the turtle is, it set the y coordinate to given input keeping the x coordinate unchanged.


1 Answers

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:

  • Dump characters (glyphs) from TrueType font (TTF) into bitmaps

  • How do you set a turtle's shape to a PIL image

And while I'm at it, this answer has been updated to be more system independent:

  • How to know the pixel size of a specific text on turtle graphics in python?

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.

enter image description here

like image 153
cdlane Avatar answered Sep 29 '22 22:09

cdlane