7
\$\begingroup\$

I am new to Python but I aspire to become a 'proper' Python coder, (maybe even one day help the noobs on CodeReview :)

Therefore I want to get into good habits early, and would really appreciate some feedback on my first mini-project.

Notes

  1. The code deals cards for a simple board game. If you are interested, this is explained in more detail below under 'Rules'. Full game rules here.
  2. There are no bugs or problems I can detect.
  3. I have made extensive use of tkinter as a means-to-an-end, however I am less interested in tkinter-specific feedback, but rather the fundamentals of Python itself: classes/objects, decorators, scope, code design/efficiency/elegance. Please point out any barn-door design mistakes, stylistic shortfalls, areas for improvement.
  4. I know i need to change some function names in line with PEP.

Rules (What the Code Does)

  • The user selects their team type and team colour from 'launch window'.
  • A 'game window' then appears, showing two decks of shuffled cards, 'R' and 'S', 15 per deck.
  • User can click either deck to draw a hand of 4 cards.
  • User can click on any of the 4 cards to 'play' the card, whereafter the card is discarded from the game, and the 3 remaining cards are set aside in a hidden 'recycle' deck (one for R, one for S).
  • When fewer than 4 cards remain in either of the main 'R' or 'S' decks, the R or S hidden recycled deck is shuffled and its cards are re-added to the bottom of the respective 'R' or 'S' main deck, and can be drawn on demand, as before.
  • Sometimes, a user is compelled to collect an 'exhaustion' card for R or for S, which is placed directly into the respective hidden recycle deck.
  • If the user can not draw 4 cards even after replenishing the R or S deck with its respective hidden recycle deck, the shortfall is made up by taking further 'exhaustion' cards as required.
  • There is no mixing between R and S decks.
  • Local image files of the cards are used as the 'buttons', so they can click on the card they want to choose it.

That's it! Simple stuff. The game itself is not coded here. The reason for the lengthy code is mainly due to the graphical elements.


The Code

Am I using @property appropriately/excessively/unneccessarily below?

################
# GameLogic.py #
################
import random
# Global constants
# (The lists represent the decks, the elements are the numbers shown on each card.
ROULEUR = [3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7]
SPRINTER =[2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 9, 9, 9]
class Deck:
 def __init__(self, team: str, rider: str, teamtype: str):
 self._team = team
 self._rider = rider
 self._cards = []
 self._teamtype = teamtype
 @property
 def show(self):
 i = 0
 total = self.count_cards
 for crd in self.cards:
 i += 1
 print(f'Value of card {i} of {total} in {self.team} {self.rider} deck is {crd.value}')
 if not isinstance(self,MainDeck):
 print('This is not the main deck')
 # Getters
 @property
 def team(self):
 return self._team
 @property
 def rider(self):
 return self._rider
 @property
 def cards(self):
 return self._cards # Returns list of Card objects
 @property
 def teamtype(self):
 return self._teamtype
 # Functions
 def get_card(self, index: int):
 return self.cards[index]
 @property
 def count_cards(self):
 return len(self.cards)
 def pick_cards(self, num: int):
 return self.cards[0:num]
 @property
 def shuffle(self):
 for i in range(self.count_cards -1, -1, -1):
 rnd = random.randint(0, i)
 self.cards[i], self.cards[rnd] = self.cards[rnd], self.cards[i]
 def add(self, newCard: object):
 self.cards.append(newCard)
 def remove_card(self, index: int):
 return self.cards.pop(index)
 @property
 def countExhaustion(self) -> int:
 cnt = 0
 for crd in self.cards:
 if isinstance(crd, ExhaustCard):
 cnt += 1
 return cnt
 def getFilePaths(self) -> str:
 filepaths = []
 for card in self.cards:
 if isinstance(card,ExhaustCard):
 filepaths.append(f'Images/exhaust_{self.rider[0:1].upper()}_{str(card.value)}.jpg')
 else:
 filepaths.append(f'Images/{self.team.lower()}_{self.rider[0:1].upper()}_{str(card.value)}.jpg')
 return filepaths
 # For a given card[index = i], return a tuple with all the remaining original indices of the other cards in that deck. Used for recycling hands of variable size. Reverse order for ease of processing later.
 def getRemainders(self, i: int) -> int:
 indices = list(range(self.count_cards))
 del indices[i]
 return indices[::-1]
