538 lines
18 KiB
Python
538 lines
18 KiB
Python
"""
|
|
This module contains the Card class and the Deck class
|
|
Card contains the information of a playing card
|
|
Deck is used as a Card container
|
|
"""
|
|
import pygame
|
|
import view
|
|
import os
|
|
import threading
|
|
import random
|
|
from enum import Enum
|
|
|
|
CLEARCOLOUR = (0, 99, 0)
|
|
|
|
# LUT for mapping int to cards symbols
|
|
CARDS_SYMBOLS = {14: "A", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7",
|
|
8: "8", 9: "9", 10: "10", 11: "J", 12: "Q", 13: "K",
|
|
100: "Clubs", 200: "Diamonds", 300: "Hearts", 400: "Spades", 500: "No Trump",
|
|
}
|
|
|
|
INPUT_SYMBOLS = {"c": 100, "d": 200, "h": 300, "s": 400, "n": 500, "a": 14,
|
|
"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
|
|
"8": 8, "9": 9, "10": 10, "j": 11, "q": 12, "k": 13,
|
|
}
|
|
BID_SYMBOLS = {"c": 100, "d": 200, "h": 300, "s": 400, "n": 500}
|
|
|
|
|
|
class DeckReveal(Enum):
|
|
SHOW_ALL = 1
|
|
HIDE_ALL = 2
|
|
ANY = 3
|
|
|
|
|
|
class DeckSort(Enum):
|
|
ASCENDING = 1
|
|
DESCENDING = 2
|
|
NOSORT = 3
|
|
|
|
|
|
class Card(pygame.sprite.Sprite):
|
|
|
|
def __init__(self, x, y, width, height, value, hidden=False, image_data=None,
|
|
backimage_data=None, parent=None, angle=0):
|
|
super().__init__()
|
|
self.x = x
|
|
self.y = y
|
|
|
|
self.width = width
|
|
self.height = height
|
|
self.angle = angle
|
|
|
|
self.value = value
|
|
self.hidden = hidden
|
|
self.parent = parent
|
|
|
|
self.original_image = None
|
|
self.original_backimage = None
|
|
self.image = None
|
|
self.backimage = None
|
|
self.rect = None
|
|
|
|
self.add_image(image_data, backimage_data)
|
|
self._layer = 0
|
|
|
|
def add_image(self, image, backimage=None):
|
|
if image:
|
|
self.original_image = image
|
|
self.original_image = pygame.transform.scale(self.original_image, (self.width, self.height))
|
|
self.image = pygame.transform.rotate(self.original_image, self.angle)
|
|
|
|
self.rect = self.image.get_rect()
|
|
|
|
if backimage:
|
|
self.original_backimage = backimage
|
|
self.original_backimage = pygame.transform.scale(self.original_backimage, (self.width, self.height))
|
|
self.backimage = pygame.transform.rotate(self.original_backimage, self.angle)
|
|
|
|
def set_angle(self, angle):
|
|
if self.original_image:
|
|
self.image = pygame.transform.rotate(self.original_image, angle)
|
|
|
|
if self.original_backimage:
|
|
self.backimage = pygame.transform.rotate(self.original_backimage, angle)
|
|
|
|
self.angle = angle
|
|
|
|
self.rect = self.image.get_rect()
|
|
|
|
def get_pos(self):
|
|
return self.x, self.y
|
|
|
|
def set_pos(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
self.rect.x = x
|
|
self.rect.y = y
|
|
|
|
def suit(self):
|
|
return get_card_suit(self.value)
|
|
|
|
def number(self):
|
|
return get_card_number(self.value)
|
|
|
|
def value_info(self):
|
|
return self.suit(), self.number()
|
|
|
|
|
|
class Deck():
|
|
|
|
def __init__(self, x, y, length, width, spacing, deck_reveal=DeckReveal.SHOW_ALL,
|
|
sort_order=DeckSort.ASCENDING, vert_orientation=False, draw_from_last=False, selectable=False,
|
|
flip=False):
|
|
super().__init__()
|
|
self.x = x
|
|
self.y = y
|
|
|
|
self.length = length
|
|
self.width = width
|
|
self.default_spacing = spacing
|
|
|
|
self.deck_reveal = deck_reveal
|
|
self.vert_orientation = vert_orientation
|
|
self.flip = flip
|
|
|
|
self.draw_from_last = draw_from_last
|
|
self.sort_order = sort_order
|
|
self.selectable = selectable
|
|
self.selected_card = -1
|
|
self.prev_selected = [-1, -1, -1]
|
|
|
|
self.cards = []
|
|
self.line_width = 5
|
|
if self.is_horizontal():
|
|
self.background = pygame.Surface((self.length, self.width))
|
|
self.background.fill(CLEARCOLOUR)
|
|
pygame.draw.rect(self.background, (255, 255, 255), self.background.get_rect(), self.line_width)
|
|
|
|
self.background = self.background.convert()
|
|
self.background.set_colorkey(CLEARCOLOUR)
|
|
self.deck_surface = self.background.copy()
|
|
self.rect = pygame.rect.Rect(x, y, length, width)
|
|
else:
|
|
self.background = pygame.Surface((self.width, self.length))
|
|
self.background.fill(CLEARCOLOUR)
|
|
pygame.draw.rect(self.background, (255, 255, 255), self.background.get_rect(), self.line_width)
|
|
self.background = self.background.convert()
|
|
self.background.set_colorkey(CLEARCOLOUR)
|
|
self.deck_surface = self.background.copy()
|
|
self.rect = pygame.rect.Rect(x, y, width, length)
|
|
|
|
self._layer = 1
|
|
|
|
def set_selectable(self, state):
|
|
self.selectable = state
|
|
|
|
def add_card(self, card, position=0):
|
|
# TODO: Add a function to add additional cards, to optimise number of recalculations
|
|
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:
|
|
self.cards.append(card)
|
|
else:
|
|
if self.sort_order == DeckSort.NOSORT:
|
|
self.cards.insert(position, card)
|
|
else:
|
|
if self.sort_order == DeckSort.DESCENDING:
|
|
self.cards.reverse()
|
|
|
|
if card.value < self.cards[0].value:
|
|
self.cards.insert(0, card)
|
|
elif card.value > self.cards[-1].value:
|
|
self.cards.append(card)
|
|
else:
|
|
lo = 0
|
|
hi = number_of_cards
|
|
|
|
while abs(lo-hi) != 1:
|
|
pos = (lo + hi) // 2
|
|
if card.value > self.cards[pos].value:
|
|
lo = pos
|
|
else:
|
|
hi = pos
|
|
|
|
self.cards.insert(hi, card)
|
|
|
|
if self.sort_order == DeckSort.DESCENDING:
|
|
self.cards.reverse()
|
|
|
|
self.set_card_positions()
|
|
|
|
def set_card_positions(self):
|
|
"""
|
|
Calculate the card positions, given the spacing.
|
|
If there is too many cards for the given spacing, the spacing is adjusted to fit.
|
|
:return: None
|
|
"""
|
|
number_of_cards = len(self.cards)
|
|
|
|
if number_of_cards > 0:
|
|
total_card_length = self.cards[0].width + self.default_spacing * (number_of_cards - 1)
|
|
if total_card_length <= self.length - 2 * self.line_width:
|
|
start_point = (self.length - total_card_length) / 2
|
|
spacing = self.default_spacing
|
|
else:
|
|
start_point = self.line_width
|
|
spacing = (self.length - self.cards[0].width - 2*self.line_width)/(number_of_cards-1)
|
|
|
|
for (i, card) in enumerate(self.cards):
|
|
x = start_point + spacing * i
|
|
y = (self.width - self.cards[0].height) / 2
|
|
if self.is_horizontal():
|
|
card.set_pos(x, y)
|
|
else:
|
|
card.set_pos(y, x)
|
|
|
|
self.update_deck_display()
|
|
|
|
def update_deck_display(self):
|
|
"""
|
|
Blits the cards onto the deck surface. Called when the deck is modified.
|
|
:return: None
|
|
"""
|
|
self.deck_surface.fill(CLEARCOLOUR)
|
|
self.deck_surface.blit(self.background, (0, 0))
|
|
if not self.is_empty():
|
|
cards_to_draw = self.cards
|
|
if self.draw_from_last:
|
|
cards_to_draw = reversed(cards_to_draw)
|
|
|
|
for i, card in enumerate(cards_to_draw):
|
|
selected = (i == self.selected_card)
|
|
image_to_draw = card.image
|
|
|
|
if self.deck_reveal == DeckReveal.HIDE_ALL:
|
|
image_to_draw = card.backimage
|
|
|
|
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,
|
|
card.y - selected * card.y * 0.5 *
|
|
(-1)**self.flip * (not self.vert_orientation)))
|
|
|
|
def remove_card(self, pos=-1):
|
|
"""
|
|
Remove a card from the deck.
|
|
Check first if the card is in deck to get the position
|
|
:param pos: Position of the card to be removed
|
|
:return: Card
|
|
"""
|
|
if not self.is_empty():
|
|
if pos < 0:
|
|
card = self.cards.pop()
|
|
else:
|
|
card = self.cards.pop(pos)
|
|
self.set_card_positions()
|
|
return card
|
|
return None
|
|
|
|
def remove_selected_card(self):
|
|
if self.selected_card >= 0:
|
|
card = self.remove_card(self.selected_card)
|
|
self.deselect_card()
|
|
return card
|
|
|
|
def is_horizontal(self):
|
|
return not self.vert_orientation
|
|
|
|
def is_empty(self):
|
|
return len(self.cards) == 0
|
|
|
|
def get_pos(self):
|
|
return self.x, self.y
|
|
|
|
def get_deck_values(self):
|
|
values = []
|
|
for card in self.cards:
|
|
values.append(card.value)
|
|
return values
|
|
|
|
def check_card_in(self, value):
|
|
card_values = self.get_deck_values()
|
|
if value in card_values:
|
|
return True, card_values.index(value)
|
|
return False, -1
|
|
|
|
def record_selected_history(self):
|
|
self.prev_selected = self.prev_selected[1:]
|
|
self.prev_selected.append(self.selected_card)
|
|
|
|
def deselect_card(self):
|
|
self.selected_card = -1
|
|
self.record_selected_history()
|
|
self.update_deck_display()
|
|
|
|
def get_selected_card(self, pos, double_clicking=False):
|
|
"""
|
|
Get the selected card based on the mouse pos, offset to give the relative position in the deck.
|
|
The selected card position is stored in the deck
|
|
:param pos: Absolute position of the mouse
|
|
:return: bool: whether the card selected is the same as before
|
|
"""
|
|
if self.selectable:
|
|
double_select = False
|
|
relative_pos_x = pos[0] - self.x
|
|
relative_pos_y = pos[1] - self.y
|
|
mouse_pos = (relative_pos_x, relative_pos_y)
|
|
self.selected_card = -1
|
|
if not self.draw_from_last:
|
|
for i, card in enumerate(reversed(self.cards)):
|
|
if card.rect.collidepoint(mouse_pos):
|
|
self.selected_card = len(self.cards) - 1 - i
|
|
break
|
|
else:
|
|
for i, card in enumerate(self.cards):
|
|
if card.rect.collidepoint(mouse_pos):
|
|
self.selected_card = i
|
|
break
|
|
|
|
if self.prev_selected[-1] == self.selected_card:
|
|
if not double_clicking:
|
|
self.selected_card = -1
|
|
|
|
self.record_selected_history()
|
|
self.update_deck_display()
|
|
|
|
selected_history = [sel for sel in self.prev_selected if sel >= 0]
|
|
|
|
return (len(selected_history) == 2 and self.prev_selected.count(self.selected_card) == 2
|
|
and self.selected_card >= 0) and double_clicking
|
|
return False
|
|
|
|
|
|
class SpriteSheet(object):
|
|
def __init__(self, filename):
|
|
try:
|
|
self.sheet = pygame.image.load(filename).convert()
|
|
except pygame.error:
|
|
print('Unable to load spritesheet image:', filename)
|
|
raise Exception("Cannot load image")
|
|
|
|
# Load a specific image from a specific rectangle
|
|
def image_at(self, rectangle, colorkey=None):
|
|
"""
|
|
Loads image from x,y,x+width,y+height
|
|
:param rectangle: tuple: (x, y, width, height)
|
|
:param colorkey: tuple (R,G,B), the transparency colour
|
|
:return: the image
|
|
"""
|
|
rect = pygame.Rect(rectangle)
|
|
image = pygame.Surface(rect.size).convert()
|
|
image.blit(self.sheet, (0, 0), rect)
|
|
if colorkey is not None:
|
|
if colorkey is -1:
|
|
colorkey = image.get_at((0, 0))
|
|
image.set_colorkey(colorkey, pygame.RLEACCEL)
|
|
return image
|
|
|
|
def images_at(self, rects, colorkey = None):
|
|
"""
|
|
Loads multiple images, supply a list of coordinates
|
|
:param rects:
|
|
:param colorkey:
|
|
:return:
|
|
"""
|
|
return [self.image_at(rect, colorkey) for rect in rects]
|
|
|
|
def load_strip(self, rect, image_count, colorkey = None):
|
|
"""
|
|
Loads a strip of images and returns them as a list
|
|
:param rect:
|
|
:param image_count:
|
|
:param colorkey:
|
|
:return:
|
|
"""
|
|
tups = [(rect[0]+rect[2]*x, rect[1], rect[2], rect[3])
|
|
for x in range(image_count)]
|
|
return self.images_at(tups, colorkey)
|
|
|
|
|
|
DATA_FOLDER = "resource"
|
|
|
|
|
|
def prepare_playing_cards(display_w, display_h):
|
|
"""
|
|
Create the 52 playing cards. Should be called only once.
|
|
:param int display_w: Card width
|
|
:param int display_h: Card Height
|
|
:return: The list of 52 Cards
|
|
:rtype: List of <Cards>
|
|
"""
|
|
card_sprites = SpriteSheet(os.path.join(DATA_FOLDER, 'card_spritesheet.png'))
|
|
all_cards = []
|
|
offset = 0
|
|
spacing = 0
|
|
width = 71
|
|
height = 96
|
|
suits_position = [2, 3, 1, 0]
|
|
card_backimg = card_sprites.image_at((offset + (width+spacing)*9, 5*(height+spacing) + offset, width, height))
|
|
for i in range(4):
|
|
y = suits_position[i] * (height+spacing) + offset
|
|
for j in range(13):
|
|
if j < 12:
|
|
x = offset + (width+spacing)*(j+1)
|
|
else:
|
|
x = offset
|
|
card_img = card_sprites.image_at((x, y, width, height))
|
|
all_cards.append(Card(0, 0, display_w, display_h, (i+1)*100 + j+2,
|
|
image_data=card_img, backimage_data=card_backimg))
|
|
|
|
return all_cards
|
|
|
|
|
|
def card_check(value):
|
|
return 1 <= get_card_suit(value) <= 4 \
|
|
and 2 <= get_card_number(value) <= 14
|
|
|
|
|
|
def get_card_suit(value):
|
|
return value // 100
|
|
|
|
|
|
def get_card_number(value):
|
|
return value % 100
|
|
|
|
|
|
def get_card_string(value):
|
|
suit = get_card_suit(value) * 100
|
|
num = get_card_number(value)
|
|
return CARDS_SYMBOLS[num] + ' ' + CARDS_SYMBOLS[suit]
|
|
|
|
|
|
def get_suit_string(value):
|
|
return CARDS_SYMBOLS[value*100]
|
|
|
|
|
|
def convert_input_string(string):
|
|
string = string.lower()
|
|
try:
|
|
if string[0:-1].isalnum() and string[-1].isalpha():
|
|
return INPUT_SYMBOLS[string[0:-1]] + INPUT_SYMBOLS[string[-1]]
|
|
return -1
|
|
except KeyError:
|
|
return -1
|
|
|
|
|
|
def convert_bid_string(string):
|
|
string = string.lower()
|
|
try:
|
|
if len(string)>1 and string[0].isdecimal() and string[1].isalpha():
|
|
return int(string[0])*10 + BID_SYMBOLS[string[1]]//100
|
|
return -1
|
|
except KeyError:
|
|
return -1
|
|
|
|
|
|
class TestScreen(view.PygView):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
all_cards = prepare_playing_cards(50, 75)
|
|
self.test_card = all_cards[15]
|
|
self.test_decks = []
|
|
self.test_decks.append(Deck(100, 100, 200, 100, 25, selectable=True))
|
|
self.test_decks.append(Deck(500, 100, 200, 100, 25, selectable=True, vert_orientation=True))
|
|
self.test_decks[0].add_card(all_cards[0])
|
|
self.test_decks[0].add_card(all_cards[13])
|
|
self.test_decks[0].add_card(all_cards[35])
|
|
self.test_decks[0].add_card(all_cards[51])
|
|
self.test_decks[1].add_card(all_cards[20])
|
|
self.test_decks[1].add_card(all_cards[21])
|
|
self.test_decks[1].add_card(all_cards[5])
|
|
self.test_decks[1].add_card(all_cards[14])
|
|
|
|
self.left_mouse_down = False
|
|
self.double_clicking = False
|
|
self.double_click_event = pygame.USEREVENT + 1
|
|
|
|
def draw_function(self):
|
|
self.screen.blit(self.test_card.image, self.test_card.get_pos())
|
|
for deck in self.test_decks:
|
|
self.screen.blit(deck.deck_surface, deck.get_pos())
|
|
|
|
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
|
|
|
|
mouse_clicks = event.type == pygame.MOUSEBUTTONDOWN
|
|
if self.left_mouse_down and not mouse_clicks:
|
|
print('mouse click')
|
|
mouse_pos = pygame.mouse.get_pos()
|
|
for deck in self.test_decks:
|
|
if deck.rect.collidepoint(mouse_pos):
|
|
reselect = deck.get_selected_card(mouse_pos, self.double_clicking)
|
|
print('a', reselect)
|
|
if self.double_clicking:
|
|
pygame.time.set_timer(self.double_click_event, 0)
|
|
print('Double clicked')
|
|
if reselect:
|
|
deck.remove_selected_card()
|
|
self.double_clicking = False
|
|
else:
|
|
self.double_clicking = True
|
|
pygame.time.set_timer(self.double_click_event, 200)
|
|
|
|
if event.type == self.double_click_event:
|
|
pygame.time.set_timer(self.double_click_event, 0)
|
|
self.double_clicking = False
|
|
print('double click disabled')
|
|
|
|
self.left_mouse_down = mouse_clicks
|
|
|
|
self.draw_function()
|
|
|
|
pygame.display.flip()
|
|
self.screen.blit(self.background, (0, 0))
|
|
|
|
pygame.quit()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_view = TestScreen(640, 400, clear_colour=(0, 0, 0))
|
|
test_view.run()
|