The GameScreen class

Our GameScreen will be a subclass from Tkinter's powerful Canvas widget and contain all of the logic to do with our window's graphics and animations:

class GameScreen(tk.Canvas):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)

self.DECK_COORDINATES = (700, 100)

self.CARD_ORIGINAL_POSITION = 100
self.CARD_WIDTH_OFFSET = 100

self.PLAYER_CARD_HEIGHT = 300
self.DEALER_CARD_HEIGHT = 100

self.PLAYER_SCORE_TEXT_COORDS = (340, 450)
self.PLAYER_MONEY_COORDS = (490, 450)
self.POT_MONEY_COORDS = (500, 100)
self.WINNER_TEXT_COORDS = (400, 250)

We begin by initializing the super class, Canvas, with the arguments passed over by our GameWindow. We use the **kwargs argument to pass all of our options over to the Canvas' __init__ method.

The first argument to our GameScreen is called master, and this will refer to our GameWindow instance. This allows our GameScreen to alter the GUI elements of the game by calling methods defined within our GameWindow class, referring to it by self.master.

Our constants will now sit in this class, with a few new ones added.

self.DECK_COORDINATES holds the position at which we will draw a card back image representing the deck of cards being dealt from. When a card is dealt to a player it will appear to slide from the deck of cards over to the calculated position on the table.

self.PLAYER_MONEY_COORDS holds the coordinates for text displaying the player's current amount of money.

self.POT_MONEY_COORDS holds the coordinates for text displaying the amount of money in the pot. The pot holds the total amount of money that will be given to the winner of the round.

With the constants taken care of, we can now define some regular attributes:

self.game_state = GameState()
self.sound_board = SoundBoard()

self.tabletop_image = tk.PhotoImage(file=assets_folder + "/tabletop.png")
self.card_back_image = Card.get_back_file()

self.player_score_text = None
self.player_money_text = None
self.pot_money_text = None
self.winner_text = None

self.cards_to_deal_pointer = 0
self.frame = 0

We grab instances of our SoundBoard and GameState classes so that we can access their methods within all methods in this class.

The images that we will need to draw every game are stored as attributes for our GameScreen class. This includes the tabletop background and the card back.

Each piece of text that needs to be drawn to the GameScreen has an attribute reserved for it, which is initially set to None.

The cards_to_deal_pointer and frame attributes will be used for displaying animations, and are covered in the relevant method explanation.

Now that we know what happens when creating a GameScreen, let's look at how it displays the opening animation of our game:

def setup_opening_animation(self):
self.sound_board.shuffle_sound.play()
self.create_image((400, 250), image=self.tabletop_image)

self.card_back_1 = self.create_image(self.DECK_COORDINATES,
image=self.card_back_image)
self.card_back_2 = self.create_image((self.DECK_COORDINATES[0] + 20,
self.DECK_COORDINATES[1]), image=self.card_back_image)

self.back_1_movement = ([10] * 6 + [-10] * 6) * 7
self.back_2_movement = ([-10] * 6 + [10] * 6) * 7

self.play_card_animation()

This animation will simply show the deck of cards being shuffled.

We start by playing the sound for our deck shuffling so that the user knows what is being shown. We do this by calling the play method of the shuffle_sound attribute on our sound_board instance. This instructs pygame to play the audio file cardShuffle1.wav.

We now need to draw the tabletop, which serves as the background image for our GameScreen.

In order to give the illusion of a shuffling deck of cards, we will use two card back images moving back and forth horizontally.

To set this up, we need to draw two card back images on our canvas. These will be referred to as card_back_1 and card_back_2.

We will draw one of them at the location of our DECK_COORDINATES and one 20 pixels over to the right.

The next thing to do is to make a list of steps to move each card back by. This may look a little strange if you are not familiar with list multiplication. The first step is to multiply a list of one value (such as [10]) by 6 to make a list of 6 x 10s ([10, 10, 10, 10, 10, 10]). Next, we reverse these steps by multiplying a list of—10s by 6 to make another list of 6 items. We add these together to create one list of 12 items. We multiply the resulting list by 7 in order to create a single list of 84 items.

To see the result of this, you can enter ([10] * 6 + [-10] * 6) * 7 in the Python REPL.

We reverse the signs of the initial lists for the second card back so that both card backs will move in opposite directions each time. We store both of these lists as back_1_movement and back_2_movement.

Now that we have prepared the variables needed for our opening animation, it's time to make it play. We now call play_card_animation to get things moving:

def play_card_animation(self):
if self.frame < len(self.back_1_movement):
self.move(self.card_back_1, self.back_1_movement[self.frame], 0)
self.move(self.card_back_2, self.back_2_movement[self.frame], 0)
self.update()
self.frame += 1
self.after(33, self.play_card_animation)
else:
self.delete(self.card_back_2)
self.frame = 0
self.display_table()

We use the frame attribute to keep a reference of how far along each list we currently are.

While our frame number is less than the total number of steps in one of our back_movement variables, we call the move method to move each card back image by the number contained in that step (10 or -10).

To show these changes, we use the update method to refresh our GameScreen and re-draw the images in their new positions.

