1

I'm making a matching game where there are several cards faced upside down and the user has to match the right pairs. The cards faced upside down are all turtle objects.

For eg. if there are 8 faced down cards, there are 8 turtle objects.

I'm having some trouble figuring out how to select the cards since I don't know which turtle is associated with the particular card selected by the user. I do have a nested list containing all turtles and those with similar images are grouped together. Is there any way to return the turtle object selected by the user?

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Does this answer your question? [How to see if a mouse click is on a turtle in python](https://stackoverflow.com/questions/57348137/how-to-see-if-a-mouse-click-is-on-a-turtle-in-python) – ggorlen Sep 22 '22 at 18:29

2 Answers2

1

What you can do is define a list, turned to store cards that are turned over.

Here is an example of a Card class:

class Card(turtle.Turtle):
    def __init__(self, number):
        super(Card, self).__init__()
        self.number = number
    def click(self, x, y):
        if self in turned:
            self.clear()
            turned.remove(self)
        else:
            self.sety(self.ycor() - self.shapesize()[1] * 7)
            self.write(self.number, align='center', font=('Arial', 20, 'bold'))
            self.sety(self.ycor() + self.shapesize()[1] * 7)
            turned.append(self)

The super(Card, self).__init__() will give the Card class all the attributes a turtle.Turtle class has. Use self.number = number to add a class variable to the Card class.

In the click function:

        if self in turned:
            self.clear()
            turned.remove(self)

That will allow to user to unselect a card that is selected by removing it from the turned list, and clearing the text, and

        else:
            self.sety(self.ycor() - self.shapesize()[1] * 7)
            self.write(self.number, align='center', font=('Arial', 20, 'bold'))
            self.sety(self.ycor() + self.shapesize()[1] * 7)
            turned.append(self)

will write the text and append the card into the turned list.

I also defined a Deck class that will use the Card class to create a whole grid of cards:

class Deck:
    def __init__(self, rows, cols, width, height, x, y):
        self.cards = []
        for i in range(cols):
            for j in range(rows):
                card = Card(randint(2, 10))
                card.shape("square")
                card.color('black', 'white')
                card.shapesize(height / 20, width / 20)
                card.goto(i * width + x, j * height + y)
                card.onclick(card.click)
                self.cards.append(card)

Example:

import turtle
from random import randint

wn = turtle.Screen()
wn.tracer(0)

turned = []

class Card(turtle.Turtle):
    def __init__(self, number):
        super(Card, self).__init__()
        self.number = number
    def click(self, x, y):
        if self in turned:
            self.clear()
            turned.remove(self)
        else:
            self.sety(self.ycor() - self.shapesize()[1] * 7)
            self.write(self.number, align='center', font=('Arial', 20, 'bold'))
            self.sety(self.ycor() + self.shapesize()[1] * 7)
            turned.append(self)
        print([card.number for card in turned]) # print to display the clicked cards

class Deck:
    def __init__(self, rows, cols, width, height, x, y):
        self.cards = []
        for i in range(cols):
            for j in range(rows):
                card = Card(randint(2, 10))
                card.shape("square")
                card.color('black', 'white')
                card.shapesize(height / 20, width / 20)
                card.goto(i * width + x, j * height + y)
                card.onclick(card.click)
                self.cards.append(card)

deck = Deck(8, 8, 45, 62.5, -165, -210)

wn.update()
wn.mainloop()

Output:

enter image description here


The above code may seem simpler, but it used one global variable, turned, and the numbers will only display for half a second with tracer left on. And clicking directly on the displayed number will not change the status of the cards.

The below code corrects those flaws:

What you can do is define a class to be each card, and a class to be the deck of cards. In the deck class, define a class variable list, turned to store cards that are turned over.

Here is an example of a Card class:

class Card(turtle.Turtle):
    def __init__(self, number):
        super(Card, self).__init__()
        self.number = number
        self.penup()

    def write_number(self, pen):
        pen.goto(self.xcor(), self.ycor() - self.shapesize()[1] * 7)
        pen.write(self.number, align='center', font=('Arial', 20, 'bold'))

    def clicked(self, x, y):
        h, w = self.shapesize()[:-1]
        half_width = w * 10
        half_height = h * 10
        return self.xcor() + half_width > x > self.xcor() - half_width and \
               self.ycor() + half_height > y > self.ycor() - half_height

The super(Card, self).__init__() will give the Card class all the attributes a turtle.Turtle class has. Use self.number = number to add a class variable to the Card class.

The write_number function will, as you can probably guess, display a number on the current turtle object using a separate turtle object so that the number will be display on the center of the card.

The clicked function will basically take in two coordinates, and detect whether the coordinates are on the current turtle object. The reason I defined a clicked function instead of using the built-in turtle.onclick method, is because I want it to return a Boolean value rather than to execute a function.

The Deck class that will use the Card class to create a whole grid of cards.:

class Deck:
    def __init__(self, rows, cols, width, height, x, y):
        self.pen = turtle.Turtle(visible=False)
        self.pen.penup()
        self.pen.speed(0)
        self.cards = []
        self.numbers = []
        self.turned = []
        for i in range(cols):
            for j in range(rows):
                card = Card(randint(2, 10))
                card.shape("square")
                card.color('black', 'white')
                card.shapesize(height / 20, width / 20)
                card.goto(i * width + x, -j * height - y)
                self.cards.append(card)
                
    def click(self, x, y):
        for card in self.cards:
            if card.clicked(x, y):
                if card in self.turned:
                    card.clear()
                    self.turned.remove(card)
                else:
                    self.turned.append(card)
        self.draw()
        print([card.number for card in self.turned])
        
    def draw(self):
        for card in self.turned:
            card.write_number(self.pen)

The self.cards list store all the Card objects, and the self.turned stores the cards that are turned over.

The click function will use the clicked function from the Card class to detect clicks on all the Card objects inside the Deck class variable, cards.

Finally, the draw function will display the number on all the cards in the turned list, using the self.pen defined in the Deck.

Full working code:

import turtle
from random import randint

wn = turtle.Screen()

class Card(turtle.Turtle):
    def __init__(self, number):
        super(Card, self).__init__()
        self.number = number
        self.penup()

    def write_number(self, pen):
        pen.goto(self.xcor(), self.ycor() - self.shapesize()[1] * 7)
        pen.write(self.number, align='center', font=('Arial', 20, 'bold'))

    def clicked(self, x, y):
        h, w = self.shapesize()[:-1]
        half_width = w * 10
        half_height = h * 10
        return self.xcor() + half_width > x > self.xcor() - half_width and \
               self.ycor() + half_height > y > self.ycor() - half_height
    
class Deck:
    def __init__(self, rows, cols, width, height, x, y):
        self.pen = turtle.Turtle(visible=False)
        self.pen.penup()
        self.pen.speed(0)
        self.cards = []
        self.numbers = []
        self.turned = []
        for i in range(cols):
            for j in range(rows):
                card = Card(randint(2, 10))
                card.shape("square")
                card.color('black', 'white')
                card.shapesize(height / 20, width / 20)
                card.goto(i * width + x, -j * height - y)
                self.cards.append(card)
                
    def click(self, x, y):
        for card in self.cards:
            if card.clicked(x, y):
                if card in self.turned:
                    card.clear()
                    self.turned.remove(card)
                else:
                    self.turned.append(card)
        self.draw()
        print([card.number for card in self.turned])
        
    def draw(self):
        for card in self.turned:
            card.write_number(self.pen)

deck = Deck(8, 8, 45, 62.5, -165, -210)

wn.onscreenclick(deck.click)
wn.mainloop()
Red
  • 26,798
  • 7
  • 36
  • 58
  • You've *nicely* encapsulated the cards and the deck as objects, except for that global `turned` list. It could instead be a boolean property of each card. As far as the `print` statement that lists all the turned cards, that seems like debugging code that shouldn't dictate the objects' implementation and is better handled in the `Deck` class which has knowledge of all card, and can interrogate their `turned` status. – cdlane Nov 29 '20 at 22:06
  • @cdlane Thanks. Should the `click` function from the `Card` class return something so that the `Deck` class can access it? – Red Nov 29 '20 at 22:22
  • I can't say, since `click()` is an event handler, it really can't return anything. I believe it depends on the bigger picture of where this application is going as to how information gets passed around. Perhaps you pass `Card` a function at instance creation that will be called when a card changes state. Then the card's `onclick()` might invoke that. – cdlane Nov 30 '20 at 00:56
  • There's a glitch in your code. You turn `tracer()` off, so we expect `update()` at the end of `click()` as it changes the screen. But it isn't there, as you get the effect you want, and a call to `update()` would upset that. I'm positing you're leveraging a glitch in turtle, but, if implemented properly, your code doesn't work. You can find discussion of [the difficulty of implementing numbered blocks in turtle](https://stackoverflow.com/a/63417926/5771269) (Needs fixing!) With tracing on, you'll see the problem--`tracer(0)` makes visuals fast and clean but they should *work* without it. – cdlane Nov 30 '20 at 01:17
0

If i got your question, one way to do so is that you should provide some id attribute to each turtle which will identify it. Then you can check easily which turtle was selected by the user.

Irfan wani
  • 4,084
  • 2
  • 19
  • 34