3
\$\begingroup\$

Here is a functional "card-drafting engine" which runs through pygame. It simulates fliping cards face up on the table, and the ability for two players to draft through the cards.There are two questions I have, and am very open to advice.

import pygame
import random
import pandas as pd

"""Layout"""
DISPLAY_HEI = 700
DISPLAY_WID = 1400

CARD_WIDTH = 100
CARD_HEIGHT = 150

TOP_Y = 100
MID_Y = DISPLAY_HEI / 2 - CARD_HEIGHT / 2
BOT_Y = DISPLAY_HEI - CARD_HEIGHT - TOP_Y

CENTER_X = DISPLAY_WID / 2

CENTER_PH_X = CENTER_X - CARD_WIDTH / 2
LEFT_PH_1_X = CENTER_PH_X - (CARD_WIDTH * 3)
LEFT_PH_2_X = CENTER_PH_X - (CARD_WIDTH * 1.5)
RIGHT_PH_1_X = CENTER_PH_X + (CARD_WIDTH * 3)
RIGHT_PH_2_X = CENTER_PH_X + (CARD_WIDTH * 1.5)

PH_BOARDER = 10

DRAFTZONE_W = CARD_WIDTH * 1.5
DRAFTZONE_H = DISPLAY_HEI

P1_ZONE_X = 0
P1_ZONE_Y = 0
P2_ZONE_X = DISPLAY_WID - DRAFTZONE_W
P2_ZONE_Y = 0

FPS = 60

W = (166, 166, 166)
R = (255, 10, 10)
B = (64, 64, 64)
G = (45, 171, 7)
U = (7, 45, 171)
M = (222, 222, 7)
A = (140, 109, 45)
L = (125, 45, 125)

DARK_BRN = (71, 53, 34)
LIGHT_BRN = (181, 121, 58)

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREY = (50, 50, 50)

Here we create the card list from which we are drafting.

xlsx_file = 'cube2019-12-19.xlsx'

WHITE_DF = pd.read_excel(xlsx_file, 'White')
RED_DF = pd.read_excel(xlsx_file, 'Red')
BLACK_DF = pd.read_excel(xlsx_file, 'Black')
GREEN_DF = pd.read_excel(xlsx_file, 'Green')
BLUE_DF = pd.read_excel(xlsx_file, 'Blue')
MULTI_DF = pd.read_excel(xlsx_file, 'Multi')
ART_DF = pd.read_excel(xlsx_file, 'Artifact')
LAND_DF = pd.read_excel(xlsx_file, 'Land')

ALL_DF = [WHITE_DF, RED_DF, BLACK_DF, GREEN_DF, BLUE_DF, MULTI_DF, ART_DF, LAND_DF]

And we begin OOP... For DRY, should I not have multiple classes all with 'render' and 'is_over' functions? Is there a neater way to write this?

"""Placeholders will show up as background figures and will have the cards 'snap into place' when they are dragged onto them."""

class Placeholder(object):
    def __init__(self, x, y, col, width=CARD_WIDTH + PH_BOARDER, height=CARD_HEIGHT + PH_BOARDER):
        self.x = x
        self.y = y
        self.col = col
        self.width = width
        self.height = height

    def render(self, screen):
        pygame.draw.rect(screen, self.col, (self.x, self.y, self.width, self.height))

    def is_over(self, pos):
        if pos[0] > self.x and pos[0] < self.x + self.width:
            if pos[1] > self.y and pos[1] < self.y + self.height:
                return True
        return False


P1 = Placeholder(LEFT_PH_1_X, TOP_Y, BLACK)
P2 = Placeholder(LEFT_PH_2_X, TOP_Y, BLACK)
P3 = Placeholder(CENTER_PH_X, TOP_Y, BLACK)
P4 = Placeholder(RIGHT_PH_1_X, TOP_Y, BLACK)
P5 = Placeholder(RIGHT_PH_2_X, TOP_Y, BLACK)

