mirror of
https://github.com/pythonguis/pythonguis-examples.git
synced 2025-01-13 16:42:55 +08:00
665 lines
18 KiB
Python
665 lines
18 KiB
Python
from PyQt5.QtGui import *
|
|
from PyQt5.QtWidgets import *
|
|
from PyQt5.QtCore import *
|
|
|
|
import os
|
|
import random
|
|
|
|
WINDOW_SIZE = 840, 600
|
|
|
|
CARD_DIMENSIONS = QSize(80, 116)
|
|
CARD_RECT = QRect(0, 0, 80, 116)
|
|
CARD_SPACING_X = 110
|
|
CARD_BACK = QImage(os.path.join('images', 'back.png'))
|
|
|
|
DEAL_RECT = QRect(30, 30, 110, 140)
|
|
|
|
OFFSET_X = 50
|
|
OFFSET_Y = 50
|
|
WORK_STACK_Y = 200
|
|
|
|
SIDE_FACE = 0
|
|
SIDE_BACK = 1
|
|
|
|
SLIPPING_OFFSET = 20
|
|
DEAL_N = 3
|
|
|
|
# We store cards as numbers 1-13, since we only need
|
|
# to know their order for solitaire.
|
|
SUITS = ["C", "S", "H", "D"]
|
|
|
|
|
|
class Signals(QObject):
|
|
complete = pyqtSignal()
|
|
clicked = pyqtSignal()
|
|
doubleclicked = pyqtSignal()
|
|
|
|
|
|
class Card(QGraphicsPixmapItem):
|
|
|
|
def __init__(self, value, suit, *args, **kwargs):
|
|
super(Card, self).__init__(*args, **kwargs)
|
|
|
|
self.signals = Signals()
|
|
|
|
self.stack = None # Stack this card currently is in.
|
|
self.child = None # Card stacked on this one (for work deck).
|
|
|
|
# Store the value & suit of the cards internal to it.
|
|
self.value = value
|
|
self.suit = suit
|
|
self.side = None
|
|
|
|
# For end animation only.
|
|
self.vector = None
|
|
|
|
# Cards have no internal transparent areas, so we can use this faster method.
|
|
self.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
|
|
self.setFlag(QGraphicsItem.ItemIsMovable)
|
|
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
|
|
|
|
self.load_images()
|
|
|
|
def load_images(self):
|
|
self.face = QPixmap(
|
|
os.path.join('cards', '%s%s.png' % (self.value, self.suit))
|
|
)
|
|
|
|
self.back = QPixmap(
|
|
os.path.join('images', 'back.png')
|
|
)
|
|
|
|
def turn_face_up(self):
|
|
self.side = SIDE_FACE
|
|
self.setPixmap(self.face)
|
|
|
|
def turn_back_up(self):
|
|
self.side = SIDE_BACK
|
|
self.setPixmap(self.back)
|
|
|
|
@property
|
|
def is_face_up(self):
|
|
return self.side == SIDE_FACE
|
|
|
|
@property
|
|
def color(self):
|
|
return 'r' if self.suit in ('H', 'D') else 'b'
|
|
|
|
def mousePressEvent(self, e):
|
|
if not self.is_face_up and self.stack.cards[-1] == self:
|
|
self.turn_face_up() # We can do this without checking.
|
|
e.accept()
|
|
return
|
|
|
|
if self.stack and not self.stack.is_free_card(self):
|
|
e.ignore()
|
|
return
|
|
|
|
self.stack.activate()
|
|
|
|
e.accept()
|
|
|
|
super(Card, self).mouseReleaseEvent(e)
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
self.stack.deactivate()
|
|
|
|
items = self.collidingItems()
|
|
if items:
|
|
# Find the topmost item from a different stack:
|
|
for item in items:
|
|
if ((isinstance(item, Card) and item.stack != self.stack) or
|
|
(isinstance(item, StackBase) and item != self.stack)):
|
|
|
|
if item.stack.is_valid_drop(self):
|
|
# Remove card + all children from previous stack, add to the new.
|
|
# Note: the only place there will be children is on a workstack.
|
|
cards = self.stack.remove_card(self)
|
|
item.stack.add_cards(cards)
|
|
break
|
|
|
|
# Refresh this card's stack, pulling it back if it was dropped.
|
|
self.stack.update()
|
|
|
|
super(Card, self).mouseReleaseEvent(e)
|
|
|
|
def mouseDoubleClickEvent(self, e):
|
|
if self.stack.is_free_card(self):
|
|
self.signals.doubleclicked.emit()
|
|
e.accept()
|
|
|
|
super(Card, self).mouseDoubleClickEvent(e)
|
|
|
|
|
|
class StackBase(QGraphicsRectItem):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(StackBase, self).__init__(*args, **kwargs)
|
|
|
|
self.setRect(QRectF(CARD_RECT))
|
|
self.setZValue(-1)
|
|
|
|
# Cards on this deck, in order.
|
|
self.cards = []
|
|
|
|
# Store a self ref, so the collision logic can handle cards and
|
|
# stacks with the same approach.
|
|
self.stack = self
|
|
self.setup()
|
|
self.reset()
|
|
|
|
def setup(self):
|
|
pass
|
|
|
|
def reset(self):
|
|
self.remove_all_cards()
|
|
|
|
def update(self):
|
|
for n, card in enumerate(self.cards):
|
|
card.setPos( self.pos() + QPointF(n * self.offset_x, n * self.offset_y))
|
|
card.setZValue(n)
|
|
|
|
def activate(self):
|
|
pass
|
|
|
|
def deactivate(self):
|
|
pass
|
|
|
|
def add_card(self, card, update=True):
|
|
card.stack = self
|
|
self.cards.append(card)
|
|
if update:
|
|
self.update()
|
|
|
|
def add_cards(self, cards):
|
|
for card in cards:
|
|
self.add_card(card, update=False)
|
|
self.update()
|
|
|
|
def remove_card(self, card):
|
|
card.stack = None
|
|
self.cards.remove(card)
|
|
self.update()
|
|
return [card] # Returns a list, as WorkStack must return children
|
|
|
|
def remove_all_cards(self):
|
|
for card in self.cards[:]:
|
|
card.stack = None
|
|
self.cards = []
|
|
|
|
def take_top_card(self):
|
|
try:
|
|
card = self.cards[-1]
|
|
self.remove_card(card)
|
|
return card
|
|
except IndexError:
|
|
pass
|
|
|
|
def is_valid_drop(self, card):
|
|
return True
|
|
|
|
def is_free_card(self, card):
|
|
return False
|
|
|
|
|
|
class DeckStack(StackBase):
|
|
|
|
offset_x = -0.2
|
|
offset_y = -0.3
|
|
|
|
restack_counter = 0
|
|
|
|
def reset(self):
|
|
super(DeckStack, self).reset()
|
|
self.restack_counter = 0
|
|
self.set_color(Qt.green)
|
|
|
|
def stack_cards(self, cards):
|
|
for card in cards:
|
|
self.add_card(card)
|
|
card.turn_back_up()
|
|
|
|
def can_restack(self, n_rounds=3):
|
|
return n_rounds is None or self.restack_counter < n_rounds-1
|
|
|
|
def update_stack_status(self, n_rounds):
|
|
if not self.can_restack(n_rounds):
|
|
self.set_color(Qt.red)
|
|
else:
|
|
# We only need this if players change the round number during a game.
|
|
self.set_color(Qt.green)
|
|
|
|
def restack(self, fromstack):
|
|
self.restack_counter += 1
|
|
|
|
# We need to slice as we're adding to the list, reverse to stack back
|
|
# in the original order.
|
|
for card in fromstack.cards[::-1]:
|
|
fromstack.remove_card(card)
|
|
self.add_card(card)
|
|
card.turn_back_up()
|
|
|
|
def set_color(self, color):
|
|
color = QColor(color)
|
|
color.setAlpha(50)
|
|
brush = QBrush(color)
|
|
self.setBrush(brush)
|
|
self.setPen(QPen(Qt.NoPen))
|
|
|
|
def is_valid_drop(self, card):
|
|
return False
|
|
|
|
|
|
class DealStack(StackBase):
|
|
|
|
offset_x = 20
|
|
offset_y = 0
|
|
|
|
spread_from = 0
|
|
|
|
def setup(self):
|
|
self.setPen(QPen(Qt.NoPen))
|
|
color = QColor(Qt.black)
|
|
color.setAlpha(50)
|
|
brush = QBrush(color)
|
|
self.setBrush(brush)
|
|
|
|
def reset(self):
|
|
super(DealStack, self).reset()
|
|
self.spread_from = 0 # Card index to start spreading cards out.
|
|
|
|
def is_valid_drop(self, card):
|
|
return False
|
|
|
|
def is_free_card(self, card):
|
|
return card == self.cards[-1]
|
|
|
|
def update(self):
|
|
# Only spread the top 3 cards
|
|
offset_x = 0
|
|
for n, card in enumerate(self.cards):
|
|
card.setPos(self.pos() + QPointF(offset_x, 0))
|
|
card.setZValue(n)
|
|
|
|
if n >= self.spread_from:
|
|
offset_x = offset_x + self.offset_x
|
|
|
|
|
|
class WorkStack(StackBase):
|
|
|
|
offset_x = 0
|
|
offset_y = 15
|
|
offset_y_back = 5
|
|
|
|
def setup(self):
|
|
self.setPen(QPen(Qt.NoPen))
|
|
color = QColor(Qt.black)
|
|
color.setAlpha(50)
|
|
brush = QBrush(color)
|
|
self.setBrush(brush)
|
|
|
|
def activate(self):
|
|
# Raise z-value of this stack so children float above all other cards.
|
|
self.setZValue(1000)
|
|
|
|
def deactivate(self):
|
|
self.setZValue(-1)
|
|
|
|
def is_valid_drop(self, card):
|
|
if not self.cards:
|
|
return True
|
|
|
|
if (card.color != self.cards[-1].color and
|
|
card.value == self.cards[-1].value -1):
|
|
return True
|
|
|
|
return False
|
|
|
|
def is_free_card(self, card):
|
|
return card.is_face_up #self.cards and card == self.cards[-1]
|
|
|
|
def add_card(self, card, update=True):
|
|
if self.cards:
|
|
card.setParentItem(self.cards[-1])
|
|
else:
|
|
card.setParentItem(self)
|
|
|
|
super(WorkStack, self).add_card(card, update=update)
|
|
|
|
def remove_card(self, card):
|
|
index = self.cards.index(card)
|
|
self.cards, cards = self.cards[:index], self.cards[index:]
|
|
|
|
for card in cards:
|
|
# Remove card and all children, returning a list of cards removed in order.
|
|
card.setParentItem(None)
|
|
card.stack = None
|
|
|
|
self.update()
|
|
return cards
|
|
|
|
def remove_all_cards(self):
|
|
for card in self.cards[:]:
|
|
card.setParentItem(None)
|
|
card.stack = None
|
|
self.cards = []
|
|
|
|
def update(self):
|
|
self.stack.setZValue(-1) # Reset this stack the the background.
|
|
# Only spread the top 3 cards
|
|
offset_y = 0
|
|
for n, card in enumerate(self.cards):
|
|
card.setPos(QPointF(0, offset_y))
|
|
|
|
if card.is_face_up:
|
|
offset_y = self.offset_y
|
|
else:
|
|
offset_y = self.offset_y_back
|
|
|
|
|
|
class DropStack(StackBase):
|
|
|
|
offset_x = -0.2
|
|
offset_y = -0.3
|
|
|
|
suit = None
|
|
value = 0
|
|
|
|
def setup(self):
|
|
self.signals = Signals()
|
|
color = QColor(Qt.blue)
|
|
color.setAlpha(50)
|
|
pen = QPen(color)
|
|
pen.setWidth(5)
|
|
self.setPen(pen)
|
|
|
|
def reset(self):
|
|
super(DropStack, self).reset()
|
|
self.suit = None
|
|
self.value = 0
|
|
|
|
def is_valid_drop(self, card):
|
|
if ((self.suit is None or card.suit == self.suit) and
|
|
(card.value == self.value + 1)):
|
|
return True
|
|
|
|
return False
|
|
|
|
def add_card(self, card, update=True):
|
|
super(DropStack, self).add_card(card, update=update)
|
|
self.suit = card.suit
|
|
self.value = self.cards[-1].value
|
|
|
|
if self.is_complete:
|
|
self.signals.complete.emit()
|
|
|
|
def remove_card(self, card):
|
|
super(DropStack, self).remove_card(card)
|
|
self.value = self.cards[-1].value if self.cards else 0
|
|
|
|
@property
|
|
def is_complete(self):
|
|
return self.value == 13
|
|
|
|
|
|
class DealTrigger(QGraphicsRectItem):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DealTrigger, self).__init__(*args, **kwargs)
|
|
self.setRect(QRectF(DEAL_RECT))
|
|
self.setZValue(1000)
|
|
|
|
pen = QPen(Qt.NoPen)
|
|
self.setPen(pen)
|
|
|
|
self.signals = Signals()
|
|
|
|
def mousePressEvent(self, e):
|
|
self.signals.clicked.emit()
|
|
|
|
|
|
class AnimationCover(QGraphicsRectItem):
|
|
def __init__(self, *args, **kwargs):
|
|
super(AnimationCover, self).__init__(*args, **kwargs)
|
|
self.setRect(QRectF(0, 0, *WINDOW_SIZE))
|
|
self.setZValue(5000)
|
|
pen = QPen(Qt.NoPen)
|
|
self.setPen(pen)
|
|
|
|
def mousePressEvent(self, e):
|
|
e.accept()
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(MainWindow, self).__init__(*args, **kwargs)
|
|
|
|
view = QGraphicsView()
|
|
self.scene = QGraphicsScene()
|
|
self.scene.setSceneRect(QRectF(0, 0, *WINDOW_SIZE))
|
|
|
|
felt = QBrush(QPixmap(os.path.join('images','felt.png')))
|
|
self.scene.setBackgroundBrush(felt)
|
|
|
|
name = QGraphicsPixmapItem()
|
|
name.setPixmap(QPixmap(os.path.join('images','ronery.png')))
|
|
name.setPos(QPointF(170, 375))
|
|
self.scene.addItem(name)
|
|
|
|
view.setScene(self.scene)
|
|
|
|
# Timer for the win animation only.
|
|
self.timer = QTimer()
|
|
self.timer.setInterval(5)
|
|
self.timer.timeout.connect(self.win_animation)
|
|
|
|
self.animation_event_cover = AnimationCover()
|
|
self.scene.addItem(self.animation_event_cover)
|
|
|
|
menu = self.menuBar().addMenu("&Game")
|
|
|
|
deal_action = QAction(QIcon(os.path.join('images', 'playing-card.png')), "Deal...", self)
|
|
deal_action.triggered.connect(self.restart_game)
|
|
menu.addAction(deal_action)
|
|
|
|
menu.addSeparator()
|
|
|
|
deal1_action = QAction("1 card", self)
|
|
deal1_action.setCheckable(True)
|
|
deal1_action.triggered.connect(lambda: self.set_deal_n(1))
|
|
menu.addAction(deal1_action)
|
|
|
|
deal3_action = QAction("3 card", self)
|
|
deal3_action.setCheckable(True)
|
|
deal3_action.setChecked(True)
|
|
deal3_action.triggered.connect(lambda: self.set_deal_n(3))
|
|
|
|
menu.addAction(deal3_action)
|
|
|
|
dealgroup = QActionGroup(self)
|
|
dealgroup.addAction(deal1_action)
|
|
dealgroup.addAction(deal3_action)
|
|
dealgroup.setExclusive(True)
|
|
|
|
menu.addSeparator()
|
|
|
|
rounds3_action = QAction("3 rounds", self)
|
|
rounds3_action.setCheckable(True)
|
|
rounds3_action.setChecked(True)
|
|
rounds3_action.triggered.connect(lambda: self.set_rounds_n(3))
|
|
menu.addAction(rounds3_action)
|
|
|
|
rounds5_action = QAction("5 rounds", self)
|
|
rounds5_action.setCheckable(True)
|
|
rounds5_action.triggered.connect(lambda: self.set_rounds_n(5))
|
|
menu.addAction(rounds5_action)
|
|
|
|
roundsu_action = QAction("Unlimited rounds", self)
|
|
roundsu_action.setCheckable(True)
|
|
roundsu_action.triggered.connect(lambda: self.set_rounds_n(None))
|
|
menu.addAction(roundsu_action)
|
|
|
|
roundgroup = QActionGroup(self)
|
|
roundgroup.addAction(rounds3_action)
|
|
roundgroup.addAction(rounds5_action)
|
|
roundgroup.addAction(roundsu_action)
|
|
roundgroup.setExclusive(True)
|
|
|
|
menu.addSeparator()
|
|
|
|
quit_action = QAction("Quit", self)
|
|
quit_action.triggered.connect(self.quit)
|
|
menu.addAction(quit_action)
|
|
|
|
self.deck = []
|
|
self.deal_n = 3 # Number of cards to deal each time
|
|
self.rounds_n = 3 # Number of rounds (restacks) before end.
|
|
|
|
for suit in SUITS:
|
|
for value in range(1, 14):
|
|
card = Card(value, suit)
|
|
self.deck.append(card)
|
|
self.scene.addItem(card)
|
|
card.signals.doubleclicked.connect(lambda card=card: self.auto_drop_card(card))
|
|
|
|
self.setCentralWidget(view)
|
|
self.setFixedSize(*WINDOW_SIZE)
|
|
|
|
self.deckstack = DeckStack()
|
|
self.deckstack.setPos(OFFSET_X, OFFSET_Y)
|
|
self.scene.addItem(self.deckstack)
|
|
|
|
# Set up the working locations.
|
|
self.works = []
|
|
for n in range(7):
|
|
stack = WorkStack()
|
|
stack.setPos(OFFSET_X + CARD_SPACING_X*n, WORK_STACK_Y)
|
|
self.scene.addItem(stack)
|
|
self.works.append(stack)
|
|
|
|
self.drops = []
|
|
# Set up the drop locations.
|
|
for n in range(4):
|
|
stack = DropStack()
|
|
stack.setPos(OFFSET_X + CARD_SPACING_X * (3+n), OFFSET_Y)
|
|
stack.signals.complete.connect(self.check_win_condition)
|
|
|
|
self.scene.addItem(stack)
|
|
self.drops.append(stack)
|
|
|
|
# Add the deal location.
|
|
self.dealstack = DealStack()
|
|
self.dealstack.setPos(OFFSET_X + CARD_SPACING_X, OFFSET_Y)
|
|
self.scene.addItem(self.dealstack)
|
|
|
|
# Add the deal click-trigger.
|
|
dealtrigger = DealTrigger()
|
|
dealtrigger.signals.clicked.connect(self.deal)
|
|
self.scene.addItem(dealtrigger)
|
|
|
|
self.shuffle_and_stack()
|
|
|
|
self.setWindowTitle("Ronery")
|
|
self.show()
|
|
|
|
def restart_game(self):
|
|
reply = QMessageBox.question(self, "Deal again", "Are you sure you want to start a new game?",
|
|
QMessageBox.Yes | QMessageBox.No)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.shuffle_and_stack()
|
|
|
|
def quit(self):
|
|
self.close()
|
|
|
|
def set_deal_n(self, n):
|
|
self.deal_n = n
|
|
|
|
def set_rounds_n(self, n):
|
|
self.rounds_n = n
|
|
self.deckstack.update_stack_status(self.rounds_n)
|
|
|
|
def shuffle_and_stack(self):
|
|
# Stop any ongoing animation.
|
|
self.timer.stop()
|
|
self.animation_event_cover.hide()
|
|
|
|
# Remove cards from all stacks.
|
|
for stack in [self.deckstack, self.dealstack] + self.drops + self.works:
|
|
stack.reset()
|
|
|
|
random.shuffle(self.deck)
|
|
|
|
# Deal out from the top of the deck, turning over the
|
|
# final card on each line.
|
|
cards = self.deck[:]
|
|
for n, workstack in enumerate(self.works, 1):
|
|
for a in range(n):
|
|
card = cards.pop()
|
|
workstack.add_card(card)
|
|
card.turn_back_up()
|
|
if a == n-1:
|
|
card.turn_face_up()
|
|
|
|
# Ensure removed from all other stacks here.
|
|
self.deckstack.stack_cards(cards)
|
|
|
|
def deal(self):
|
|
if self.deckstack.cards:
|
|
self.dealstack.spread_from = len(self.dealstack.cards)
|
|
for n in range(self.deal_n):
|
|
card = self.deckstack.take_top_card()
|
|
if card:
|
|
self.dealstack.add_card(card)
|
|
card.turn_face_up()
|
|
|
|
elif self.deckstack.can_restack(self.rounds_n):
|
|
self.deckstack.restack(self.dealstack)
|
|
self.deckstack.update_stack_status(self.rounds_n)
|
|
|
|
def auto_drop_card(self, card):
|
|
for stack in self.drops:
|
|
if stack.is_valid_drop(card):
|
|
card.stack.remove_card(card)
|
|
stack.add_card(card)
|
|
break
|
|
|
|
def check_win_condition(self):
|
|
complete = all(s.is_complete for s in self.drops)
|
|
if complete:
|
|
# Add click-proof cover to play area.
|
|
self.animation_event_cover.show()
|
|
# Get the stacks of cards from the drop,stacks.
|
|
self.timer.start()
|
|
|
|
def win_animation(self):
|
|
# Start off a new card
|
|
for drop in self.drops:
|
|
card = drop.take_top_card()
|
|
if card and card.vector is None:
|
|
card.vector = QPoint(-random.randint(1, 10), 0)
|
|
break
|
|
|
|
for card in self.deck:
|
|
if card.vector is not None:
|
|
card.setPos(card.pos() + card.vector)
|
|
card.vector += QPoint(0, 1) # Gravity
|
|
|
|
if card.pos().y() > WINDOW_SIZE[1] - CARD_DIMENSIONS.height():
|
|
card.vector = QPoint(card.vector.x(), -card.vector.y() )
|
|
|
|
if card.pos().x() < - CARD_DIMENSIONS.width():
|
|
card.vector = None
|
|
# Put the card back where it started.
|
|
card.stack.add_card(card)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
app = QApplication([])
|
|
window = MainWindow()
|
|
app.exec_() |