class MainDeck(Deck):
 def __init__(self, team: str, rider: str, teamtype: str):
 self._team = team
 self._rider = rider
 self._cards = []
 self._teamtype = teamtype
 self._recycleDeck = Deck(team,rider,teamtype)
 if self.teamtype.lower() == 'player':
 if rider[0:1].upper() == 'R':
 for i in range(len(ROULEUR)):
 self._cards.append(Card(ROULEUR[i]))
 elif rider[0:1].upper() == 'S':
 for i in range(len(SPRINTER)):
 self._cards.append(Card(SPRINTER[i]))
 @property
 def recycleDeck(self):
 return self._recycleDeck
 def recycle(self, index: int):
 self.recycleDeck.cards.append(self.remove_card(index))
 def refresh_deck(self):
 self.recycleDeck.shuffle # Shuffle the recycled cards (not those remaining in main deck)
 for i in range(self.recycleDeck.count_cards -1, -1,-1): # Add the shuffled recycled cards back in to the back of the main deck
 self.cards.append(self.recycleDeck.remove_card(i))
class Card():
 def __init__(self, value):
 self._value = value
 @property
 def value(self):
 return self._value
class ExhaustCard(Card):
 def __init__(self):
 self._value = 2
# TO DO:
# - error handling on init verificaction

By contrast the file below feels bloated and messy. There must be a cleaner way to perform the interactive image initialisation instead of laboriously coding each item individually?

############
# GUI Module
############
import tkinter as tk
from PIL import Image, ImageTk
import GameLogic as GL
from tkinter import messagebox
# Constants
CARD_W = 180
CARD_H = 300
W_MULT = 1.2
TEAMS = ('Blue', 'Red', 'Green', 'Black', 'White', 'Pink')
RIDERS = ('Rouleur', 'Sprinter')
PLAYER = ('Player', 'Bot: Muscle Team', 'Bot: Peloton Team')
HANDSIZE = 4
DECK_PATHS = ('deck_main_R', 'deck_exhaust_R', 'FR_logo', 'deck_main_S', 'deck_exhaust_S')
##################
# LaunchWindow Class
##################
class LaunchWindow:
 def __init__(self, master):
 self.master = master
 # Make the frames
 self.frm_header = tk.Frame(self.master)
 self.frm_options = tk.Frame(self.master)
 self.frm_colour = tk.Frame(master=self.frm_options)
 self.frm_player = tk.Frame(master=self.frm_options)
 self.frm_footer = tk.Frame(master=self.master)
 # (1) Header widgets
 tk.Label(master=self.frm_header, text='Flamme Rouge', anchor=tk.NW, font=('Verdana', 50), fg='red').pack(side=tk.TOP, pady=10, padx=10)
 tk.Label(master=self.frm_header, text='Select team type options', anchor=tk.NW, font=('Verdana', 25)).pack(side=tk.TOP,pady=5)
 # (2) Team Choice Widgets
 # (A) Team Colour Choice Input
 colour_choice = tk.StringVar()
 tk.Label(master= self.frm_colour, text='Team colour:').pack(side=tk.TOP, pady = 3)
 for team in TEAMS:
 textcolour = 'White'
 if team == 'White' or team == 'Pink':
 textcolour = 'Black'
 rb_teams = tk.Radiobutton(master= self.frm_colour, text=team, value=team, variable=colour_choice, bg=team, fg=textcolour)
 rb_teams.pack(anchor=tk.N)
 # (B) Player (Human or Bot) Choice Input
 player_choice = tk.StringVar()
 tk.Label(master=self.frm_player, text='Team Type:').pack(side=tk.TOP, pady=3)
 for p in PLAYER:
 rb_teams = tk.Radiobutton(master=self.frm_player, text=p, value=p, variable=player_choice, indicatoron=0)
 rb_teams.pack(anchor=tk.N)
 # (3) Footer Widgets
 self.btn_start = tk.Button(master=self.frm_footer, text='Start Game', anchor=tk.S,
 relief=tk.RAISED, font=('Verdana', 35),
 command=lambda: self.start_game(colour_choice.get(), player_choice.get()),
 padx=20, pady=10).pack(side=tk.LEFT)
 self.btn_close = tk.Button(master=self.frm_footer, text='Close', anchor=tk.S, padx=20, pady=10,
 relief=tk.RAISED, font=('Verdana', 25), command=self.handle_close).pack(side=tk.LEFT)
 #Pack the frames
 self.frm_header.pack(side=tk.TOP)
 self.frm_colour.pack(side=tk.LEFT)
 self.frm_player.pack(side=tk.LEFT)
 self.frm_options.pack(side=tk.TOP)
 self.frm_footer.pack(side=tk.BOTTOM, pady = 10)
 # Handle events, clicks etc
 def handle_close(self):
 self.master.destroy()
 def start_game(self, colour: str, team_type: str):
 self.master.destroy()
 game_window = tk.Tk()
 game = GameWindow(game_window, colour, team_type)
