Forum Archive

Solitaire

borrax

Here is a simple solitaire game I wrote. It uses the Unicode playing card characters, so it wonโ€™t work on older devices that canโ€™t display them.

from scene import *
from random import *


# a class to define the basic card behavior
class Card(Node):
    # array of card icons. order is spades, clubs, hearts, diamonds
    card_icons = ['๐Ÿ‚ก', '๐Ÿ‚ข', '๐Ÿ‚ฃ', '๐Ÿ‚ค', '๐Ÿ‚ฅ', '๐Ÿ‚ฆ', '๐Ÿ‚ง', '๐Ÿ‚จ', '๐Ÿ‚ฉ', '๐Ÿ‚ช', '๐Ÿ‚ซ', '๐Ÿ‚ญ', '๐Ÿ‚ฎ', '๐Ÿƒ‘', '๐Ÿƒ’', '๐Ÿƒ“', '๐Ÿƒ”', '๐Ÿƒ•', '๐Ÿƒ–', '๐Ÿƒ—', '๐Ÿƒ˜', '๐Ÿƒ™', '๐Ÿƒš', '๐Ÿƒ›', '๐Ÿƒ', '๐Ÿƒž', '๐Ÿ‚ฑ', '๐Ÿ‚ฒ', '๐Ÿ‚ณ', '๐Ÿ‚ด', '๐Ÿ‚ต', '๐Ÿ‚ถ', '๐Ÿ‚ท', '๐Ÿ‚ธ', '๐Ÿ‚น', '๐Ÿ‚บ', '๐Ÿ‚ป', '๐Ÿ‚ฝ', '๐Ÿ‚พ', '๐Ÿƒ', '๐Ÿƒ‚', '๐Ÿƒƒ', '๐Ÿƒ„', '๐Ÿƒ…', '๐Ÿƒ†', '๐Ÿƒ‡', '๐Ÿƒˆ', '๐Ÿƒ‰', '๐ŸƒŠ', '๐Ÿƒ‹', '๐Ÿƒ', '๐ŸƒŽ']
    card_back = '๐Ÿ‚ '

    def __init__(self, card_number):
        '''init the card object'''
        # number is the 0 - 51 value for the card in the deck
        self.number = card_number
        # value is the 0 - 12 value in the suit
        self.value = self.number % 13
        # suit is the 0 - 3 value for the suit
        # 0 = spades 1 = clubs 2 = hearts 3 = daimonds
        self.suit = self.number // 13
        # set the icon based on the card number
        self.face = self.card_icons[self.number]
        # set the color based on card number. 
        if self.number < 26:
            self.color = 'black'
        else:
            self.color = 'red'

        # bool to determine if face up
        self.face_up = False
        # bool to determine if active
        self.active = False

        # set up the card image with nodes
        # the path defines the rectangular shape and size and line width
        rect_path = ui.Path.rect(0, 0, 75, 100)
        rect_path.line_width = 1
        # the bg_node is a shape node that uses the path to draw the shape
        bg_node = ShapeNode(rect_path, 'white', 'black')
        # the text node draws the unicode playing card on the bg_node
        self.text_node = LabelNode(self.card_back, ('Courier New', 100))
        if self.face_up:
            self.text_node.text = self.face
            self.text_node.color = self.color
        else:
            self.text_node.text = self.card_back
            self.text_node.color = 'blue'
        self.text_node.position = (0, 0)
        # add the nodes as children of the card node
        self.add_child(bg_node)
        bg_node.add_child(self.text_node)

    def point_inside(self, x, y):
        '''Check if the point x, y is within the bounding box'''
        return self.bbox.contains_point((x, y))

    def update(self):
        # Check if the card has been set to face up or not
        # Then set the text node and color accordingly
        if self.face_up:
            self.text_node.text = self.face
            self.text_node.color = self.color
        else:
            self.text_node.text = self.card_back
            self.text_node.color = 'blue'

class Deck():
    # this object will hold all the cards needed for the game
    # all cards will be added to the deck during setup
    # then the deck will be shuffled, and one card will be taken
    # from the deck at a time
    def __init__(self):
        self.cards = []
        for i in range(0, 52):
            self.cards.append(Card(i))

    def shuffle(self):
        shuffle(self.cards)

    def print(self):
        for i in self.cards:
            print(i.face)

    def get_card(self):
        return self.cards.pop(0)