We increase the frame value so that we will call the next step of our animation the next time our loop runs, and finish off by scheduling this function once again after 33 milliseconds. The 33 milliseconds is chosen in order to approximate 30 frames per second as our animation speed.

Once we have exhausted all 84 of our frames, we remove one of the card back images, leaving one remaining to represent our shuffled deck.

We reset our current frame to 0, allowing us to play another animation if need be.

Now that all of our frames have played, our animation will have finished showing, and we can draw the initial table state as we did in the previous chapter. Unlike before, the initial table will also feature animations—cards will move across the table as if being dealt from the deck:

def display_table(self, hide_dealer=True, table_state=None):
if not table_state:
table_state = self.game_state.get_table_state()

player_card_images = [card.get_file() for card in
table_state['player_cards']]
dealer_card_images = [card.get_file() for card in
table_state['dealer_cards']]
if hide_dealer and not table_state['blackjack']:
dealer_card_images[0] = Card.get_back_file()
The get_table_state method can be copied over from the previous chapter and added to the new GameState class. The only change to be made is replacing self.player_hand.cards with self.player.cards to make use of our new properties.

The beginning of this method looks the same as before. We once again grab the initial state of the table from our GameState instance, extract the player's and dealer's cards, and grab the card images from our Card class:

self.cards_to_deal_images = []
self.cards_to_deal_positions = []

for card_number, card_image in enumerate(player_card_images):
image_pos = self.get_player_card_pos(card_number)
self.cards_to_deal_images.append(card_image)
self.cards_to_deal_positions.append(image_pos)

for card_number, card_image in enumerate(dealer_card_images):
image_pos = (self.CARD_ORIGINAL_POSITION + self.CARD_WIDTH_OFFSET * card_number, self.DEALER_CARD_HEIGHT)
self.cards_to_deal_images.append(card_image)
self.cards_to_deal_positions.append(image_pos)

self.play_deal_animation()

while self.playing_animation:
self.master.update()

In order to set up for an animation, we need to pass two lists of information back over to our animating method – the card image files and the position at which to place them. We will use two list attributes in order to store them.

We again loop over the cards held by the player and calculate the position at which to place it within the canvas. This has been abstracted out to a new method, get_player_card_pos, but the code is the same as it was in the previous chapter:

def get_player_card_pos(self, card_number):
return (self.CARD_ORIGINAL_POSITION + self.CARD_WIDTH_OFFSET * card_number, self.PLAYER_CARD_HEIGHT)

We append each position and card image to our cards_to_deal attributes before doing the same process with the dealer's cards.

Now that we have the required information, we can play the deal animations by calling play_deal_animation.

Once the animation has begun playing, we want to wait for it to finish playing before continuing in this method, so we use an attribute called playing_animation to communicate this. We wait on this using a while loop, which simply tells the window to update its content, thus playing each frame of the animation. This has the added bonus of blocking in this method until the play_deal_animation method sets playing_animation to False.

Let's have a look at how the play_deal_animation will handle all of this:

def play_deal_animation(self):
self.playing_animation = True
self.animation_frames = 15

self.card_back_2 = self.create_image(self.DECK_COORDINATES,
image=self.card_back_image)

target_coords = self.cards_to_deal_positions
[self.cards_to_deal_pointer]

x_diff = self.DECK_COORDINATES[0] - target_coords[0]
y_diff = self.DECK_COORDINATES[1] - target_coords[1]

x_step = (x_diff / self.animation_frames) * -1
y_step = (y_diff / self.animation_frames) * -1

self.move_func = partial(self.move_card, item=self.card_back_2,
x_dist=x_step, y_dist=y_step)
self.move_func.__name__ = 'move_card'

self.move_card(self.card_back_2, x_step, y_step)

We begin by setting playing_animation to True and blocking the previous function.

Another attribute called animation_frames is used to control how long each card deal animation will play for. A value of 15 at roughly 30 frames per second means each deal animation will take around half a second.

To simulate a card being dealt from the deck, we will begin by drawing another card back image on top of the one that represents the deck. We use the DECK_COORDINATES constant to ensure that they are perfectly aligned.

We now need to calculate each of the 15 positions that the card will need to be drawn in to place it in the target position obtained from the cards_to_deal_positions.

The relevant target position is extracted from cards_to_deal_positions using our cards_to_deal_pointer, which was initialized to 0 in our __init__ method.

With this, we now need to find the total distance between the target position and the coordinates of our deck. We store these as x_diff and y_diff.

To calculate each step, we divide these total differences by the number of animation frames. This gives us how much we need to move in each direction per frame.

Since our deck is placed top-right of the dealing positions and our cards need to move to the bottom-left, we need to reverse the sign of each of our steps. We achieve this by multiplying the steps by -1. These steps are stored as x_step and y_step.

As we did when playing our shuffle animation, we need a function that we can repeatedly call using Tkinter's after method. The partial function from the functools library will allow us to do that.

A partial function is a function that has some (or all) of its arguments frozen at certain values. You can think of it as cloning another function but setting the default values of all of its arguments.