##################
# GameWindow Class
##################
class GameWindow:
 def __init__(self, master, colour, teamtype): # Create labels, entries,buttons
 self.master = master
 # Make the frames
 self.frm_header = tk.Frame(self.master)
 self.frm_decks = tk.Frame(self.master)
 self.frm_cards = tk.Frame(self.master)
 self.frm_footer = tk.Frame(self.master)
 # Initialise the Card decks according to user input on launch window
 self._deck_R = GL.MainDeck(colour, 'Rouleur', teamtype)
 self._deck_S = GL.MainDeck(colour, 'Sprinter', teamtype)
 self.deck_R.shuffle
 self.deck_S.shuffle
 # (1) Header Widgets
 tk.Label(master=self.frm_header, text=colour+' Team', fg=colour, bg='grey', anchor=tk.NW, font=('Verdana', 35, 'bold')).pack(side=tk.TOP, pady=10)
 # (2) Deck Widgets
 deck_images = []
 deck_btns = []
 # (a) Rouleur deck
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[0]}.jpg').resize((int(CARD_W*W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W*W_MULT), height=CARD_H, image=deck_images[0],
 command=lambda:self.show_hand(self.deck_R)))
 deck_btns[0].image = deck_images[0]
 deck_btns[0].grid(row=2, column=1, padx=20, pady=20)
 # (b) Rouleur Exhaustion
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[1]}.jpg').resize((int(CARD_W*W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W*W_MULT), height=CARD_H, image=deck_images[1],
 command=lambda:self.pickup_exhaust(self.deck_R.recycleDeck)))
 deck_btns[1].image = deck_images[1]
 deck_btns[1].grid(row=2, column=2, padx=20, pady=20)
 # (c) FR Logo
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[2]}.jpg').resize((int(CARD_W*2), CARD_H))))
 deck_btns.append(tk.Label(master=self.frm_decks, width=int(CARD_W*2.2), height=CARD_H, image=deck_images[2]))
 deck_btns[2].image = deck_images[2]
 deck_btns[2].grid(row=2, column=3, padx=20, pady=20)
 # (d) Sprinter Deck
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[3]}.jpg').resize((int(CARD_W * W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W * W_MULT), height=CARD_H, image=deck_images[3],
 command=lambda: self.show_hand(self.deck_S)))
 deck_btns[3].image = deck_images[3]
 deck_btns[3].grid(row=2, column=4, padx=20, pady=20)
 # (e) Sprinter Exhaustion
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[4]}.jpg').resize((int(CARD_W * W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W * W_MULT), height=CARD_H, image=deck_images[4],
 command=lambda: self.pickup_exhaust(self.deck_S.recycleDeck)))
 deck_btns[4].image = deck_images[4]
 deck_btns[4].grid(row=2, column=5, padx=20, pady=20)
 # (f) Rouleur: deck label & card count
 tk.Label(master=self.frm_decks, text='Rouleur Deck', font=('Verdana', 20), anchor=tk.S).grid(row=1,column=1)
 self.vR = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vR, font=('Verdana', 12), anchor=tk.N).grid(row=3,column=1)
 self.vRrec = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vRrec, font=('Verdana', 12), anchor=tk.N).grid(row=4, column=1)
 # (g) Sprinter: deck label & card count
 tk.Label(master=self.frm_decks, text='Sprinter Deck', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=4)
 self.vS = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vS, font=('Verdana', 12), anchor=tk.N).grid(row=3, column=4)
 self.vSrec = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vSrec, font=('Verdana', 12), anchor=tk.N).grid(row=4, column=4)
 # (h) Exhaustion labels
 tk.Label(master=self.frm_decks, text='Rouleur Exhaustion Cards', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=2)
 tk.Label(master=self.frm_decks, text='Sprinter Exhaustion Cards', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=5)
 # (4) Footer Widgets
 self.btn_close = tk.Button(master=self.frm_footer, text='Close', anchor=tk.S,
 relief=tk.RAISED, font=('Verdana', 25), command=self.handle_close,
 padx=10, pady=10).pack(side=tk.BOTTOM)
 # Pack the frames
 self.frm_header.pack(side=tk.TOP)
 self.frm_decks.pack(side=tk.TOP)
 self.frm_cards.pack(side=tk.TOP)
 self.frm_footer.pack(side=tk.BOTTOM)
 self.update_counts()
 ### END INIT
 #Properties, getters
 @property
 def deck_R(self):
 return self._deck_R
 @property
 def deck_S(self):
 return self._deck_S
 # Handle events, clicks etc
 def handle_close(self):
 self.master.destroy()
 def show_hand(self, deck: GL.MainDeck, num=HANDSIZE):
 # Check we have enough cards, if not, shuffle and reintroduce cards in the recycle deck
 if deck.count_cards < num:
 messagebox.askokcancel('information', f'You have used all your {deck.rider} cards! Shuffling and reintroducing recycled cards...',)
 deck.refresh_deck()
 self.update_counts()
 hand = GL.Deck(deck.team, deck.rider, deck.teamtype)
 self.frm_cards.pack()
 for i in range(0,num):
 crd = deck.get_card(i)
 hand.add(crd)
 # Hand widgets
 imgs = hand.getFilePaths()
 hand_photos = []
 for im in imgs:
 hand_photos.append(ImageTk.PhotoImage(Image.open(im).resize((CARD_W, CARD_H))))
 hand_btns = []
 for i in range(len(imgs)):
 hand_btns.append(tk.Button(master=self.frm_cards, width=CARD_W, height=CARD_H,
 image=hand_photos[i],
 command=lambda i=i: self.playthecard(hand, i)))
 hand_btns[i].image = hand_photos[i]
 hand_btns[i].grid(row=1, column=i, padx=20, pady=20)
 # Pick up an exhaustion card and add to the recycle deck for that rider
 def pickup_exhaust(self, deck: GL.Deck, num=1):
 for i in range(0,num):
 answer = messagebox.askokcancel("Question", f'Collect an exhaustion card for your {deck.rider.upper()}?')
 if answer:
 ex_crd = GL.ExhaustCard()
 deck.add(ex_crd)
 self.update_counts()
 # Given a subdeck (e.g. a hand), returns the relevant main deck of cards, i.e. either the player's Rouleur deck or Sprinter Deck
 def getTargetDeck(self, D: GL.Deck) -> GL.MainDeck:
 if D.rider[0:1].upper() == 'R':
 return self.deck_R
 elif D.rider[0:1].upper() == 'S':
 return self.deck_S
 # Play the selected card; i.e. burn it, eliminate from the deck without recycling. Recycle the rest of the hand.
 def playthecard(self, hand: GL.Deck, i: int):
 answer = messagebox.askokcancel('information', f'Play the {hand.rider} {hand.get_card(i).value} card and recycle the other cards?', )
 if answer:
 currentDeck = self.getTargetDeck(hand)
 ToRecycle = hand.getRemainders(i)
 for index in ToRecycle:
 currentDeck.recycle(index) #Recycle the unused cards into recycle deck
 currentDeck.remove_card(0) # Index 0 not index i. After remainder cards have been recycled (above), the first card in the Maindeck list will be that selected by the player.
 self.frm_cards.pack_forget() # Clear the hand widgets from view (does not destroy!)
 self.update_counts()
 # Update the main deck card counts on the GUI
 def update_counts(self):
 self.vR.set(f'{self.deck_R.count_cards} cards in Rouleur deck')
 self.vS.set(f'{self.deck_S.count_cards} cards in Sprinter deck')
 self.vRrec.set(f'and {self.deck_R.recycleDeck.count_cards} recycled cards')
 self.vSrec.set(f'and {self.deck_S.recycleDeck.count_cards} recycled cards')