class Pile(Node):
    # this class represents the pile of face down cards in the top
    # right corner. When clicked, it transfers a card to the waste
    def __init__(self):
        self.cards = []

        rect_path = ui.Path.rect(0, 0, 75, 100)
        rect_path.line_width = 1
        self.bg_node = ShapeNode(rect_path, 'white', 'black')
        self.label_node = LabelNode('๐Ÿ‚ ', ('Courier New', 100))
        self.label_node.color = 'blue'
        self.label_node.position = (0, 0)

        # If the pile has cards in it, draw a single face down card
        # if no cards, just draw a green rectangle
        if len(self.cards) == 0:
            self.bg_node.fill_color = 'green'
            self.label_node.text = ' '
        else:
            self.bg_node.fill_color = white
            self.label_node.text = '๐Ÿ‚ '

        self.add_child(self.bg_node)
        self.bg_node.add_child(self.label_node)

    def add_card(self, c):
        self.cards.insert(0, c)

    def get_card(self):
        return self.cards.pop(0)

    def update(self):
        # redraw the pile
        if len(self.cards) == 0:
            self.bg_node.fill_color = 'green'
            self.label_node.text = ' '
        else:
            self.bg_node.fill_color = 'white'
            self.label_node.text = '๐Ÿ‚ '

    def point_inside(self, x, y):
        return self.bbox.contains_point((x, y))

class Waste(Node):
    # this class represents the stack of cards next to the pile
    # when clicked, it transfers a card to the moving stack
    def __init__(self):
        self.cards = []

        rect_path = ui.Path.rect(0, 0, 75, 100)
        rect_path.line_width = 1
        self.bg_node = ShapeNode(rect_path, 'green', 'black')
        self.text_node = LabelNode(' ', ('Courier New', 100))
        self.text_node.color = 'blue'

        self.add_child(self.bg_node)
        self.bg_node.add_child(self.text_node)

    def add_card(self, c):
        self.cards.insert(0, c)

    def get_card(self):
        return self.cards.pop(0)

    def update(self):
        # if no cards in the waste, draw a green rectangle
        # if cards are present, draw the top card face up
        if len(self.cards) == 0:
            self.bg_node.fill_color = 'green'
            self.text_node.text = ' '
        else:
            self.bg_node.fill_color = 'white'
            self.text_node.text = self.cards[0].face
            self.text_node.color = self.cards[0].color

    def point_inside(self, x, y):
        return self.bbox.contains_point((x, y))

class F_stack(Node):
    # this class represents the stacks of cards in the top left where
    # all the cards go to win the game. There will be 4 of them.
    def __init__(self):
        self.cards = []
        self.suit = -1 # start with no suit

        rect_path = ui.Path.rect(0, 0, 75, 100)
        rect_path.line_width = 1
        self.bg_node = ShapeNode(rect_path, 'green', 'black')
        self.text_node = LabelNode(' ', ('Courier New', 100))

        self.add_child(self.bg_node)
        self.bg_node.add_child(self.text_node)

    def add_card(self, c):
        # if the suit has not been set yet, set it to what the card is
        # only accept cards with matching suit
        if self.suit == -1:
            self.cards.insert(0, c)
            self.suit = c.suit
        elif self.suit == c.suit:
            self.cards.insert(0, c)

    def get_card(self):
        return self.cards.pop(0)

    def point_inside(self, x, y):
        return self.bbox.contains_point((x, y))

    def update(self):
        # if the f stack has cards, draw the top card
        # if not, draw a green rectangle
        if len(self.cards) == 0:
            self.bg_node.fill_color = 'green'
            self.text_node.text = ' '
        else:
            self.bg_node.fill_color = 'white'
            self.text_node.text = self.cards[0].face
            self.text_node.color = self.cards[0].color


class T_stack(Node):
    # this class represents the stacks of cards in the middle
    # of the game. Will hold both face up and face down cards.
    # When clicked, will need to determine which card in the 
    # stack was clicked and transfer the appropriate cards to
    # the moving stack
    def __init__(self):
        self.cards = []

        rect_path = ui.Path.rect(0, 0, 75, 100)
        rect_path.line_width = 1
        self.bg_node = ShapeNode(rect_path, 'green', 'black')
        self.add_child(self.bg_node)

    def add_card(self, c):
        # must also add each card as a child node so that it gets drawn.
        self.cards.append(c)
        self.add_child(c)

    def get_card(self):
        # must also remove card from children so that it stops being drawn.
        c = self.cards.pop()
        c.remove_from_parent()
        return c

    def point_inside(self, x, y):
        return self.bbox.contains_point((x, y))

    def get_touched_card(self, y):
        # cards are placed every 25 pixels down from the top of the
        # stack. This function takes the y coord of the touch location
        # and determines how many 25 pixel units fits between it and the
        # top of the bounding box. If the bottom card is touched, we might
        # calculate an out of bounds index, so we return the last valid 
        # index instead
        bbox_top = self.bbox[1] + self.bbox[3]
        i = round((bbox_top - y) // 25)
        return min(i, len(self.cards) - 1)

    def update(self):
        # set the position of each card in the stack
        # cards are placed every 25 pixels down
        for i in range(len(self.cards)):
            self.cards[i].position = (0, -i * 25)
            self.cards[i].z_position = i
            self.cards[i].update()

class Stack(Node):
    # this class represents the cards being moved around the board
    # by the player.
    def __init__(self):
        self.cards = []

    def add_card(self, c):
        # all cards in the stack will be face up.
        # must add them to children so they are drawn
        c.face_up = True
        self.cards.insert(0, c)
        self.add_child(c)

    def get_card(self):
        # must remove from children so they are not drawn
        c = self.cards.pop(0)
        c.remove_from_parent()
        return c

    def update(self):
        # place cards every 25 pixels down from top
        for i in range(len(self.cards)):
            self.cards[i].position = (0, -i * 25)
            self.cards[i].z_position = i
            self.cards[i].update()

class Bounce_card(Node):
    # this class represents the bouncing cards created at the end of a 
    # game. 
    def __init__(self, i, p, s):
        # i = the card to copy
        # p = the position to create the bounce card at
        # s = the size of the screen
        self.face = i.face
        self.color = i.color

        rect_path = ui.Path.rect(0, 0, 75, 100)
        self.bg_node = ShapeNode(rect_path, 'white', 'black')
        self.text_node = LabelNode(self.face, ('Courier New', 100))
        self.text_node.color = self.color
        self.add_child(self.bg_node)
        self.bg_node.add_child(self.text_node)
        self.position = p
        # randomly select speeds in the x and y directions
        self.x_speed = uniform(-5, 5)
        self.y_speed = uniform(-5, 5)
        self.size = s

    def update(self):
        # check if the bounding box is outside the scene
        # is so, reverse the speed so it comes back inside
        if self.bbox[0] < 0:
            self.x_speed *= -1
        if self.bbox[1] < 0:
            self.y_speed *= -1
        if self.bbox[0] + self.bbox[2] > self.size.w:
            self.x_speed *= -1
        if self.bbox[1] + self.bbox[3] > self.size.h:
            self.y_speed *= -1
        # create a move action with our speeds
        move_action = Action.move_by(self.x_speed, self.y_speed, 0, TIMING_LINEAR)
        self.run_action(move_action)


class Reset(Node):
    # this class represents a reset button
    # when clicked, it will begin a new game
    def __init__(self):
        rect_path = ui.Path.rect(0, 0, 100, 75)
        self.bg_node = ShapeNode(rect_path, 'white', 'black')
        self.text_node = LabelNode('RESET', ('Helvetica', 28))
        self.text_node.color = 'black'
        self.bg_node.add_child(self.text_node)
        self.add_child(self.bg_node)

    def point_inside(self, x, y):
        return self.bbox.contains_point((x, y))

class TestScene(Scene):
    def setup(self):
        self.background_color = 'green'

        # set up the deck object that initially holds all the cards
        # will be used to distribute cards to the other objects
        self.deck = Deck()
        self.deck.shuffle()

        # the pile is the stack of face down cards in the top right
        # when clicked, the top card will be transferred to waste
        # when empty, click will transfer all waste cards back to pile
        self.pile = Pile()
        self.pile.position = (self.size.w - 100, self.size.h - 100)
        self.add_child(self.pile)

        # holds face up cards that came from the pile
        # when clicked, will transfer top card to the stack
        self.waste = Waste()
        self.waste.position = (self.size.w - 200, self.size.h - 100)
        self.add_child(self.waste)

        # f stacks are the 4 sets of cards in the top left
        # when all cards are added to the f stacks the game is won
        # clicking an f stack should transfer the top card to the stack
        self.f_stacks = []
        for i in range(4):
            f = F_stack()
            f.position = (100 * (i + 1), self.size.h - 100)
            self.f_stacks.append(f)
            self.add_child(f)



        # t stacks are the 7 sets of cards in the middle
        # when clicking on the t stack, it should transfer all cards
        # below the click point to the stack
        self.t_stacks = []
        for i in range(7):
            t = T_stack()
            t.position = (100 * (i + 1), self.size.h - 250)
            # add a set number of face down cards to each t stack
            for j in range(i):
                t.add_card(self.deck.get_card())
            # add 1 face up card to each t stack
            c = self.deck.get_card()
            c.face_up = True
            t.add_card(c)
            self.t_stacks.append(t)
            self.add_child(t)


        # put remaining deck cards into the pile
        for i in range(len(self.deck.cards)):
            self.pile.add_card(self.deck.get_card())

        # the stack is used to move cards between waste, t stacks, and f stacks
        # the stack should follow the touch when not empty
        # when a touch ends the cards should all be emptied from the stack
        # either to a new location or back to previous location
        self.stack = Stack()
        self.add_child(self.stack)

        self.bouncers = []
        self.win = False

        self.reset = Reset()
        self.reset.position = (self.size.w - 150, 100)
        self.add_child(self.reset)



    def touch_began(self, touch):
        x, y = touch.location
        # check if pile was clicked
        # if pile is not empty, transfer card to waste
        # if pile is empty, get all cards from waste
        if self.pile.point_inside(x, y):
            if len(self.pile.cards) > 0:
                self.waste.add_card(self.pile.get_card())
            else:
                for i in range(0, len(self.waste.cards)):
                    self.pile.add_card(self.waste.get_card())
        # check if waste was clicked
        # if waste is not empty, transfer top card to stack
        # if waste is empty, do nothing?
        if self.waste.point_inside(x, y):
            if len(self.waste.cards) > 0:
                self.stack.card_return = self.waste
                self.stack.add_card(self.waste.get_card())
                move_action = Action.move_to(x, y, 0, TIMING_LINEAR)
                self.stack.run_action(move_action)
        # check if T stacks were clicked
        # if the bottom card is face down, make it face up
        # otherwise, determine which card was touched and add the
        # cards below it to the moving stack 
        for t in self.t_stacks:
            if t.point_inside(x, y):
                # make sure t stack is not empty
                if len(t.cards) > 0:
                    # get the last card in the t stack
                    last_card = t.cards[-1]
                    # check if the last card is face up
                    if last_card.face_up:
                        # set this t stack as the card return so that
                        # the cards from the moving stack get put back
                        # in case the move is cancelled
                        self.stack.card_return = t
                        # get the index of the touched card
                        c = t.get_touched_card(y)
                        # if the touched card is face up, add it and all
                        # cards below it to the moving stack
                        if t.cards[c].face_up:
                            for i in range(c, len(t.cards)):
                                self.stack.add_card(t.cards.pop())
                            # move the stack to the touched location.
                            move_action = Action.move_to(x, y, 0,TIMING_LINEAR)
                            self.stack.run_action(move_action)
                    # last card face down, make it face up.
                    else:
                        c = t.get_touched_card(y)
                        if c == len(t.cards) - 1:
                            last_card.face_up = True


        # check if reset button is pressed.
        # reset game by removing all children from the scene
        # and running the setup function again.
        if self.reset.point_inside(x, y):
            for i in self.children:
                i.remove_from_parent()
            self.setup()

    def touch_moved(self, touch):
        x, y = touch.location
        # move the stack to the touch's current location
        if len(self.stack.cards) > 0:
            move_action = Action.move_to(x, y, 0, TIMING_LINEAR)
            self.stack.run_action(move_action)

    def touch_ended(self, touch):
        x, y = touch.location
        # check f stacks and t stacks.
        # if any are clicked, transfer cards from stack to target
        for f in self.f_stacks:
            if f.point_inside(x, y):
                if len(self.stack.cards) == 1:
                # check if empty and if stack is an ace
                    if len(f.cards) == 0:
                        if self.stack.cards[0].value == 0:
                            self.stack.card_return = f
                    # if not empty check that suit matches and that value is ok
                    else:
                        if f.suit == self.stack.cards[0].suit:
                            if f.cards[0].value == (self.stack.cards[0].value - 1):
                                self.stack.card_return = f

        # if the stack has cards and we end the touch on a t stack,
        # then check if the t stack has cards. If so, make sure that the
        # card we're adding to the t stack has an appropriate value and
        # color
        for t in self.t_stacks:
            if len(self.stack.cards) > 0:
                if t.point_inside(x, y):
                    # check if the t stack is empty
                    if len(t.cards) > 0:
                        # check that first card in stack has the right value
                        stack_first = self.stack.cards[0]
                        t_stack_last = t.cards[len(t.cards) - 1]
                        if t_stack_last.value == (stack_first.value + 1):
                            # check suits
                            if (t_stack_last.suit == 0 or t_stack_last.suit == 1):
                                if (stack_first.suit == 2 or stack_first.suit == 3):
                                    self.stack.card_return = t
                            elif (t_stack_last.suit == 2 or t_stack_last.suit == 3):
                                if (stack_first.suit == 0 or stack_first.suit == 1):
                                    self.stack.card_return = t
                    # is empty, only allow kings
                    else:
                        if self.stack.cards[0].value == 12:
                            self.stack.card_return = t



        # transfer cards from stack to card_return target
        for i in range(len(self.stack.cards)):
            self.stack.card_return.add_card(self.stack.get_card())

    def did_change_size(self):
        self.pile.position = (self.size.w - 100, self.size.h - 100)
        self.waste.position = (self.size.w - 200, self.size.h - 100)
        for i in range(len(self.f_stacks)):
            f = self.f_stacks[i]
            f.position = (100 * (i + 1), self.size.h - 100)
        for i in range(len(self.t_stacks)):
            t = self.t_stacks[i]
            t.position = (100 * (i + 1), self.size.h - 250)
        self.reset.position = (self.size.w - 150, 100)

    def update(self):
        self.pile.update()
        self.waste.update()
        for f in self.f_stacks:
            f.update()
        for t in self.t_stacks:
            t.update()
        self.stack.update()

        # check if won
        if self.win == False:
            # the game is won once the f stacks each have 13 cards
            if len(self.f_stacks[0].cards) == 13 and len(self.f_stacks[1].cards) == 13 and len(self.f_stacks[2].cards) == 13 and len(self.f_stacks[3].cards) == 13:
                # create bouncers at each f stack location
                for i in range(0, 13):
                    for f in self.f_stacks:
                        self.bouncers.append(Bounce_card(f.cards[i], f.position, self.size))
                        f.remove_from_parent()

                for i in self.bouncers:
                    self.add_child(i)

                self.win = True


        for i in self.bouncers:
            i.update()



run(TestScene())
ccc

Cool! I will play some more. Some lines to simplify things:

# A Python string will also work and is smaller and easier to read and understand.
card_icons = '๐Ÿ‚ก๐Ÿ‚ข๐Ÿ‚ฃ๐Ÿ‚ค๐Ÿ‚ฅ๐Ÿ‚ฆ๐Ÿ‚ง๐Ÿ‚จ๐Ÿ‚ฉ๐Ÿ‚ช๐Ÿ‚ซ๐Ÿ‚ญ๐Ÿ‚ฎ๐Ÿƒ‘๐Ÿƒ’๐Ÿƒ“๐Ÿƒ”๐Ÿƒ•๐Ÿƒ–๐Ÿƒ—๐Ÿƒ˜๐Ÿƒ™๐Ÿƒš๐Ÿƒ›๐Ÿƒ๐Ÿƒž๐Ÿ‚ฑ๐Ÿ‚ฒ๐Ÿ‚ณ๐Ÿ‚ด๐Ÿ‚ต๐Ÿ‚ถ๐Ÿ‚ท๐Ÿ‚ธ๐Ÿ‚น๐Ÿ‚บ๐Ÿ‚ป๐Ÿ‚ฝ๐Ÿ‚พ๐Ÿƒ๐Ÿƒ‚๐Ÿƒƒ๐Ÿƒ„๐Ÿƒ…๐Ÿƒ†๐Ÿƒ‡๐Ÿƒˆ๐Ÿƒ‰๐ŸƒŠ๐Ÿƒ‹๐Ÿƒ๐ŸƒŽ'
---
# divmod() is a Python builtin function https://docs.python.org/3/library/functions.html
self.suit, self.value = divmod(card_number)
---
# Python ternary if replaces 4 lines with one and easier to read.
self.color = 'black' if card_number < 26 else 'red'
---
# Python list comprehension replaces 3 lines with one and is faster and easier to read.
self.cards = [Card(i) for i in range(52)]

The class names are very intuitive except for F_stack and T_stack. Could there be more self-documenting names for these two classes so that readers would know what they do without reading the comments?

borrax

@ccc Thanks for the suggestions, I am new to python so I donโ€™t know all the tricks.

The T_stacks and F_stacks are technically called the Tableu and Foundations, but I thought those were equally obsfucating and harder to type, so I didnโ€™t know what else to call them.

ccc

My sense is that Foundation and Tableu would have been more understanable and that they did not need to be typed many times.

Another tip...

                            if (t_stack_last.suit == 0 or t_stack_last.suit == 1):
                                if (stack_first.suit == 2 or stack_first.suit == 3):
                                    self.stack.card_return = t
# -->
                            if t_stack_last.suit in (0, 1) and stack_first.suit in (2, 3):
                                self.stack.card_return = t