P6 = Placeholder(LEFT_PH_1_X, MID_Y, BLACK)
P7 = Placeholder(LEFT_PH_2_X, MID_Y, BLACK)
P8 = Placeholder(CENTER_PH_X, MID_Y, BLACK)
P9 = Placeholder(RIGHT_PH_1_X, MID_Y, BLACK)
P10 = Placeholder(RIGHT_PH_2_X, MID_Y, BLACK)

P11 = Placeholder(LEFT_PH_1_X, BOT_Y, BLACK)
P12 = Placeholder(LEFT_PH_2_X, BOT_Y, BLACK)
P13 = Placeholder(CENTER_PH_X, BOT_Y, BLACK)
P14 = Placeholder(RIGHT_PH_1_X, BOT_Y, BLACK)
P15 = Placeholder(RIGHT_PH_2_X, BOT_Y, BLACK)

ALL_PH = [P1, P2, P3, P4, P5,
          P6, P7, P8, P9, P10,
          P11, P12, P13, P14, P15]


class Card(object):
    def __init__(self, name, cmc, tpe, col, x=pos[0], y=pos[1], width=CARD_WIDTH, height=CARD_HEIGHT):
        self.name = name
        self.cmc = cmc
        self.tpe = tpe
        self.x = x
        self.y = y
        self.width = width
        self.height = height

        if col == 'W':
            self.col = W
        if col == 'R':
            self.col = R
        if col == 'B':
            self.col = B
        if col == 'U':
            self.col = U
        if col == 'G':
            self.col = G
        if col == 'MULTI':
            self.col = M
        if col == 'A':
            self.col = A
        if col == 'L':
            self.col = L

    def render(self, screen):
        pygame.draw.rect(screen, self.col, (self.x, self.y, self.width, self.height))

        font = pygame.font.SysFont('ariel', 18)
        text = font.render(self.name, 1, WHITE)
        screen.blit(text,
                    (self.x + (self.width / 2 - text.get_width() / 2),
                     self.y + (self.height / 2 - text.get_height() / 2)))

    def is_over(self, pos):
        if pos[0] > self.x and pos[0] < self.x + self.width:
            if pos[1] > self.y and pos[1] < self.y + self.height:
                return True
        return False

"""Technical name of the deck of cards that are being drafted is 'cube'."""
class Cube(object):
    def __init__(self):
        self.cards = []
        self.build()
        self.shuffle()

    def build(self):
        for df in ALL_DF:
            for ind, row in df.iterrows():
                self.cards.append(
                    Card(name=row['Name'], cmc=row['CMC'],
                         tpe=row['Type'], col=row['Color'])
                )

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

    def top_card(self):
        return self.cards.pop()


class DraftZone(object):
    def __init__(self, x, y, col=LIGHT_BRN, width=DRAFTZONE_W, height=DRAFTZONE_H):
        self.col = col
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def render(self, screen):
        pygame.draw.rect(screen, self.col, (self.x, self.y, self.width, self.height))

        font = pygame.font.SysFont('ariel', 18)
        text = font.render('DraftZone', 1, WHITE)
        screen.blit(text,
                    (self.x + (self.width / 2 - text.get_width() / 2),
                     self.y + (self.height / 2 - text.get_height() / 2)))

    def is_over(self, pos):
        if pos[0] > self.x and pos[0] < self.x + self.width:
            if pos[1] > self.y and pos[1] < self.y + self.height:
                return True
        return False