# Run it
def main():
 window = tk.Tk()
 app = GameWindow(window, 'Pink', 'Player')
 window.mainloop()
if __name__ == '__main__':
 print('GUI Main called')
 main()

And finally... this probably didn't need a file of it's own did it lololol..

import GUI
import tkinter as tk
# Launch the game window
window = tk.Tk()
app = GUI.LaunchWindow(window)
window.mainloop()

Thank you very much in advance!

asked Nov 26, 2020 at 18:31
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Hey RoadBadger :) There's one important skill you can learn using the CodeReview website and it's to give good context on what code is supposed to do :) Right now, you state that game rules aren't important, but that's you'd like to make sure your code doesn't have bugs you didn't see. But how can we know this if we don't know the rules :) You'll need to give us the context of the code (and change your post title to what the code does!) in order for us to help! \$\endgroup\$ Commented Nov 26, 2020 at 18:53
  • 1
    \$\begingroup\$ I don't want to vote to close this question, since a little edit that adds proper context will make it good! \$\endgroup\$ Commented Nov 26, 2020 at 19:15
  • 1
    \$\begingroup\$ Thanks for the feedback guys - will edit the question to explain what the code is supposed to do. \$\endgroup\$ Commented Nov 26, 2020 at 20:07

2 Answers 2

1
\$\begingroup\$

I'll include inline comments on your second file.