The first argument to the partial function is the function that needs its arguments fixed. We pass this through the move_card method, which will be detailed next. We then pass the item argument as our card_back_2 image, the x_step as our calculated x_step, and the y_step as our calculated y_step.

In order to avoid an error, we set the __name__ attribute of this partial function to "move_card". This allows our partial function to be compatible with Tkinter's after method.

Now we can call the first round of this method to begin the chain:

def move_card(self, item, x_dist, y_dist):
self.move(item, x_dist, y_dist)
self.update()
self.frame += 1
if self.frame < self.animation_frames:
self.after(33, self.move_func)
else:
self.frame = 0
self.delete(self.card_back_2)
self.show_card()
self.sound_board.place_sound.play()

As with the previous animation function, we move our image the required distances with the move method, update the canvas to show the image in its new position, and then update our frame attribute to advance the animation counter.

If we still have frames left to display, then we reschedule our move_func partial for 33 milliseconds later using the after method.

Once we have exhausted all of our frames, we reset the frame counter back to 0 again. Next, the card back image needs to be removed and the face-up card image needs to be drawn in its place. We do this using the show_card method, which will be shown next. A sound is also played to represent the card hitting the table.

Showing the face up card simply involves creating the card image in the target position and updating our canvas to display it:

def show_card(self):
self.create_image(
self.cards_to_deal_positions[self.cards_to_deal_pointer],
image=self.cards_to_deal_images[self.cards_to_deal_pointer]
)
self.update()

Back inside our else statement in move_card, we need to check if there are any more cards to play the dealing animation for:

if self.cards_to_deal_pointer < (len(self.cards_to_deal_images) - 1):
self.cards_to_deal_pointer += 1
self.play_deal_animation()
else:
self.cards_to_deal_pointer = 0
self.cards_to_deal_images = []
self.cards_to_deal_positions = []
self.playing_animation = False

If we have more cards to display, we update the pointer to refer to the next item in our cards_to_deal lists and call play_deal_animation once again.

When we have played our animation for each necessary card, our pointer is reset back to 0, our cards_to_deal lists are emptied out, and we set playing_animation to False, unblocking the display_table method.

Speaking of which, let's carry on with the rest of display_table. This code will sit underneath our while loop:

self.sound_board.chip_sound.play()
self.update_text()

if table_state['blackjack']:
self.master.show_next_round_options()
self.show_winner_text(table_state['has_winner'])
else:
self.master.show_gameplay_buttons()

Since all of the cards have been dealt, it's time for the money to be bet. To indicate this, we play the sound of a casino chip being placed down.

We then call the update_text method to display various pieces of information on the screen.

The method is finished off by checking the game state for a winner. If someone was dealt blackjack from the beginning, we need to indicate the winner and tell the GameWindow to show the appropriate buttons at the bottom of the window. Otherwise, we get our GameWindow to display the hit and stick buttons with show_gameplay_buttons.

The update_text method is responsible for all create_text calls on our canvas:

def update_text(self):
self.delete(self.player_money_text, self.player_score_text, self.pot_money_text)

self.player_score_text = self.create_text(self.PLAYER_SCORE_TEXT_COORDS, text=self.game_state.player_score_as_text(), font=(None, 20))
self.player_money_text = self.create_text(self.PLAYER_MONEY_COORDS, text=self.game_state.player_money_as_text(), font=(None, 20))
self.pot_money_text = self.create_text(self.POT_MONEY_COORDS, text=self.game_state.pot_money_as_text(), font=(None, 20))

When updating the text displayed, we first delete all of our text from the canvas. This ensures that there is no old text hanging around.

The three text items we create will display the player's score (as in the previous chapter), the player's money, and the money in the pot (which will be won at the end of the round).

All three of our text items are drawn at locations specified in our class' constants. Each item also has a method within the GameState to return their values as a string:

def player_score_as_text(self):
return "Score: " + str(self.player.score)

def player_money_as_text(self):
return "Money: £" + str(self.player.money)

def pot_money_as_text(self):
return "Pot: £" + str(self.pot)

The only remaining text items that are not displayed by this method are the winner text, which gets its own method as it is not drawn at the beginning of each round, and the out-of-money text, which only displays at the end of a game. These also live in the GameState class:

def show_winner_text(self, winner):
if winner == 'p':
self.winner_text = self.create_text(self.WINNER_TEXT_COORDS,
text="YOU WIN!", font=(None, 50))
elif winner == 'dp':
self.winner_text = self.create_text(self.WINNER_TEXT_COORDS,
text="TIE!", font=(None, 50))
else:
self.winner_text = self.create_text(self.WINNER_TEXT_COORDS,
text="DEALER WINS!", font=(None, 50))

def show_out_of_money_text(self):
self.winner_text = self.create_text(self.WINNER_TEXT_COORDS,
text="Out Of Money - Game Over", font=(None, 50))

These should seem familiar from the previous chapter – the relevant text is drawn at the center of our canvas.

With the animations displayed and the GameWindow showing our hit and stick buttons, it's time for the game logic to begin. Let's create our GameState class to handle the rules and variables we need to begin playing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset