From 51239f9182cfb3f36e999355f098af51861c833a Mon Sep 17 00:00:00 2001 From: En Yi Date: Mon, 10 Jun 2019 17:40:51 +0100 Subject: [PATCH] More improvements --- cards.py | 10 +- game_consts.py | 21 ++ main.py | 52 ++--- players.py | 582 +------------------------------------------------ table.py | 547 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 604 insertions(+), 608 deletions(-) create mode 100644 game_consts.py create mode 100644 table.py diff --git a/cards.py b/cards.py index f291e94..7817642 100644 --- a/cards.py +++ b/cards.py @@ -57,9 +57,9 @@ class Card(pygame.sprite.Sprite): self.original_backimage = None self.image = None self.backimage = None + self.rect = None self.add_image(image_data, backimage_data) - self.rect = self.image.get_rect() self._layer = 0 def add_image(self, image, backimage=None): @@ -157,6 +157,8 @@ class Deck(): card.parent = self if self.vert_orientation: card.set_angle(90) + else: + card.set_angle(0) number_of_cards = len(self.cards) if number_of_cards == 0: @@ -254,9 +256,9 @@ class Deck(): if self.deck_reveal == DeckReveal.HIDE_ALL: image_to_draw = card.backimage - if self.flip: - image_to_draw = pygame.transform.flip(card.image, self.vert_orientation, - not self.vert_orientation) + if self.flip: + image_to_draw = pygame.transform.flip(image_to_draw, self.vert_orientation, + not self.vert_orientation) self.deck_surface.blit(image_to_draw, (card.x - selected * card.x * 0.5 * (-1)**self.flip * self.vert_orientation, diff --git a/game_consts.py b/game_consts.py new file mode 100644 index 0000000..d58c77c --- /dev/null +++ b/game_consts.py @@ -0,0 +1,21 @@ +from enum import Enum + + +class GameState(Enum): + DEALING = 0 + POINT_CHECK = 1 + BIDDING = 2 + PLAYING = 3 + ENDING = 4 + + +class PlayerRole(Enum): + UNKNOWN = 0 + ATTACKER = 1 + DEFENDER = 2 + + +NUM_OF_PLAYERS = 4 +STARTING_HAND = 13 +HIGHEST_CARD = 414 +LOWEST_CARD = 102 diff --git a/main.py b/main.py index c1da2b8..39a4904 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,22 @@ import view import pygame -import players +import table import random import pickle import sys -import queue -import threading +#import queue +#import threading class GameScreen(view.PygView): def __init__(self, *args, autoplay=False, view_all_cards=False, **kwargs): super().__init__(*args, **kwargs) - self.table = players.Table(0, 0, self.width, self.height, (0, 32, 0), + self.table = table.Table(0, 0, self.width, self.height, (0, 32, 0), autoplay=autoplay, view_all_cards=view_all_cards) self.table.update_table.connect(self.draw_table) self.draw_table() - self.player_commands = queue.Queue(1) - self.player_thread = threading.Thread(target=self.get_player_inputs) + #self.player_commands = queue.Queue(1) + #self.player_thread = threading.Thread(target=self.get_player_inputs) self.running = False def draw_table(self, **kwargs): @@ -37,32 +37,34 @@ class GameScreen(view.PygView): pygame.display.flip() - def get_player_inputs(self): - while self.running: - if not self.player_commands.full(): - player_cmd = input("Enter something:") - self.player_commands.put(player_cmd) - print('Finished') + def process_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self.running = False + if event.key == pygame.K_p: + if not self.table.ongoing: + self.table.ongoing = True + + #def get_player_inputs(self): + # while self.running: + # if not self.player_commands.full(): + # player_cmd = input("Enter something:") + # self.player_commands.put(player_cmd) def run(self): self.running = True #self.player_thread.start() while self.running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - self.running = False - if event.key == pygame.K_p: - if not self.table.ongoing: - self.table.ongoing = True - #if event.key == pygame.K_l: + self.process_events() + if self.table.ongoing: self.table.continue_game() - if not self.player_commands.empty(): - player_cmd = self.player_commands.get() - print("Player Command Received: " + player_cmd) + #if not self.player_commands.empty(): + # player_cmd = self.player_commands.get() + # print("Player Command Received: " + player_cmd) pygame.quit() diff --git a/players.py b/players.py index 7ed3aba..1caffbf 100644 --- a/players.py +++ b/players.py @@ -1,530 +1,6 @@ -import pygame import cards -import view -import random -import copy -import time import pprint -from signalslot import Signal -from ai_comp import ai -from enum import Enum - -NUM_OF_PLAYERS = 4 -STARTING_HAND = 13 -HIGHEST_CARD = 414 -LOWEST_CARD = 102 -VIEW_TRANSPARENT = False # Make the text box not transparent - - -class GameState(Enum): - DEALING = 0 - POINT_CHECK = 1 - BIDDING = 2 - PLAYING = 3 - ENDING = 4 - - -class PlayerRole(Enum): - UNKNOWN = 0 - ATTACKER = 1 - DEFENDER = 2 - - -class Table: - """ - A Table is the place where all actions takes place. It is essentially a FSM, doing different - routines at each state. It needs to keep track of the score, roles, the rules, etc. It needs - to ask each player for decisions and respond to them accordingly. The table will also need - to inform any decision to the Main Screen so that it can update the screen to reflect that - change through the use of callbacks (Signal and Slot). This call should be minimised by making - all the changes before calling to update the screen in one go. - - FSM cycles - --- - Preloop - Prepare the cards once - - Initiate Players and connect them to the Table - 1. Shuffle and Deal out cards to Players. - 2a. Detect weak hands and ask for reshuffle. - 2b. Return to (1) if any reshuffle occurs, otherwise proceed. - 3. Bidding round. Randomly pick a starting player, in clockwise manner - ask for a bid until it is valid. - 3b. Proceed only if 3 consecutive skips are detected. - 3c. Ask the winner of the bid a card not in their hand. - 3d. Set up the player roles, trump suit, rounds to win for both side - 3e. Play the game. Start with bid winner if NO TRUMP, otherwise - Starting next to the bid winner. - 4a. With the first player, ask for any card, excluding trump suits if trump - is not broken - 4b. With subsequent players, ask for cards that follow the suit of the first player - , include trump suit if trump is broken. Ask for any card if the player cannot - follow suit. - 4c. Once all 4 players has made valid plays, announce results, update scoring. Announce - player roles if the partner card is played. Break trump if trump is played. - 4d. Repeat 4 until 13 rounds are made. Maybe add early win if confirmed one side wins - 5. Ask for a new game. Go back to 1 if true. - - All played cards go into a hidden discard pile. - - """ - update_table = Signal() - - def __init__(self, x, y, width, height, clear_colour, autoplay=False, view_all_cards=False): - # TODO: Reduce the amount of update_table call - self.x = x - self.y = y - self.width = width - self.height = height - - self.table_font = pygame.font.SysFont("None", 25) - self.player_font = pygame.font.SysFont("None", 25) - - # For gameplay - self.game_state = GameState.DEALING - self.current_round = 0 - self.passes = 0 - self.current_player = 0 - self.first_player = False # This is for bidding purposes - self.players = [] - self.players_playzone = [] - # Table status will be made known to the player by reference - self.table_status = {'played cards': [0, 0, 0, 0], 'leading player': 0, 'trump suit': 1, - 'trump broken': False, 'round history': [], 'bid': 0, 'partner': 0, - 'partner reveal': False, 'defender': {'target': 0, 'wins': 0}, - 'attacker': {'target': 0, 'wins': 0}} - - # Prepare the surfaces for displaying - self.background = pygame.Surface((self.width, self.height)) - self.background.fill(clear_colour) - self.background = self.background.convert() - - # TODO: Update the drawing of the table? - # Prepare the card with dimensions - w_deck = min(self.height, self.width) * 0.18 - l_deck = min(self.width, self.height) * 0.7 - # This is not a deck as it will never be drawn - self.discard_deck = cards.prepare_playing_cards(int(w_deck*0.7), int(w_deck*0.9)) - game_margins = 5 - - # Players' deck positioning - playerx = ((self.width - l_deck)//2, - game_margins, - (self.width - l_deck)//2, - self.width - w_deck - game_margins) - playery = (self.height - w_deck - game_margins, - (self.height - l_deck)//2, - game_margins, - (self.height - l_deck)//2) - h_spacing = 20 - v_spacing = 25 - - # Middle playfield for announcer and player playing deck positioning - playfield_margins = 5 - margins_with_w_deck = w_deck + playfield_margins + game_margins - playfield_x = margins_with_w_deck - playfield_y = margins_with_w_deck - playfield_width = self.width - margins_with_w_deck * 2 - playfield_height = self.height - margins_with_w_deck * 2 - - playdeckx = (playfield_x + (playfield_width - w_deck) / 2, - playfield_x, - playfield_x + (playfield_width - w_deck) / 2, - playfield_x + playfield_width - w_deck) - playdecky = (playfield_y + playfield_height - w_deck, - playfield_y + (playfield_height - w_deck) / 2, - playfield_y, - playfield_y + (playfield_height - w_deck) / 2) - - # Player stats positioning - stats_width = 100 - self.stats_height = 100 - stats_spacing = 10 - self.player_stats_x = (playdeckx[0] - stats_width - stats_spacing, - playdeckx[1], - playdeckx[2] + w_deck + stats_spacing, - playdeckx[3]) - self.player_stats_y = (playdecky[0] + w_deck - self.stats_height, - playdecky[1] - self.stats_height - stats_spacing, - playdecky[2], - playdecky[3] + w_deck + stats_spacing) - - self.player_stats = [[], [], [], []] - - # TODO: change surface to use colorkey, maybe, if the performance is tanked - # Prepare all the player surfaces - for i in range(NUM_OF_PLAYERS): - vert = i % 2 == 1 - spacing = h_spacing - if vert: - spacing = v_spacing - - reveal_mode = cards.DeckReveal.HIDE_ALL - if i == 0 or view_all_cards: - reveal_mode = cards.DeckReveal.SHOW_ALL - self.players.append(Player(playerx[i], playery[i], - l_deck, w_deck, - spacing, vert_orientation=vert, - deck_reveal=reveal_mode)) - self.players[i].connect_to_table(self.table_status) - if i > 0: - self.players[i].add_ai(ai.RandomAI(self.table_status)) - - self.players_playzone.append(cards.Deck(playdeckx[i], playdecky[i], - w_deck, w_deck, 0)) - for j in range(3): - surf = pygame.Surface((stats_width, self.stats_height / 3), pygame.SRCALPHA) - rendered_text = self.player_font.render("Player {0:d}".format(i), True, - (255, 0, 255)).convert_alpha() - self.center_text_on_surface(surf, rendered_text, - (255, 255, 255, 255 * VIEW_TRANSPARENT)) - self.player_stats[i].append(surf) - - if autoplay: - self.players[0].add_ai(ai.RandomAI(self.table_status)) - - # Announcer positioning and surface creation - announcer_margins = 5 - announcer_spacing = announcer_margins + w_deck - self.announcer_x = playfield_x + announcer_spacing - self.announcer_y = playfield_y + announcer_spacing - self.announcer_width = playfield_width - 2 * announcer_spacing - self.announcer_height = playfield_height - 2 * announcer_spacing - self.announcer_line = [] - for i in range(3): - surf = pygame.Surface((self.announcer_width, self.announcer_height/3), pygame.SRCALPHA) - self.announcer_line.append(surf) - - self.update_all_players(role=True, wins=True) - - self.write_message("Press P to play!") - - self.ongoing = False - - - - def center_text_on_surface(self, surf, rendered_text, clear_colour): - line_center = surf.get_rect().center - text_rect = rendered_text.get_rect(center=line_center) - surf.fill(clear_colour) - surf.blit(rendered_text, text_rect) - - def write_message(self, text, delay_time=0.5, line=0, update_now=True): - """ - Write a message into the center board surface (announcer) - :param text: String to be displayed on the center board - :param delay_time: How much delay to put once the string is display - :param line: Which line of the announcer to write to - :return: None - """ - if 0 <= line < len(self.announcer_line): - print(text) - text = text.strip('\n') - rendered_text = self.table_font.render(text, True, (255, 255, 255)).convert_alpha() - self.center_text_on_surface(self.announcer_line[line], rendered_text, - (255, 255, 255, 255*VIEW_TRANSPARENT)) - if update_now: - self.update_table.emit() - time.sleep(delay_time) - - def update_players_role(self, player_num, update_now=True): - self.player_stats[player_num][1].fill((255, 255, 255, 255*VIEW_TRANSPARENT)) - if self.players[player_num].role == PlayerRole.DEFENDER: - rendered_text = self.player_font.render("Defender", True, (0, 64, 192)).convert_alpha() - self.center_text_on_surface(self.player_stats[player_num][1], rendered_text, - (255, 255, 255, 255 * VIEW_TRANSPARENT)) - elif self.players[player_num].role == PlayerRole.ATTACKER: - rendered_text = self.player_font.render("Attacker", True, (192, 0, 0)).convert_alpha() - self.center_text_on_surface(self.player_stats[player_num][1], rendered_text, - (255, 255, 255, 255 * VIEW_TRANSPARENT)) - if update_now: - self.update_table.emit() - - def update_player_wins(self, player_num, update_now=True): - self.player_stats[player_num][2].fill((255, 255, 255, 255*VIEW_TRANSPARENT)) - if self.players[player_num].score > 1: - rendered_text = self.player_font.render("Wins: {0:d}".format(self.players[player_num].score), True, - (255, 255, 255)).convert_alpha() - else: - rendered_text = self.player_font.render("Win: {0:d}".format(self.players[player_num].score), True, - (255, 255, 255)).convert_alpha() - self.center_text_on_surface(self.player_stats[player_num][2], rendered_text, - (255, 255, 255, 255 * VIEW_TRANSPARENT)) - if update_now: - self.update_table.emit() - - def update_all_players(self, role=False, wins=True): - for i in range(NUM_OF_PLAYERS): - if wins: - self.update_player_wins(i, update_now=False) - if role: - self.update_players_role(i, update_now=False) - self.update_table.emit() - - def display_current_player(self, current=-1): - if current >= 0: - print("Player {0:d}\n".format(current)) - for i in range(NUM_OF_PLAYERS): - rendered_text = self.player_font.render("Player {0:d}".format(i), True, - (255, 0, 255)).convert_alpha() - if i == current: - self.center_text_on_surface(self.player_stats[i][0], rendered_text, - (0, 64, 0, 255)) - else: - self.center_text_on_surface(self.player_stats[i][0], rendered_text, - (255, 255, 255, 255 * VIEW_TRANSPARENT)) - - self.update_table.emit() - - def update_team_scores(self): - if self.table_status['partner reveal']: - msg = "Defender: {0:d}/{2:d}, Attacker: {1:d}/{3:d}\n".format(self.table_status['defender']['wins'], - self.table_status['attacker']['wins'], - self.table_status['defender']['target'], - self.table_status['attacker']['target']) - self.write_message(msg, line=2) - else: - msg = "Defender: {0:d}?/{1:d}, Attacker: ?/{2:d}\n".format(self.table_status['defender']['wins'], - self.table_status['defender']['target'], - self.table_status['attacker']['target']) - self.write_message(msg, line=2) - - def get_pos(self): - return self.x, self.y - - def continue_game(self): - """ - This is where the FSM is. State transition should occur here. - What takes place in the state should be in a function. - :return: None - """ - # TODO: Adjust the timing of sleep - if self.game_state == GameState.DEALING: - self.shuffle_and_deal() - self.write_message("Shuffle Complete!") - self.game_state = GameState.POINT_CHECK - - elif self.game_state == GameState.POINT_CHECK: - if self.check_reshuffle(): - self.write_message('Reshuffle Initiated!', line=1) - self.game_state = GameState.ENDING - else: - self.write_message('No Reshuffle needed!') - self.game_state = GameState.BIDDING - self.write_message("Start to Bid") - self.prepare_bidding() - elif self.game_state == GameState.BIDDING: - bid_complete = self.start_bidding() - if bid_complete: - self.game_state = GameState.PLAYING - self.update_all_players(role=True, wins=True) - self.update_team_scores() - - elif self.game_state == GameState.PLAYING: - self.play_a_round() - if self.current_round == 13: - self.write_message("Game Set! Press P to play again!") - self.ongoing = False - self.game_state = GameState.ENDING - else: - self.reset_game() - self.game_state = GameState.DEALING - - def shuffle_and_deal(self): - """ - Shuffle and deal the discard deck to the players, which should have 52 cards. - :return: None - """ - if self.discard_deck: - for i in range(10): - random.shuffle(self.discard_deck) - for player in self.players: - for i in range(STARTING_HAND): - player.add_card(self.discard_deck.pop()) - self.update_table.emit() - - def check_reshuffle(self): - """ - Detect any possible reshuffle request within the players - :return: True if reshuffle requested, else False - """ - print("Player Point Count") - for i, player in enumerate(self.players): - print("Player {0:d}: {1:d}".format(i, player.get_card_points())) - if player.get_card_points() < 4: - self.write_message("Low points detected in Player {0:d}! ".format(i)) - return player.make_decision(self.game_state, 0) - - def prepare_bidding(self): - # Randomly pick a starting player, whom also is the current bid winner - self.current_player = random.randint(1, NUM_OF_PLAYERS) - 1 - print("Starting Player: {0:d}".format(self.current_player)) - self.passes = 0 - self.table_status["bid"] = 11 # Lowest Bid: 1 Club by default - self.first_player = True # Starting bidder "privilege" to raise the starting bid - msg = "Current Bid: {0:d} {1:s}".format(self.table_status["bid"] // 10, - cards.get_suit_string(self.table_status["bid"] % 10)) - self.write_message(msg, line=1, delay_time=0) - self.display_current_player(self.current_player) - msg = 'Bid Leader: Player {0:d}'.format((self.current_player - self.passes - - 1 * (not self.first_player)) % NUM_OF_PLAYERS) - self.write_message(msg, line=2, delay_time=1) - - def start_bidding(self): - """ - The bidding procedure. - :return: Whether bidding is completed - """ - # Highest bid: 7 NoTrump. No further check required - if self.passes < NUM_OF_PLAYERS - 1 and self.table_status["bid"] < 75: - player_bid = self.players[self.current_player].make_decision(self.game_state, 0) - if not player_bid: - if not self.first_player: # Starting bidder pass do not count at the start - self.passes += 1 - else: - self.table_status["bid"] = player_bid - self.passes = 0 - - if self.table_status["bid"] < 75: - self.current_player += 1 - self.current_player %= NUM_OF_PLAYERS - msg = "Current Bid: {0:d} {1:s}".format(self.table_status["bid"] // 10, - cards.get_suit_string(self.table_status["bid"] % 10)) - self.write_message(msg, line=1, update_now=False) - msg = 'Bid Leader: Player {0:d}'.format((self.current_player - self.passes - - 1 * (not self.first_player)) % NUM_OF_PLAYERS) - self.write_message(msg, line=2, update_now=False) - self.display_current_player(self.current_player) - if self.first_player: - self.first_player = False - time.sleep(0.5) - return False - else: - self.write_message("Player {0:d} is the bid winner!".format(self.current_player), delay_time=1) - msg = "Player {0:d} is calling a partner...".format(self.current_player) - self.write_message(msg, delay_time=1) - self.display_current_player(self.current_player) - # Ask for the partner card - self.table_status["partner"] = self.players[self.current_player].make_decision(self.game_state, 1) - - # Setup the table status before the play starts - self.table_status['partner reveal'] = False - self.table_status["trump suit"] = self.table_status["bid"] % 10 - self.table_status["trump broken"] = False - self.table_status['played cards'] = [0, 0, 0, 0] - if self.table_status['trump suit'] == 5: - self.table_status["leading player"] = self.current_player - else: - self.table_status["leading player"] = (self.current_player + 1) % NUM_OF_PLAYERS - self.table_status['defender']['target'] = self.table_status["bid"] // 10 + 6 - self.table_status['attacker']['target'] = 14 - self.table_status['defender']['target'] - - # Set the roles of the players - self.players[self.current_player].role = PlayerRole.DEFENDER - - self.write_message('Bidding Complete', delay_time=0) - msg = 'Trump: {1:s}, Partner: {0:s}'.format(cards.get_card_string(self.table_status["partner"]), - cards.get_suit_string(self.table_status['trump suit'])) - self.write_message(msg, line=1, delay_time=1) - return True - - def play_a_round(self): - """ - Ask each player to play a valid card and determine the winner of the round - :return: None - """ - if not any(self.table_status["played cards"]): - # Leading player starts with the leading card, which determines the leading suit - self.current_player = self.table_status['leading player'] - self.display_current_player(self.current_player) - card = self.players[self.current_player].make_decision(self.game_state, 0) - self.table_status["played cards"][self.current_player] = card - self.players_playzone[self.current_player].add_card(card) - elif not all(self.table_status["played cards"]): - # Subsequent player make their plays, following suit if possible - self.display_current_player(self.current_player) - print("Player {0:d}\n".format(self.current_player)) - card = self.players[self.current_player].make_decision(self.game_state, 1) - self.players_playzone[self.current_player].add_card(card) - self.table_status["played cards"][self.current_player] = card - else: - # Once all player played, find out who wins - leading_card = self.table_status["played cards"][self.table_status['leading player']] - card_suits = [card.suit() for card in self.table_status["played cards"]] - card_nums = [card.number() for card in self.table_status["played cards"]] - follow_suits = [suit == leading_card.suit() for suit in card_suits] - trumps = [suit == self.table_status['trump suit'] for suit in card_suits] - trump_played = any(trumps) - - # Break trump if the trump suit is played - if not self.table_status['trump broken']: - if trump_played: - self.table_status['trump broken'] = True - self.write_message("Trump Broken!", delay_time=1) - - # Determine which players to check for winner, and determine winner - valid_nums = [card_nums[i] * ((follow_suits[i] and not trump_played) or trumps[i]) - for i in range(NUM_OF_PLAYERS)] - winning_player = valid_nums.index(max(valid_nums)) - self.write_message("Player {0:d} wins!\n".format(winning_player), delay_time=1) - self.players[winning_player].score += 1 - self.update_player_wins(winning_player) - - # Clean up the cards, update score, set the next leading player, update round history - for deck in self.players_playzone: - self.discard_deck.append(deck.remove_card()) - - if self.players[winning_player].role == PlayerRole.DEFENDER: - self.table_status['defender']['wins'] += 1 - elif self.players[winning_player].role == PlayerRole.ATTACKER: - self.table_status['attacker']['wins'] += 1 - - self.table_status['leading player'] = winning_player - self.table_status['round history'].append(copy.copy(self.table_status["played cards"])) - self.update_team_scores() - self.table_status["played cards"] = [0]*NUM_OF_PLAYERS - self.current_round += 1 - self.update_table.emit() - return - - if not self.table_status['partner reveal']: - if card.value == self.table_status['partner']: - self.table_status['partner reveal'] = True - self.write_message("Partner Revealed!", delay_time=1) - self.reveal_all_roles(self.current_player) - self.update_all_players(role=True, wins=False) - - self.current_player += 1 - self.current_player %= NUM_OF_PLAYERS - self.update_table.emit() - time.sleep(0.5) - - def reveal_all_roles(self, partner): - self.players[partner].role = PlayerRole.DEFENDER - self.table_status['defender']['wins'] += self.players[partner].score - for i in range(NUM_OF_PLAYERS): - if self.players[i].role == PlayerRole.UNKNOWN: - self.players[i].role = PlayerRole.ATTACKER - self.table_status['attacker']['wins'] += self.players[i].score - - def reset_game(self): - for player in self.players: - while not player.is_empty(): - self.discard_deck.append(player.remove_card()) - player.score = 0 - player.role = PlayerRole.UNKNOWN - - for i in range(NUM_OF_PLAYERS): - self.update_players_role(i) - self.update_player_wins(i) - self.table_status['defender']['wins'] = 0 - self.table_status['attacker']['wins'] = 0 - self.table_status["played cards"] = [0]*NUM_OF_PLAYERS - self.table_status['round history'] = [] - self.current_round = 0 - self.write_message("", line=1, update_now=False) - self.write_message("", line=2) - self.display_current_player() - print(len(self.discard_deck)) - self.update_table.emit() +from game_consts import GameState, PlayerRole, STARTING_HAND class Player(cards.Deck): @@ -705,67 +181,15 @@ class Player(cards.Deck): return True -class MainPlayer(cards.Deck): +class MainPlayer(Player): def __init__(self, *args, ai_component=None, **kwargs): super().__init__(*args, **kwargs) self.AI = ai_component self.table_status = None # This is found in Table and updated through Table - - def connect_to_table(self, table): - self.table_status = table - - def make_a_bid(self): - pass + self.selectable = True def make_a_play(self): pass - def view_last_round(self): - pass - def check_for_valid_moves(self): - pass - - -class TestView(view.PygView): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.table = Table(0, 0, self.width, self.height, (0, 0, 255)) - self.table.update_table.connect(self.draw_table) - self.draw_table() - - def draw_table(self, **kwargs): - self.screen.blit(self.background, (0, 0)) - self.screen.blit(self.table.background, self.table.get_pos()) - for player in self.table.players: - self.screen.blit(player.deck_surface, player.get_pos()) - for playerzone in self.table.players_playzone: - self.screen.blit(playerzone.deck_surface, playerzone.get_pos()) - pygame.display.flip() - - def run(self): - running = True - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - running = False - if event.key == pygame.K_p: - print('add cards') - pass - - # milliseconds = self.clock.tick(self.fps) - # self.playtime += milliseconds / 1000.0 - - # self.draw_function() - - pygame.quit() - - -if __name__ == '__main__': - test_view = TestView(900, 600, clear_colour=(0, 0, 0)) - test_view.run() diff --git a/table.py b/table.py new file mode 100644 index 0000000..6605691 --- /dev/null +++ b/table.py @@ -0,0 +1,547 @@ +import pygame +import cards +import players +import view +import random +import copy +import time +from signalslot import Signal +from ai_comp import ai +from game_consts import GameState, PlayerRole, STARTING_HAND, NUM_OF_PLAYERS + +VIEW_TRANSPARENT = False # Make the text box not transparent + + +class Table: + """ + A Table is the place where all actions takes place. It is essentially a FSM, doing different + routines at each state. It needs to keep track of the score, roles, the rules, etc. It needs + to ask each player for decisions and respond to them accordingly. The table will also need + to inform any decision to the Main Screen so that it can update the screen to reflect that + change through the use of callbacks (Signal and Slot). This call should be minimised by making + all the changes before calling to update the screen in one go. + + FSM cycles + --- + Preloop - Prepare the cards once + - Initiate Players and connect them to the Table + 1. Shuffle and Deal out cards to Players. + 2a. Detect weak hands and ask for reshuffle. + 2b. Return to (1) if any reshuffle occurs, otherwise proceed. + 3. Bidding round. Randomly pick a starting player, in clockwise manner + ask for a bid until it is valid. + 3b. Proceed only if 3 consecutive skips are detected. + 3c. Ask the winner of the bid a card not in their hand. + 3d. Set up the player roles, trump suit, rounds to win for both side + 3e. Play the game. Start with bid winner if NO TRUMP, otherwise + Starting next to the bid winner. + 4a. With the first player, ask for any card, excluding trump suits if trump + is not broken + 4b. With subsequent players, ask for cards that follow the suit of the first player + , include trump suit if trump is broken. Ask for any card if the player cannot + follow suit. + 4c. Once all 4 players has made valid plays, announce results, update scoring. Announce + player roles if the partner card is played. Break trump if trump is played. + 4d. Repeat 4 until 13 rounds are made. Maybe add early win if confirmed one side wins + 5. Ask for a new game. Go back to 1 if true. + + All played cards go into a hidden discard pile. + + """ + update_table = Signal() + + def __init__(self, x, y, width, height, clear_colour, autoplay=False, view_all_cards=False): + # TODO: Reduce the amount of update_table call + self.x = x + self.y = y + self.width = width + self.height = height + + self.table_font = pygame.font.SysFont("None", 25) + self.player_font = pygame.font.SysFont("None", 25) + + # For gameplay + self.game_state = GameState.DEALING + self.current_round = 0 + self.passes = 0 + self.current_player = 0 + self.first_player = False # This is for bidding purposes + self.players = [] + self.players_playzone = [] + # Table status will be made known to the player by reference + self.table_status = {'played cards': [0, 0, 0, 0], 'leading player': 0, 'trump suit': 1, + 'trump broken': False, 'round history': [], 'bid': 0, 'partner': 0, + 'partner reveal': False, 'defender': {'target': 0, 'wins': 0}, + 'attacker': {'target': 0, 'wins': 0}} + + # Prepare the surfaces for displaying + self.background = pygame.Surface((self.width, self.height)) + self.background.fill(clear_colour) + self.background = self.background.convert() + + # TODO: Update the drawing of the table? + # Prepare the card with dimensions + w_deck = min(self.height, self.width) * 0.18 + l_deck = min(self.width, self.height) * 0.7 + # This is not a deck as it will never be drawn + self.discard_deck = cards.prepare_playing_cards(int(w_deck*0.7), int(w_deck*0.9)) + game_margins = 5 + + # Players' deck positioning + playerx = ((self.width - l_deck)//2, + game_margins, + (self.width - l_deck)//2, + self.width - w_deck - game_margins) + playery = (self.height - w_deck - game_margins, + (self.height - l_deck)//2, + game_margins, + (self.height - l_deck)//2) + h_spacing = 20 + v_spacing = 25 + + # Middle playfield for announcer and player playing deck positioning + playfield_margins = 5 + margins_with_w_deck = w_deck + playfield_margins + game_margins + playfield_x = margins_with_w_deck + playfield_y = margins_with_w_deck + playfield_width = self.width - margins_with_w_deck * 2 + playfield_height = self.height - margins_with_w_deck * 2 + + playdeckx = (playfield_x + (playfield_width - w_deck) / 2, + playfield_x, + playfield_x + (playfield_width - w_deck) / 2, + playfield_x + playfield_width - w_deck) + playdecky = (playfield_y + playfield_height - w_deck, + playfield_y + (playfield_height - w_deck) / 2, + playfield_y, + playfield_y + (playfield_height - w_deck) / 2) + + # Player stats positioning + stats_width = 100 + self.stats_height = 100 + stats_spacing = 10 + self.player_stats_x = (playdeckx[0] - stats_width - stats_spacing, + playdeckx[1], + playdeckx[2] + w_deck + stats_spacing, + playdeckx[3]) + self.player_stats_y = (playdecky[0] + w_deck - self.stats_height, + playdecky[1] - self.stats_height - stats_spacing, + playdecky[2], + playdecky[3] + w_deck + stats_spacing) + + self.player_stats = [[], [], [], []] + + # TODO: change surface to use colorkey, maybe, if the performance is tanked + # Prepare all the player surfaces + for i in range(NUM_OF_PLAYERS): + vert = i % 2 == 1 + spacing = h_spacing + if vert: + spacing = v_spacing + + reveal_mode = cards.DeckReveal.HIDE_ALL + if i == 0 or view_all_cards: + reveal_mode = cards.DeckReveal.SHOW_ALL + self.players.append(players.Player(playerx[i], playery[i], + l_deck, w_deck, + spacing, vert_orientation=vert, + deck_reveal=reveal_mode, flip=(i == 1 or i == 2))) + self.players[i].connect_to_table(self.table_status) + if i > 0: + self.players[i].add_ai(ai.RandomAI(self.table_status)) + + self.players_playzone.append(cards.Deck(playdeckx[i], playdecky[i], + w_deck, w_deck, 0)) + for j in range(3): + surf = pygame.Surface((stats_width, self.stats_height / 3), pygame.SRCALPHA) + rendered_text = self.player_font.render("Player {0:d}".format(i), True, + (255, 0, 255)).convert_alpha() + self.center_text_on_surface(surf, rendered_text, + (255, 255, 255, 255 * VIEW_TRANSPARENT)) + self.player_stats[i].append(surf) + + if autoplay: + self.players[0].add_ai(ai.RandomAI(self.table_status)) + + # Announcer positioning and surface creation + announcer_margins = 5 + announcer_spacing = announcer_margins + w_deck + self.announcer_x = playfield_x + announcer_spacing + self.announcer_y = playfield_y + announcer_spacing + self.announcer_width = playfield_width - 2 * announcer_spacing + self.announcer_height = playfield_height - 2 * announcer_spacing + self.announcer_line = [] + for i in range(3): + surf = pygame.Surface((self.announcer_width, self.announcer_height/3), pygame.SRCALPHA) + self.announcer_line.append(surf) + + self.update_all_players(role=True, wins=True) + + self.write_message("Press P to play!") + + self.ongoing = False + self.require_player_input = False + + + + def center_text_on_surface(self, surf, rendered_text, clear_colour): + line_center = surf.get_rect().center + text_rect = rendered_text.get_rect(center=line_center) + surf.fill(clear_colour) + surf.blit(rendered_text, text_rect) + + def write_message(self, text, delay_time=0.5, line=0, update_now=True): + """ + Write a message into the center board surface (announcer) + :param text: String to be displayed on the center board + :param delay_time: How much delay to put once the string is display + :param line: Which line of the announcer to write to + :return: None + """ + if 0 <= line < len(self.announcer_line): + print(text) + text = text.strip('\n') + rendered_text = self.table_font.render(text, True, (255, 255, 255)).convert_alpha() + self.center_text_on_surface(self.announcer_line[line], rendered_text, + (255, 255, 255, 255*VIEW_TRANSPARENT)) + if update_now: + self.update_table.emit() + time.sleep(delay_time) + + def update_players_role(self, player_num, update_now=True): + self.player_stats[player_num][1].fill((255, 255, 255, 255*VIEW_TRANSPARENT)) + if self.players[player_num].role == PlayerRole.DEFENDER: + rendered_text = self.player_font.render("Defender", True, (0, 64, 192)).convert_alpha() + self.center_text_on_surface(self.player_stats[player_num][1], rendered_text, + (255, 255, 255, 255 * VIEW_TRANSPARENT)) + elif self.players[player_num].role == PlayerRole.ATTACKER: + rendered_text = self.player_font.render("Attacker", True, (192, 0, 0)).convert_alpha() + self.center_text_on_surface(self.player_stats[player_num][1], rendered_text, + (255, 255, 255, 255 * VIEW_TRANSPARENT)) + if update_now: + self.update_table.emit() + + def update_player_wins(self, player_num, update_now=True): + self.player_stats[player_num][2].fill((255, 255, 255, 255*VIEW_TRANSPARENT)) + if self.players[player_num].score > 1: + rendered_text = self.player_font.render("Wins: {0:d}".format(self.players[player_num].score), True, + (255, 255, 255)).convert_alpha() + else: + rendered_text = self.player_font.render("Win: {0:d}".format(self.players[player_num].score), True, + (255, 255, 255)).convert_alpha() + self.center_text_on_surface(self.player_stats[player_num][2], rendered_text, + (255, 255, 255, 255 * VIEW_TRANSPARENT)) + if update_now: + self.update_table.emit() + + def update_all_players(self, role=False, wins=True): + for i in range(NUM_OF_PLAYERS): + if wins: + self.update_player_wins(i, update_now=False) + if role: + self.update_players_role(i, update_now=False) + self.update_table.emit() + + def display_current_player(self, current=-1): + if current >= 0: + print("Player {0:d}\n".format(current)) + for i in range(NUM_OF_PLAYERS): + rendered_text = self.player_font.render("Player {0:d}".format(i), True, + (255, 0, 255)).convert_alpha() + if i == current: + self.center_text_on_surface(self.player_stats[i][0], rendered_text, + (0, 64, 0, 255)) + else: + self.center_text_on_surface(self.player_stats[i][0], rendered_text, + (255, 255, 255, 255 * VIEW_TRANSPARENT)) + + self.update_table.emit() + + def update_team_scores(self): + if self.table_status['partner reveal']: + msg = "Defender: {0:d}/{2:d}, Attacker: {1:d}/{3:d}\n".format(self.table_status['defender']['wins'], + self.table_status['attacker']['wins'], + self.table_status['defender']['target'], + self.table_status['attacker']['target']) + self.write_message(msg, line=2) + else: + msg = "Defender: {0:d}?/{1:d}, Attacker: ?/{2:d}\n".format(self.table_status['defender']['wins'], + self.table_status['defender']['target'], + self.table_status['attacker']['target']) + self.write_message(msg, line=2) + + def get_pos(self): + return self.x, self.y + + def continue_game(self): + """ + This is where the FSM is. State transition should occur here. + What takes place in the state should be in a function. + :return: None + """ + # TODO: Adjust the timing of sleep + if self.game_state == GameState.DEALING: + self.shuffle_and_deal() + self.write_message("Shuffle Complete!") + self.game_state = GameState.POINT_CHECK + + elif self.game_state == GameState.POINT_CHECK: + if self.check_reshuffle(): + self.write_message('Reshuffle Initiated!', line=1) + self.game_state = GameState.ENDING + else: + self.write_message('No Reshuffle needed!') + self.game_state = GameState.BIDDING + self.write_message("Start to Bid") + self.prepare_bidding() + elif self.game_state == GameState.BIDDING: + bid_complete = self.start_bidding() + if bid_complete: + self.game_state = GameState.PLAYING + self.update_all_players(role=True, wins=True) + self.update_team_scores() + + elif self.game_state == GameState.PLAYING: + self.play_a_round() + if self.current_round == 13: + self.write_message("Game Set! Press P to play again!") + self.ongoing = False + self.game_state = GameState.ENDING + else: + self.reset_game() + self.game_state = GameState.DEALING + + def shuffle_and_deal(self): + """ + Shuffle and deal the discard deck to the players, which should have 52 cards. + :return: None + """ + if self.discard_deck: + for i in range(10): + random.shuffle(self.discard_deck) + for player in self.players: + for i in range(STARTING_HAND): + player.add_card(self.discard_deck.pop()) + self.update_table.emit() + + def check_reshuffle(self): + """ + Detect any possible reshuffle request within the players + :return: True if reshuffle requested, else False + """ + print("Player Point Count") + for i, player in enumerate(self.players): + print("Player {0:d}: {1:d}".format(i, player.get_card_points())) + if player.get_card_points() < 4: + self.write_message("Low points detected in Player {0:d}! ".format(i)) + return player.make_decision(self.game_state, 0) + + def prepare_bidding(self): + # Randomly pick a starting player, whom also is the current bid winner + self.current_player = random.randint(1, NUM_OF_PLAYERS) - 1 + print("Starting Player: {0:d}".format(self.current_player)) + self.passes = 0 + self.table_status["bid"] = 11 # Lowest Bid: 1 Club by default + self.first_player = True # Starting bidder "privilege" to raise the starting bid + msg = "Current Bid: {0:d} {1:s}".format(self.table_status["bid"] // 10, + cards.get_suit_string(self.table_status["bid"] % 10)) + self.write_message(msg, line=1, delay_time=0) + self.display_current_player(self.current_player) + msg = 'Bid Leader: Player {0:d}'.format((self.current_player - self.passes - + 1 * (not self.first_player)) % NUM_OF_PLAYERS) + self.write_message(msg, line=2, delay_time=1) + + def start_bidding(self): + """ + The bidding procedure. + :return: Whether bidding is completed + """ + # Highest bid: 7 NoTrump. No further check required + if self.passes < NUM_OF_PLAYERS - 1 and self.table_status["bid"] < 75: + player_bid = self.players[self.current_player].make_decision(self.game_state, 0) + if not player_bid: + if not self.first_player: # Starting bidder pass do not count at the start + self.passes += 1 + else: + self.table_status["bid"] = player_bid + self.passes = 0 + + if self.table_status["bid"] < 75: + self.current_player += 1 + self.current_player %= NUM_OF_PLAYERS + msg = "Current Bid: {0:d} {1:s}".format(self.table_status["bid"] // 10, + cards.get_suit_string(self.table_status["bid"] % 10)) + self.write_message(msg, line=1, update_now=False) + msg = 'Bid Leader: Player {0:d}'.format((self.current_player - self.passes + - 1 * (not self.first_player)) % NUM_OF_PLAYERS) + self.write_message(msg, line=2, update_now=False) + self.display_current_player(self.current_player) + if self.first_player: + self.first_player = False + time.sleep(0.5) + return False + else: + self.write_message("Player {0:d} is the bid winner!".format(self.current_player), delay_time=1) + msg = "Player {0:d} is calling a partner...".format(self.current_player) + self.write_message(msg, delay_time=1) + self.display_current_player(self.current_player) + # Ask for the partner card + self.table_status["partner"] = self.players[self.current_player].make_decision(self.game_state, 1) + + # Setup the table status before the play starts + self.table_status['partner reveal'] = False + self.table_status["trump suit"] = self.table_status["bid"] % 10 + self.table_status["trump broken"] = False + self.table_status['played cards'] = [0, 0, 0, 0] + if self.table_status['trump suit'] == 5: + self.table_status["leading player"] = self.current_player + else: + self.table_status["leading player"] = (self.current_player + 1) % NUM_OF_PLAYERS + self.table_status['defender']['target'] = self.table_status["bid"] // 10 + 6 + self.table_status['attacker']['target'] = 14 - self.table_status['defender']['target'] + + # Set the roles of the players + self.players[self.current_player].role = PlayerRole.DEFENDER + + self.write_message('Bidding Complete', delay_time=0) + msg = 'Trump: {1:s}, Partner: {0:s}'.format(cards.get_card_string(self.table_status["partner"]), + cards.get_suit_string(self.table_status['trump suit'])) + self.write_message(msg, line=1, delay_time=1) + return True + + def play_a_round(self): + """ + Ask each player to play a valid card and determine the winner of the round + :return: None + """ + if not any(self.table_status["played cards"]): + # Leading player starts with the leading card, which determines the leading suit + self.current_player = self.table_status['leading player'] + self.display_current_player(self.current_player) + card = self.players[self.current_player].make_decision(self.game_state, 0) + self.table_status["played cards"][self.current_player] = card + self.players_playzone[self.current_player].add_card(card) + elif not all(self.table_status["played cards"]): + # Subsequent player make their plays, following suit if possible + self.display_current_player(self.current_player) + print("Player {0:d}\n".format(self.current_player)) + card = self.players[self.current_player].make_decision(self.game_state, 1) + self.players_playzone[self.current_player].add_card(card) + self.table_status["played cards"][self.current_player] = card + else: + # Once all player played, find out who wins + leading_card = self.table_status["played cards"][self.table_status['leading player']] + card_suits = [card.suit() for card in self.table_status["played cards"]] + card_nums = [card.number() for card in self.table_status["played cards"]] + follow_suits = [suit == leading_card.suit() for suit in card_suits] + trumps = [suit == self.table_status['trump suit'] for suit in card_suits] + trump_played = any(trumps) + + # Break trump if the trump suit is played + if not self.table_status['trump broken']: + if trump_played: + self.table_status['trump broken'] = True + self.write_message("Trump Broken!", delay_time=1) + + # Determine which players to check for winner, and determine winner + valid_nums = [card_nums[i] * ((follow_suits[i] and not trump_played) or trumps[i]) + for i in range(NUM_OF_PLAYERS)] + winning_player = valid_nums.index(max(valid_nums)) + self.write_message("Player {0:d} wins!\n".format(winning_player), delay_time=1) + self.players[winning_player].score += 1 + self.update_player_wins(winning_player) + + # Clean up the cards, update score, set the next leading player, update round history + for deck in self.players_playzone: + self.discard_deck.append(deck.remove_card()) + + if self.players[winning_player].role == PlayerRole.DEFENDER: + self.table_status['defender']['wins'] += 1 + elif self.players[winning_player].role == PlayerRole.ATTACKER: + self.table_status['attacker']['wins'] += 1 + + self.table_status['leading player'] = winning_player + self.table_status['round history'].append(copy.copy(self.table_status["played cards"])) + self.update_team_scores() + self.table_status["played cards"] = [0]*NUM_OF_PLAYERS + self.current_round += 1 + self.update_table.emit() + return + + if not self.table_status['partner reveal']: + if card.value == self.table_status['partner']: + self.table_status['partner reveal'] = True + self.write_message("Partner Revealed!", delay_time=1) + self.reveal_all_roles(self.current_player) + self.update_all_players(role=True, wins=False) + + self.current_player += 1 + self.current_player %= NUM_OF_PLAYERS + self.update_table.emit() + time.sleep(0.5) + + def reveal_all_roles(self, partner): + self.players[partner].role = PlayerRole.DEFENDER + self.table_status['defender']['wins'] += self.players[partner].score + for i in range(NUM_OF_PLAYERS): + if self.players[i].role == PlayerRole.UNKNOWN: + self.players[i].role = PlayerRole.ATTACKER + self.table_status['attacker']['wins'] += self.players[i].score + + def reset_game(self): + for player in self.players: + while not player.is_empty(): + self.discard_deck.append(player.remove_card()) + player.score = 0 + player.role = PlayerRole.UNKNOWN + + for i in range(NUM_OF_PLAYERS): + self.update_players_role(i) + self.update_player_wins(i) + self.table_status['defender']['wins'] = 0 + self.table_status['attacker']['wins'] = 0 + self.table_status["played cards"] = [0]*NUM_OF_PLAYERS + self.table_status['round history'] = [] + self.current_round = 0 + self.write_message("", line=1, update_now=False) + self.write_message("", line=2) + self.display_current_player() + print(len(self.discard_deck)) + self.update_table.emit() + +class TestView(view.PygView): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.table = Table(0, 0, self.width, self.height, (0, 0, 255)) + self.table.update_table.connect(self.draw_table) + self.draw_table() + + def draw_table(self, **kwargs): + self.screen.blit(self.background, (0, 0)) + self.screen.blit(self.table.background, self.table.get_pos()) + for player in self.table.players: + self.screen.blit(player.deck_surface, player.get_pos()) + for playerzone in self.table.players_playzone: + self.screen.blit(playerzone.deck_surface, playerzone.get_pos()) + pygame.display.flip() + + def run(self): + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + if event.key == pygame.K_p: + print('add cards') + pass + + pygame.quit() + + +if __name__ == '__main__': + test_view = TestView(900, 600, clear_colour=(0, 0, 0)) + test_view.run()