############
# GUI Module
############
import tkinter as tk
from PIL import Image, ImageTk
import GameLogic as GL
from tkinter import messagebox
# Constants
CARD_W = 180
CARD_H = 300
W_MULT = 1.2
TEAMS = ('Blue', 'Red', 'Green', 'Black', 'White', 'Pink')
RIDERS = ('Rouleur', 'Sprinter')
PLAYER = ('Player', 'Bot: Muscle Team', 'Bot: Peloton Team')
HANDSIZE = 4
DECK_PATHS = ('deck_main_R', 'deck_exhaust_R', 'FR_logo', 'deck_main_S', 'deck_exhaust_S')

Parentheses are the syntax for a tuple, which usually signifies a heterogeneous bunch of related things that are parts of a whole. But it would be more idiomatic here to use square brackets and create a list, since it is a bunch of things of the same type which are accessed sequentially and are distinct from each other. It doesn't change the functionality.

##################
# LaunchWindow Class
##################
class LaunchWindow:
 def __init__(self, master):
 self.master = master
 # Make the frames
 self.frm_header = tk.Frame(self.master)
 self.frm_options = tk.Frame(self.master)
 self.frm_colour = tk.Frame(master=self.frm_options)
 self.frm_player = tk.Frame(master=self.frm_options)
 self.frm_footer = tk.Frame(master=self.master)
 # (1) Header widgets
 tk.Label(master=self.frm_header, text='Flamme Rouge', anchor=tk.NW, font=('Verdana', 50), fg='red').pack(side=tk.TOP, pady=10, padx=10)
 tk.Label(master=self.frm_header, text='Select team type options', anchor=tk.NW, font=('Verdana', 25)).pack(side=tk.TOP,pady=5)
 # (2) Team Choice Widgets
 # (A) Team Colour Choice Input
 colour_choice = tk.StringVar()
 tk.Label(master= self.frm_colour, text='Team colour:').pack(side=tk.TOP, pady = 3)

Both here and in other places, the spacing around the named parameters' =s is inconsistent -- pick a style and stick with it.

 for team in TEAMS:
 textcolour = 'White'
 if team == 'White' or team == 'Pink':
 textcolour = 'Black'

It would be slightly more idiomatic to define LIGHT_COLOURS outside the loop and then condense this to one line, e.g. textcolour = 'Black' if team in LIGHT_COLOURS else 'White' to avoid repeating team and textcolour

 rb_teams = tk.Radiobutton(master= self.frm_colour, text=team, value=team, variable=colour_choice, bg=team, fg=textcolour)
 rb_teams.pack(anchor=tk.N)

Why do you create a variable rb_teams only to immediately throw it away? Just put the .pack() at the end of the line like you did with the other UI elements you're not concerned with.

 # (B) Player (Human or Bot) Choice Input
 player_choice = tk.StringVar()
 tk.Label(master=self.frm_player, text='Team Type:').pack(side=tk.TOP, pady=3)
 for p in PLAYER:
 rb_teams = tk.Radiobutton(master=self.frm_player, text=p, value=p, variable=player_choice, indicatoron=0)
 rb_teams.pack(anchor=tk.N)

This is wrong and confusing; these aren't teams buttons but player buttons. Lose the variable name and put the .pack on the same line.

 # (3) Footer Widgets
 self.btn_start = tk.Button(master=self.frm_footer, text='Start Game', anchor=tk.S,
 relief=tk.RAISED, font=('Verdana', 35),
 command=lambda: self.start_game(colour_choice.get(), player_choice.get()),
 padx=20, pady=10).pack(side=tk.LEFT)

The lambda here is confusing, as you're mixing program logic into the middle of UI definitions. Create a separate method.

 self.btn_close = tk.Button(master=self.frm_footer, text='Close', anchor=tk.S, padx=20, pady=10,
 relief=tk.RAISED, font=('Verdana', 25), command=self.handle_close).pack(side=tk.LEFT)
 #Pack the frames
 self.frm_header.pack(side=tk.TOP)
 self.frm_colour.pack(side=tk.LEFT)
 self.frm_player.pack(side=tk.LEFT)
 self.frm_options.pack(side=tk.TOP)
 self.frm_footer.pack(side=tk.BOTTOM, pady = 10)

This is confusing; other items were packed at the time of creation, but these are packed after a bunch of other stuff is done. Also the comment doesn't add anything, it's obvious from the code. Comments should explain why something is being done. The code itself should make it clear what is being done.

 # Handle events, clicks etc
 def handle_close(self):
 self.master.destroy()
 def start_game(self, colour: str, team_type: str):
 self.master.destroy()
 game_window = tk.Tk()
 game = GameWindow(game_window, colour, team_type)
##################
# GameWindow Class
##################
class GameWindow:

One class per file usually, please.

 def __init__(self, master, colour, teamtype): # Create labels, entries,buttons