Here's where I got confused. I wanted to obviously have a single class Player, but ran into trouble. I would create player1 = Player... and player2 = Player... BUT whenever either player 'drafted' a card, both players' rosters would be updated (because I assume, the class "Player" had a single 'roster list' which was being appended by the 'draft' function. I don't know how to do this, and would love some suggestions.

class Player1(object):
    def __init__(self, d_zone_x, d_zone_y, name):
        self.d_zone_x = d_zone_x
        self.d_zone_y = d_zone_y
        self.draft_zone = DraftZone(self.d_zone_x, self.d_zone_y)
        self.name = name

    roster = []

    def draft(self, card):
        pick_num = len(self.roster) + 1
        self.roster.append((pick_num, card.name))


class Player2(object):
    def __init__(self, d_zone_x, d_zone_y, name):
        self.d_zone_x = d_zone_x
        self.d_zone_y = d_zone_y
        self.draft_zone = DraftZone(self.d_zone_x, self.d_zone_y)
        self.name = name

    roster = []

    def draft(self, card):
        pick_num = len(self.roster) + 1
        self.roster.append((pick_num, card.name))


PLAYER1 = Player1(P1_ZONE_X, P1_ZONE_Y, 'Player 1')
PLAYER2 = Player2(P2_ZONE_X, P2_ZONE_Y, 'Player 2')
PLAYERS = [PLAYER1, PLAYER2]


def print_rosters():
    for player in PLAYERS:
        print(f'\n{player.name}:')
        for each in player.roster:
            print(each)

And the main, of course.

def main():
    pygame.init()
    cube = Cube()

    screen = pygame.display.set_mode((DISPLAY_WID, DISPLAY_HEI))

    running = True

    render_list = []
    mouse_pressed = False  # pressed down THIS frame
    mouse_down = False  # mouse held down
    mouse_released = False  # released THIS frame

    right_clicked = False  # to delete

    target = None  # target of drag / drop or delete

    while running:
        screen.fill(DARK_BRN)
        PLAYER1.draft_zone.render(screen)
        PLAYER2.draft_zone.render(screen)
        for ph in ALL_PH:
            ph.render(screen)

        pos = pygame.mouse.get_pos()

        for event in pygame.event.get():
            # print(event)
            if event.type == pygame.QUIT:
                print_rosters()                           # Print rosters on exit.
                pygame.quit()
                quit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    mouse_pressed = True
                    mouse_down = True
                elif event.button == 3:
                    right_clicked = True
            if event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    mouse_released = True
                    mouse_down = False

        """Handling 'drag and drop'."""
        if mouse_pressed:
            for item in render_list:
                if item.is_over(pos):
                    target = item

            if target is None:                          # create new card if you're not dragging one
                target = cube.top_card()
                render_list.append(target)

        if mouse_down and target is not None:
            target.x = pos[0] - CARD_WIDTH / 2          # grabs the center of the card
            target.y = pos[1] - CARD_HEIGHT / 2

        if mouse_released:
            for ph in ALL_PH:                           # snap-to-placeholders
                if ph.is_over(pos):
                    target.x = ph.x + PH_BOARDER / 2
                    target.y = ph.y + PH_BOARDER / 2

            for player in PLAYERS:                      # drafting cards
                if player.draft_zone.is_over(pos):
                    player.draft(target)
                    render_list.remove(target)
            target = None

        if right_clicked:                               # right-click to delete
            for item in render_list:
                if item.is_over(pos):
                    render_list.remove(item)

        for item in render_list:
            item.render(screen)

        mouse_pressed = False
        mouse_released = False
        right_clicked = False
        # mid_clicked = False
        pygame.display.flip()


if __name__ == '__main__':
    main()
\$\endgroup\$
3
\$\begingroup\$

(I'm on my phone on a road trip, so I can't do anything fancy here)

Your issues with roster are because you have it as a attribute of the class, not an instance attribute. That means that every instance shares the same roster. You need to define it in the __init__ (or elsewhere, but ideally in the initializer) as self.roster, just like you did with the other instance attributes.

class Player:
    def __init__(self, d_zone_x, d_zone_y, name):
        self.d_zone_x = d_zone_x
        self.d_zone_y = d_zone_y
        self.draft_zone = DraftZone(self.d_zone_x, self.d_zone_y)
        self.name = name
        self.roster = []  # Here

    def draft(self, card):
        pick_num = len(self.roster) + 1
        self.roster.append((pick_num, card.name))

When referring to it previously, self.roster worked despite it not being an instance attribute because you can refer to class attributes using an instance. Also note, you don't need (object) in Python 3. That was important in 2, but Python 3 uses "new style" classes by default.

With that change, now you can write:

PLAYER1 = Player(P1_ZONE_X, P1_ZONE_Y, 'Player 1')
PLAYER2 = Player(P2_ZONE_X, P2_ZONE_Y, 'Player 2')

PLAYERS = [PLAYER1, PLAYER2]

I think your use of UPPERCASE names for some variables like PLAYER1 and PLAYER2 are inappropriate. In my mind, constants (which uppercase signifies) are variables that always hold the same value. Now, you never reassign PLAYER1, but, since it and PLAYER2 are mutable, they don't maintain the same value throughout the program.

Yes, it could be argued that the "value" being referred to as constant is the reference to the object, not the value of the object itself.

I'm going to suggest though only treating objects as constants if the value of the object itself, as well as the reference to it remain constant. I'd like to hear though if anyone disagrees with me on this point.


I think the if col == checks in Card could be neatened up using a dictionary. Something like:

COL_NAME_TO_VALUE = \
    {"W": W, 
     "R": R
     . . . }

. . . 

self.col = COL_NAME_TO_VALUE[col]

That gets rid of all the duplicate if col == . . .: self.col =. . . bits which would complicate refactoring later.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Yes, thank you. All good advice. \$\endgroup\$ – Claude Fried Dec 25 '19 at 20:50
2
\$\begingroup\$

In addition to what @Carcigenicate said:

Spell out color instead of col - I thought it was short for column.

pygame has a Rect class. Use it instead of separate x,y,width,height attributes. The Rect class already implements is_over-functionality. If desired, some common code can be pulled out into a parent/base class.

Make text bliting into a standalone function.

Example code (untested) to give you an idea:

PH_WIDTH = CARD_WIDTH + PH_BORDER
PH_HEIGHT = CARD_HEIGHT + PH_BORDER

ARIEL18 = pygame.font.SysFont('ariel', 18)

def blittext(text, point, font, color):
    text = font.render(text, True, color)
    screen.blit(text,
                (point.x - text.get_width() / 2),
                (point.y - text.get_height() / 2))

class GameObject:
    def __init__(self, color, rect):
        self.color = color
        self.rect = rect

    def is_over(self, point):
        return self.rect.collidepoint(point)


class Placeholder(GameObject):
    def __init__(self, color, x, y):
        super().__init__(color, Rect(x, y, PH_WIDTH, PH_HEIGHT))

    def render(self, screen):
        pygame.draw.rect(screen, self.col, self.rect)


class Card(GameObject):
    def __init__(self, color, x, y, name, cmc, tpe):
        super().__init__(self, color, Rect(x, y, CARD_WIDTH, CARD_HEIGHT))
        self.name = name
        self.cmc = cmc
        self.tpe = tpe


    def render(self, screen):
        pygame.draw.rect(screen, self.color, self.rect)
        blittext(self.name, point, ARIAL18, WHITE)


class DraftZone(GameObject):
    def __init__(self, color, x, y):
        self.color = color
        super().__init__(color, Rect(x, y, DRAFTZONE_W, DRAFTZONE_H)

    def render(self, screen):
        pygame.draw.rect(screen, self.color, self.rect)
        blittext('DraftZone', point, ARIAL18, WHITE)
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ @Graipher - Yes, fixed. \$\endgroup\$ – RootTwo Dec 24 '19 at 17:19
  • \$\begingroup\$ Appreciated. Didn't know about collidepoint... \$\endgroup\$ – Claude Fried Dec 25 '19 at 20:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.