Again this comment is more confusing than helpful. Anyone looking at the code and see that's what it's doing. Don't add comments just for the sake of having comments; the comments should explain your thinking and make it easier for someone to understand the code.

 self.master = master
 # Make the frames
 self.frm_header = tk.Frame(self.master)
 self.frm_decks = tk.Frame(self.master)
 self.frm_cards = tk.Frame(self.master)
 self.frm_footer = tk.Frame(self.master)
 # Initialise the Card decks according to user input on launch window

Except you don't know that. Classes/methods/functions called with parameters shouldn't know or care where their parameters came from. What if you at some point created a "game replay" feature that allowed replaying of a past game -- then the params would not come from user input in the launch window.

 self._deck_R = GL.MainDeck(colour, 'Rouleur', teamtype)
 self._deck_S = GL.MainDeck(colour, 'Sprinter', teamtype)
 self.deck_R.shuffle
 self.deck_S.shuffle

This is confusing; these two decks are clearly the same kind of thing. Looks like the code will alternate between them. Why not put them in a list that you then index into? Then you could do things like for deck in self.decks: deck.shuffle rather than duplicating code.

Also I don't understand why you're sometimes using the underscore version and sometimes the non-underscore version. What's the benefit of making a member private if you then go and make it completely accessible?

 # (1) Header Widgets
 tk.Label(master=self.frm_header, text=colour+' Team', fg=colour, bg='grey', anchor=tk.NW, font=('Verdana', 35, 'bold')).pack(side=tk.TOP, pady=10)

I don't like repeated strings 'Verdana' -- you should put the font name in a constant so you can easily change it everywhere.

 # (2) Deck Widgets
 deck_images = []
 deck_btns = []

Okay. Here is a key clue about what you're doing wrong. Python has very strong list manipulation techniques. It's almost never ideal to start with an empty list and then add items to it. Instead, create a list that defines how it will be created, and then use list comprehensions.

A related issue is, are deck_images and deck_btns the same size and corresponding to each other? If so they should probably be a single list of tuples, classes, or collections.namedtuples.

e.g. DECK_CONFIG as a mixture of DECK_PATHS and DECK_IMAGES, then say deck_ui = [(ImageTk.PhotoImage(Image.open(f'Images/{dc.path}.jpg').resize(dc.dimensions), tk.Button(..., image=dc.image, command=dc.command) for dc in deck_config]

 # (a) Rouleur deck
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[0]}.jpg').resize((int(CARD_W*W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W*W_MULT), height=CARD_H, image=deck_images[0],
 command=lambda:self.show_hand(self.deck_R)))
 deck_btns[0].image = deck_images[0]

You already set that in the previous line.

 deck_btns[0].grid(row=2, column=1, padx=20, pady=20)
 # (b) Rouleur Exhaustion
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[1]}.jpg').resize((int(CARD_W*W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W*W_MULT), height=CARD_H, image=deck_images[1],
 command=lambda:self.pickup_exhaust(self.deck_R.recycleDeck)))
 deck_btns[1].image = deck_images[1]
 deck_btns[1].grid(row=2, column=2, padx=20, pady=20)
 # (c) FR Logo
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[2]}.jpg').resize((int(CARD_W*2), CARD_H))))
 deck_btns.append(tk.Label(master=self.frm_decks, width=int(CARD_W*2.2), height=CARD_H, image=deck_images[2]))
 deck_btns[2].image = deck_images[2]
 deck_btns[2].grid(row=2, column=3, padx=20, pady=20)
 # (d) Sprinter Deck
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[3]}.jpg').resize((int(CARD_W * W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W * W_MULT), height=CARD_H, image=deck_images[3],
 command=lambda: self.show_hand(self.deck_S)))
 deck_btns[3].image = deck_images[3]
 deck_btns[3].grid(row=2, column=4, padx=20, pady=20)
 # (e) Sprinter Exhaustion
 deck_images.append(ImageTk.PhotoImage(Image.open(f'Images/{DECK_PATHS[4]}.jpg').resize((int(CARD_W * W_MULT), CARD_H))))
 deck_btns.append(tk.Button(master=self.frm_decks, width=int(CARD_W * W_MULT), height=CARD_H, image=deck_images[4],
 command=lambda: self.pickup_exhaust(self.deck_S.recycleDeck)))
 deck_btns[4].image = deck_images[4]
 deck_btns[4].grid(row=2, column=5, padx=20, pady=20)
 # (f) Rouleur: deck label & card count
 tk.Label(master=self.frm_decks, text='Rouleur Deck', font=('Verdana', 20), anchor=tk.S).grid(row=1,column=1)
 self.vR = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vR, font=('Verdana', 12), anchor=tk.N).grid(row=3,column=1)
 self.vRrec = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vRrec, font=('Verdana', 12), anchor=tk.N).grid(row=4, column=1)

It's confusing here to intersperse UI setup and setting members of the class. Again this should probably be a config list and then a loop.

 # (g) Sprinter: deck label & card count
 tk.Label(master=self.frm_decks, text='Sprinter Deck', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=4)
 self.vS = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vS, font=('Verdana', 12), anchor=tk.N).grid(row=3, column=4)
 self.vSrec = tk.StringVar()
 tk.Label(master=self.frm_decks, textvariable=self.vSrec, font=('Verdana', 12), anchor=tk.N).grid(row=4, column=4)

I really suggest putting these v* elements into a list or map rather than having a bunch of separate variables.

 # (h) Exhaustion labels
 tk.Label(master=self.frm_decks, text='Rouleur Exhaustion Cards', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=2)
 tk.Label(master=self.frm_decks, text='Sprinter Exhaustion Cards', font=('Verdana', 20), anchor=tk.S).grid(row=1, column=5)
 # (4) Footer Widgets
 self.btn_close = tk.Button(master=self.frm_footer, text='Close', anchor=tk.S,
 relief=tk.RAISED, font=('Verdana', 25), command=self.handle_close,
 padx=10, pady=10).pack(side=tk.BOTTOM)
 # Pack the frames
 self.frm_header.pack(side=tk.TOP)
 self.frm_decks.pack(side=tk.TOP)
 self.frm_cards.pack(side=tk.TOP)
 self.frm_footer.pack(side=tk.BOTTOM)
 self.update_counts()
 ### END INIT
 #Properties, getters
 @property
 def deck_R(self):
 return self._deck_R

This is useless. The purpose of having a getter is to control access to the member and make sure that only authorized actions are performed. But you're allowing anyone to perform any action. Better to not even have it.

 @property
 def deck_S(self):
 return self._deck_S
 # Handle events, clicks etc
 def handle_close(self):
 self.master.destroy()
 def show_hand(self, deck: GL.MainDeck, num=HANDSIZE):
 # Check we have enough cards, if not, shuffle and reintroduce cards in the recycle deck
 if deck.count_cards < num:
 messagebox.askokcancel('information', f'You have used all your {deck.rider} cards! Shuffling and reintroducing recycled cards...',)
 deck.refresh_deck()
 self.update_counts()
 hand = GL.Deck(deck.team, deck.rider, deck.teamtype)
 self.frm_cards.pack()
 for i in range(0,num):

Just use range(num); 0 is the default start

 crd = deck.get_card(i)
 hand.add(crd)
 # Hand widgets
 imgs = hand.getFilePaths()
 hand_photos = []
 for im in imgs:
 hand_photos.append(ImageTk.PhotoImage(Image.open(im).resize((CARD_W, CARD_H))))
 hand_btns = []
 for i in range(len(imgs)):
 hand_btns.append()
 hand_btns[i].image = hand_photos[i]
 hand_btns[i].grid(row=1, column=i, padx=20, pady=20)

Use list comprehension: hand_btns = [tk.Button(master=self.frm_cards, width=CARD_W, height=CARD_H, image=ImageTk.PhotoImage(Image.open(im).resize((CARD_W, CARD_H))), command=lambda i=i: self.playthecard(hand, i)) for (i,im) in enumerate(imgs)]

 # Pick up an exhaustion card and add to the recycle deck for that rider
 def pickup_exhaust(self, deck: GL.Deck, num=1):
 for i in range(0,num):
 answer = messagebox.askokcancel("Question", f'Collect an exhaustion card for your {deck.rider.upper()}?')
 if answer:
 ex_crd = GL.ExhaustCard()
 deck.add(ex_crd)
 self.update_counts()
 # Given a subdeck (e.g. a hand), returns the relevant main deck of cards, i.e. either the player's Rouleur deck or Sprinter Deck
 def getTargetDeck(self, D: GL.Deck) -> GL.MainDeck:
 if D.rider[0:1].upper() == 'R':

You can just use D.rider[0].upper(), but this makes it clear that deck_R and deck_S should be in a map that's indexed by 'R' or 'S'

 return self.deck_R
 elif D.rider[0:1].upper() == 'S':
 return self.deck_S
 # Play the selected card; i.e. burn it, eliminate from the deck without recycling. Recycle the rest of the hand.
 def playthecard(self, hand: GL.Deck, i: int):
 answer = messagebox.askokcancel('information', f'Play the {hand.rider} {hand.get_card(i).value} card and recycle the other cards?', )
 if answer:
 currentDeck = self.getTargetDeck(hand)
 ToRecycle = hand.getRemainders(i)
 for index in ToRecycle:
 currentDeck.recycle(index) #Recycle the unused cards into recycle deck
 currentDeck.remove_card(0) # Index 0 not index i. After remainder cards have been recycled (above), the first card in the Maindeck list will be that selected by the player.
 self.frm_cards.pack_forget() # Clear the hand widgets from view (does not destroy!)
 self.update_counts()
 # Update the main deck card counts on the GUI
 def update_counts(self):
 self.vR.set(f'{self.deck_R.count_cards} cards in Rouleur deck')
 self.vS.set(f'{self.deck_S.count_cards} cards in Sprinter deck')
 self.vRrec.set(f'and {self.deck_R.recycleDeck.count_cards} recycled cards')
 self.vSrec.set(f'and {self.deck_S.recycleDeck.count_cards} recycled cards')
# Run it
def main():
 window = tk.Tk()
 app = GameWindow(window, 'Pink', 'Player')
 window.mainloop()
if __name__ == '__main__':
 print('GUI Main called')
 main()
answered Nov 30, 2020 at 14:18
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Hi @Snowbody thank you so much! That's just what I needed. Your comments are insightful and clear. I am delighted with the feedback. You clearly know your stuff. Just one question: I completely agree with the sentiment of "why make a member private if you then go and make it completely accessible?" and "the purpose of a getter is to control access to the member and make sure that only authorised actions are performed but you're allowing anyone to perform any action", but I don't quite see how/where I am doing that? I am so grateful, please let me know if I can return the favour somehow. \$\endgroup\$ Commented Dec 2, 2020 at 17:33
  • \$\begingroup\$ Yeah, I think you're right. I misunderstood that one. You can return the favor by becoming a better programmer day by day, and when you have spare time helping out other programmers become better, in whatever location suits you. \$\endgroup\$ Commented Dec 4, 2020 at 6:45
  • \$\begingroup\$ Ok, it's a deal! Thanks again \$\endgroup\$ Commented Dec 12, 2020 at 20:15
1
\$\begingroup\$

I can point out a few Python language features that will make the code a bit simpler. In show():

@property
def show(self):
 i = 0
 total = self.count_cards
 for crd in self.cards:
 i += 1
 print(f'Value of card {i} of {total} in {self.team} {self.rider} deck is {crd.value}')
 if not isinstance(self,MainDeck):
 print('This is not the main deck')
  1. Not a @property since it doesn't return anything
  2. Use the enumerate() function, let python handle the index variable
  3. The "not main deck" warning should be shown just once, not every card.

So, for instance:

 def show(self):
 total = self.count_cards
 for (i, crd) in enumerate(self.cards):
 print(f'Value of card {i} of {total} in {self.team} {self.rider} deck is {crd.value}')
 if not isinstance(self,MainDeck):
 print('This is not the main deck')

In shuffle():

@property
def shuffle(self):
 for i in range(self.count_cards -1, -1, -1):
 rnd = random.randint(0, i)
 self.cards[i], self.cards[rnd] = self.cards[rnd], self.cards[i]
  1. Again, definitely not a @property

  2. Python has a shuffle() in the random module, no need to reinvent the wheel

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

In countExhaustion():

@property
def countExhaustion(self) -> int:
 cnt = 0
 for crd in self.cards:
 if isinstance(crd, ExhaustCard):
 cnt += 1
 return cnt
  1. Yes this is a @property
  2. But it might be easier to just have an additional variable and keep it updated, since you know exactly when cards are added or removed whether it's an ExhaustCard
  3. If you decide not to do that, use sum() to count:
 @property
 def countExhaustion(self) -> int:
 return sum(1 for crd in self.cards if isinstance(crd, ExhaustCard))

In MainDeck.__init__:

 if rider[0:1].upper() == 'R':
 for i in range(len(ROULEUR)):
 self._cards.append(Card(ROULEUR[i]))
 elif rider[0:1].upper() == 'S':
 for i in range(len(SPRINTER)):
 self._cards.append(Card(SPRINTER[i]))

This code is duplicated. You should write the code only once and use a dict to map between the first character of rider and which deck. Also the extend() method is defined for sequences which should be preferred over append() in a loop.

deckMap = {'R': ROULEUR, 'S': SPRINTER}
deck = deckMap[rider[0]].upper()
self._cards.extend(deck)
answered Nov 27, 2020 at 2:00
\$\endgroup\$
2
  • 2
    \$\begingroup\$ Just a small nitpick: there's no need to wrap (i, crd) around parentheses here: for (i, crd) in enumerate(self.cards). \$\endgroup\$ Commented Nov 27, 2020 at 7:47
  • \$\begingroup\$ Hi @Snowbody, thank you very much for the useful feedback! Much appreciated. \$\endgroup\$ Commented Nov 28, 2020 at 15:46

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.