跳到主要内容

使用Python和Flet创建纸牌接龙游戏-第一部分

在本教程中,我们将逐步介绍如何使用Python和Flet创建一个著名的Klondike纸牌接龙游戏。为了灵感,我们参考了这个在线游戏:https://www.solitr.com/

本教程面向初学者/中级水平的Python开发者,需要具备基本的Python和面向对象编程知识。

在这里,您可以看到使用Flet和本教程将要实现的最终结果: https://gallery.flet.dev/solitaire/

我们将游戏实现分解为以下步骤:

在第2部分中(将在下个教程中介绍),我们将添加一个Appbar,其中包含开始新游戏、查看游戏规则和更改游戏设置的选项。

使用Flet入门

要使用Flet创建一个web应用程序,您不需要了解HTML、CSS或JavaScript,但您需要基本的Python和面向对象编程的知识。

Flet需要Python 3.8或更高版本。要使用Flet在Python中创建一个web应用程序,您需要先安装flet模块:

pip install flet

首先,让我们创建一个简单的Hello World应用程序。

创建hello.py并添加以下内容:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(value="Hello, world!"))

ft.app(target=main)

运行此应用程序,您将看到一个带有问候语的新窗口:

可拖动卡片的概念验证应用

在概念验证中,我们只使用了三种类型的控件:

  • Stack - 用于绝对定位插槽和纸牌的父控件
  • GestureDetector - 将在Stack中移动的纸牌
  • Container - 将放置纸牌的插槽。同时也将用作GestureDetector的content

我们将概念验证应用程序分解为四个简单的步骤,这样在每个步骤之后,您都有一个完整的短程序可以运行和测试。

第1步:拖动纸牌

在此步骤中,我们将创建一个Stack(接龙游戏区域)和一个GestureDetector(纸牌)。然后将纸牌添加到Stack的controls列表中。GestureDetector的lefttop属性用于在Stack中进行绝对定位纸牌。

import flet as ft

def main(page: ft.Page):

card = ft.GestureDetector(
left=0,
top=0,
content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100),
)

page.add(ft.Stack(controls=[card], width=1000, height=500))

ft.app(target=main)

运行应用程序,您将看到已将纸牌添加到堆栈中:

为了能够移动纸牌,我们将创建一个drag方法,并在GestureDetector的on_pan_update事件中调用该方法,该事件在用户拖动纸牌时每隔drag_interval触发一次。

为了显示纸牌的移动,我们将在drag方法中在每次发生on_pan_update事件时更新纸牌的topleft属性。

以下是在Stack中拖动GestureDetector的最简代码:

import flet as ft

def main(page: ft.Page):

card = ft.GestureDetector(
left=0,
top=0,
content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100),
)

def drag(dx: int, dy: int):
card.left += dx
card.top += dy

card.on_pan_update = drag

page.add(ft.Stack(controls=[card], width=1000, height=500))

ft.app(target=main)

现在,您可以在Stack中拖动纸牌了:

在下一部分,我们将介绍如何添加扇形堆叠纸牌功能。

import flet as ft

# Use of GestureDetector for with on_pan_update event for dragging card
# Absolute positioning of controls within stack

def main(page: ft.Page):

def drag(e: ft.DragUpdateEvent):
e.control.top = max(0, e.control.top + e.delta_y)
e.control.left = max(0, e.control.left + e.delta_x)
e.control.update()

def drop(e: ft.DragEndEvent):
if (
abs(e.control.top - slot.top) < 20
and abs(e.control.left - slot.left) < 20
):
place(e.control, slot)
else:
bounce_back(solitaire, e.control)
e.control.update()

def place(card, slot):
"""place card to the slot"""
card.top = slot.top
card.left = slot.left
page.update()

def bounce_back(game, card):
"""return card to its original position"""
card.top = game.start_top
card.left = game.start_left
page.update()

class Solitaire:
def __init__(self):
self.start_top = 0
self.start_left = 0

solitaire = Solitaire()

def start_drag(e: ft.DragStartEvent):
solitaire.start_top = e.control.top
solitaire.start_left = e.control.left
e.control.update()

card_1 = ft.GestureDetector(
mouse_cursor=ft.MouseCursor.MOVE,
drag_interval=5,
on_pan_update=drag,
on_pan_end=drop,
on_pan_start=start_drag,
left=0,
top=0,
content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100),
)

card_2 = ft.GestureDetector(
mouse_cursor=ft.MouseCursor.MOVE,
drag_interval=5,
on_pan_update=drag,
on_pan_end=drop,
on_pan_start=start_drag,
left=100,
top=0,
content=ft.Container(bgcolor=ft.colors.BLUE, width=70, height=100),
)

slot = ft.Container(
width=70, height=100, left=200, top=0, border=ft.border.all(1)
)

page.add(ft.Stack(controls=[slot, card_1, card_2], width=1000, height=500))

ft.app(target=main)

上述代码中添加了第二张卡片,并且重构了之前的代码逻辑,将与拖动和放置有关的函数逻辑移到了 drop 函数中,并且对 start_drag 事件进行了定义。我们将保留之前的 Solitaire 类,用于存储第一张卡片的初始位置。

完整的代码可以在这里找到:step3.py


card2 = ft.GestureDetector(
mouse_cursor=ft.MouseCursor.MOVE,
drag_interval=5,
on_pan_start=start_drag,
on_pan_update=drag,
on_pan_end=drop,
left=100,
top=0,
content=ft.Container(bgcolor=ft.colors.YELLOW, width=70, height=100),
)

controls = [slot, card1, card2]
page.add(ft.Stack(controls=controls, width=1000, height=500))

现在,如果你运行这个应用程序,你会注意到当你移动这些卡片时,黄色卡片(card2)正常移动,但是绿色卡片(card1)会被黄色卡片遮挡。这是因为card2被添加到stack的controls列表中card1后面。为了修复这个问题,我们需要在on_pan_start事件中将可拖动的卡片移动到controls列表的顶部:

def move_on_top(card, controls):
"""将可拖动的卡片移动到堆栈的顶部"""
controls.remove(card)
controls.append(card)
page.update()

def start_drag(e: ft.DragStartEvent):
move_on_top(e.control, controls)
solitaire.start_top = e.control.top
solitaire.start_left = e.control.left

现在这两个卡片可以正常拖动了:

为了实现这个功能,我们需要获取卡槽中的卡片堆叠的信息,包括拖动的卡片所在的卡槽和目标卡槽。让我们重新构建我们的程序,为实现展开堆叠的功能做好准备。

卡槽、卡片和纸牌类

卡槽将具有pile属性,用于存储放置在该位置的卡片列表。现在卡槽是一个Container控件对象,我们不能向其添加任何新属性。让我们创建一个新的Slot类,该类将继承自Container类,并在其中添加一个pile属性:

SLOT_WIDTH = 70
SLOT_HEIGHT = 100

import flet as ft

class Slot(ft.Container):
def __init__(self, top, left):
super().__init__()
self.pile=[]
self.width=SLOT_WIDTH
self.height=SLOT_HEIGHT
self.left=left
self.top=top
self.border=ft.border.all(1)

类似于Slot类,让我们创建一个新的Card类,其中包含slot属性,用于记住它所在的卡槽。它将继承自GestureDetector类,并将所有与卡片相关的方法移到其中:

CARD_WIDTH = 70
CARD_HEIGTH = 100
DROP_PROXIMITY = 20

import flet as ft

class Card(ft.GestureDetector):
def __init__(self, solitaire, color):
super().__init__()
self.slot = None
self.mouse_cursor=ft.MouseCursor.MOVE
self.drag_interval=5
self.on_pan_start=self.start_drag
self.on_pan_update=self.drag
self.on_pan_end=self.drop
self.left=None
self.top=None
self.solitaire = solitaire
self.color = color
self.content=ft.Container(bgcolor=self.color, width=CARD_WIDTH, height=CARD_HEIGTH)

def move_on_top(self):
"""将可拖动的卡片移到标准堆栈的顶部"""
self.solitaire.controls.remove(self)
self.solitaire.controls.append(self)
self.solitaire.update()

def bounce_back(self):
"""将卡片返回到其原始位置"""
self.top = self.slot.top
self.left = self.slot.left
self.update()

def place(self, slot):
"""将卡片放置到卡槽中"""
self.top = slot.top
self.left = slot.left

def start_drag(self, e: ft.DragStartEvent):
self.move_on_top()
self.update()

def drag(self, e: ft.DragUpdateEvent):
self.top = max(0, self.top + e.delta_y)
self.left = max(0, self.left + e.delta_x)
self.update()

def drop(self, e: ft.DragEndEvent):
for slot in self.solitaire.slots:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()
备注

注意:由于每个卡片现在都有slot属性,所以在Solitaire类中不再需要记住拖动卡片的起始左侧和顶部位置,因为我们可以将其恢复到卡槽中。

让我们更新Solitaire类,使其继承自Stack类,并将卡片和卡槽的创建移动到其中:

SOLITAIRE_WIDTH = 1000
SOLITAIRE_HEIGHT = 500

import flet as ft
from slot import Slot
from card import Card

class Solitaire(ft.Stack):
def __init__(self):
super().__init__()
self.controls = []
self.slots = []
self.cards = []
self.width = SOLITAIRE_WIDTH
self.height = SOLITAIRE_HEIGHT

def did_mount(self):
self.create_card_deck()
self.create_slots()
self.deal_cards()

def create_card_deck(self):
card1 = Card(self, color="GREEN")
card2 = Card(self, color="YELLOW")
self.cards = [card1, card2]

def create_slots(self):
self.slots.append(Slot(top=0, left=0))
self.slots.append(Slot(top=0, left=200))
self.slots.append(Slot(top=0, left=300))
self.controls.extend(self.slots)
self.update()

def deal_cards(self):
self.controls.extend(self.cards)
for card in self.cards:
card.place(self.slots[0])
self.update()

def main(page: ft.Page):

solitaire = Solitaire()

page.add(solitaire)

ft.app(target=main)

附上完整的源代码和证明概念应用程序相同,但已用新的类重新编写,以准备为其添加更复杂的功能。

带偏移量放置卡片

当卡片被放置在card.place()方法的槽中时,我们需要做三件事:

  • 从原先的槽中移除卡片(如果存在)
  • 将卡片的槽更改为新的槽
  • 将卡片添加到新槽的堆中
def place(self, slot):
# 从原先的槽中移除卡片(如果存在)
if self.slot is not None:
self.slot.pile.remove(self)

# 将卡片的槽更改为新的槽
self.slot = slot

# 将卡片添加到新槽的堆中
slot.pile.append(self)

更新卡片的topleft位置时,left应保持不变,但top将取决于新槽堆的长度:

    self.top = slot.top + len(slot.pile) * CARD_OFFSET
self.left = slot.left

现在卡片将带有偏移量放置在槽中,使我们能够获得堆叠的外观:

拖动卡片堆

如果现在尝试从堆的底部拖动卡片,它会显示如下:

为了解决这个问题,我们需要更新所有与可拖动卡片一起工作的方法,以便与可拖动的堆一起工作。

让我们创建get_draggable_pile()方法,它将返回需要一起拖动的卡片列表,从您选择的卡片开始:

def get_draggable_pile(self):
"""返回需要一起拖动的卡片列表,从当前卡片开始"""
if self.slot is not None:
return self.slot.pile[self.slot.pile.index(self):]
return [self]

然后,我们将更新move_on_top()方法:

def move_on_top(self):
"""将可拖动的卡片堆移到堆栈的顶部"""
for card in draggable_pile:
self.solitaire.controls.remove(card)
self.solitaire.controls.append(card)
self.solitaire.update()

此外,我们需要更新drag()方法,以对所有被拖动的卡片进行迭代,并更新它们的位置:

    draggable_pile = self.get_draggable_pile()
for card in draggable_pile:
card.move(event.dx, event.dy)
class Card:
def __init__(self, rank: str, suite: str):
self.suite = suite
self.rank = rank
self.face_up = False

def flip(self):
self.face_up = not self.face_up

class Container:
def __init__(self, content):
self.content = content
self.face_up = False

def flip(self):
self.face_up = not self.face_up

Next, in the Solitaire class, create a method called create_deck() that initializes a list of 52 cards.

class Solitaire:
def __init__(self):
# other code

def create_deck(self):
"""Creates a new deck of cards"""
deck = []
for suite in ["hearts", "diamonds", "clubs", "spades"]:
for rank in ["Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King"]:
card = Card(rank, suite)
deck.append(card)
return deck

洗牌

下一步是洗牌。为此,我们将使用 Python 的 random 模块及其 shuffle() 函数。

  1. 在文件顶部导入 random 模块:import random
  2. Solitaire 类中,创建一个名为 shuffle_deck() 的方法,该方法使用 random.shuffle() 函数对牌组进行洗牌。
class Solitaire:
def __init__(self):
# 其他代码

def shuffle_deck(self, deck):
"""洗牌"""
random.shuffle(deck)

创建台面堆

接下来是创建台面堆。根据规则,会有 7 个台面堆,第一个堆包含 1 张牌,第二个堆包含 2 张牌,依此类推。

要完成这个步骤:

  1. Solitaire 类中,创建一个名为 create_tableau_piles() 的方法,该方法创建台面堆并在其中分发牌。
class Solitaire:
def __init__(self):
# 其他代码

def create_tableau_piles(self, deck):
"""创建台面堆"""
self.tableau_piles = [[] for _ in range(7)]

for i, pile in enumerate(self.tableau_piles):
for _ in range(i + 1):
card = deck.pop()
card.face_up = True
pile.append(card)
  1. Solitaire 类构造函数(__init__())中,在创建牌组并洗牌后调用 create_tableau_piles() 方法。
class Solitaire:
def __init__(self):
# 其他代码
deck = self.create_deck()
self.shuffle_deck(deck)
self.create_tableau_piles(deck)

创建库存和废牌堆

接下来,我们需要创建库存和废牌堆。 库存堆是未发放的剩余牌组。 废牌堆是从库存堆中翻开的一张张牌堆。

  1. Solitaire 类中,创建一个名为 create_stock_pile() 的方法,该方法使用 Container 类创建库存堆。
class Solitaire:
def __init__(self):
# 其他代码

def create_stock_pile(self, deck):
"""创建库存堆"""
self.stock_pile = Container(deck)
  1. Solitaire 类中,创建一个名为 create_waste_pile() 的方法,该方法使用 Container 类创建废牌堆并将顶部的牌翻面。
class Solitaire:
def __init__(self):
# 其他代码

def create_waste_pile(self):
"""创建废牌堆"""
self.waste_pile = Container([])
if len(self.stock_pile.content) > 0:
top_card = self.stock_pile.content[-1]
self.waste_pile.content.append(top_card)
top_card.flip()
  1. Solitaire 类构造函数(__init__())中,在创建台面堆后调用 create_stock_pile()create_waste_pile() 方法。
class Solitaire:
def __init__(self):
# 其他代码
deck = self.create_deck()
self.shuffle_deck(deck)
self.create_tableau_piles(deck)
self.create_stock_pile(deck)
self.create_waste_pile()

通过这些步骤,我们完成了纸牌游戏的设置。现在我们可以进入下一阶段。

构建游戏界面

我们将使用 Docusaurus 教程项目来创建 Klondike 纸牌游戏界面。有关设置项目和创建基本游戏界面的详细信息,请参考本教程的前几部分。

为游戏界面添加拖放功能

接下来,我们需要为游戏界面添加拖放功能。这将允许我们在不同的牌组之间移动牌。

对于拖放功能,我们将使用 toga 库。

  1. 通过以下命令安装 toga 库:
pip install toga
  1. 在 main.py 文件的开头导入拖放功能所需的模块:
from enum import Enum
from toga.interface.window import Window
from toga_draggable_pile import draggable
import toga
import toga.constants as c

CARD_OFFSET = 20
  1. 创建一个名为 PileType 的枚举类,定义不同的牌组类型:
class PileType(Enum):
TABLEAU = 1
FOUNDATION = 2
STOCK = 3
WASTE = 4
  1. Solitaire 类中,创建一个名为 create_pile() 的方法,该方法在游戏界面上创建牌组:
class Solitaire:
def __init__(self, window):
# 其他代码
self.create_pile(window, self.tableau_piles, PileType.TABLEAU)
self.create_pile(window, self.foundation_piles, PileType.FOUNDATION)
self.create_pile(window, self.stock_pile, PileType.STOCK)
self.create_pile(window, self.waste_pile, PileType.WASTE)

def create_pile(self, window, pile, pile_type):
"""在游戏界面上创建单个牌组"""
pile_box = toga.Box()
pile_box.style.update(flex=1)
pile_box.add(toga.Label(text=str(pile_type.name)))

for card in pile:
card_view = self.create_card_view(card)
pile_box.add(card_view)
draggable.make_draggable(card_view)

pile_box.content.alignment = c.CENTER

window.content.add(pile_box)

def create_card_view(self, card):
"""创建用于在游戏界面上显示卡片的卡片视图"""
card_label = toga.Label(text=str(card))

card_view = toga.Box()
card_view.style.update(
width=c.UNIT * 3,
height=c.UNIT * 4
)
card_view.add(card_label)

if not card.face_up:
card_view.style.update(background_color="#ccc")

return card_view
  1. Solitaire类中,创建一个名为 update_piles() 的方法,该方法更新游戏界面上的牌:
class Solitaire:
def __init__(self, window):
# 其他代码
self.update_piles()

def update_piles(self):
"""更新游戏界面上的牌"""
pile_boxes = []

for pile_type in PileType:
pile_box = None

if pile_type == PileType.TABLEAU:
pile = self.tableau_piles
elif pile_type == PileType.FOUNDATION:
pile = self.foundation_piles
elif pile_type == PileType.STOCK:
pile = self.stock_pile
elif pile_type == PileType.WASTE:
pile = self.waste_pile

for card in pile:
pile_box = self.find_pile_box(pile_boxes, card.slot)
if pile_box is None:
pile_box = toga.Box()
pile_box.style.update(flex=1)
pile_box.add(toga.Label(text=str(pile_type.name)))

pile_boxes.append({
'box': pile_box,
'slot': card.slot
})

card_view = self.create_card_view(card)
pile_box.add(card_view)
draggable.make_draggable(card_view)

if pile_box is not None:
pile_box.content.alignment = c.CENTER

return pile_boxes

def find_pile_box(self, pile_boxes, slot):
"""在给定的牌组框中查找与槽位对应的框"""
for pile_box_info in pile_boxes:
if pile_box_info['slot'] == slot:
return pile_box_info['box']

return None

def create_card_view(self, card):
# 其他代码
  1. Solitaire 类构造函数(__init__())中,在游戏界面上创建牌组后调用 update_piles() 方法:
class Solitaire:
def __init__(self, window):
# 其他代码
pile_boxes = self.update_piles()

box = toga.Box()
box.style.update(flex=1)
for pile_box_info in pile_boxes:
box.add(pile_box_info['box'])

scroller = toga.ScrollContainer(content=box)
window.content = scroller
  1. main() 函数中,在创建 MainWindow 后添加以下代码:
def main():
# 其他代码
solitaire = Solitaire(window)

现在,您应该能够运行游戏并在游戏界面上看到 Klondike 纸牌游戏的详细设置。您还应该能够在不同的牌组之间拖放牌。

在本教程的下一部分中,我们将实现 Klondike 纸牌游戏的游戏逻辑。

class Card(ft.GestureDetector):
def __init__(self, solitaire, suite, rank):
super().__init__()
self.mouse_cursor=ft.MouseCursor.MOVE
self.drag_interval=5
self.on_pan_start=self.start_drag
self.on_pan_update=self.drag
self.on_pan_end=self.drop
self.suite=suite
self.rank=rank
self.face_up=False
self.top=None
self.left=None
self.solitaire = solitaire
self.slot = None
self.content=ft.Container(
width=CARD_WIDTH,
height=CARD_HEIGTH,
border_radius = ft.border_radius.all(6),
content=ft.Image(src="card_back.png"))

所有正面朝上的图片以及卡背都存储在与main.py相同目录下的”images”文件夹中。

备注

为了使图片文件的引用有效工作,我们需要在main.py中的assets_dir中指定它所在的文件夹:

ft.app(target=main, assets_dir="images")

最后,在solitaire.create_card_deck()中,我们将创建套数和等级的列表,然后创建52张牌:

def create_card_deck(self):
suites = [
Suite("hearts", "RED"),
Suite("diamonds", "RED"),
Suite("clubs", "BLACK"),
Suite("spades", "BLACK"),
]
ranks = [
Rank("Ace", 1),
Rank("2", 2),
Rank("3", 3),
Rank("4", 4),
Rank("5", 5),
Rank("6", 6),
Rank("7", 7),
Rank("8", 8),
Rank("9", 9),
Rank("10", 10),
Rank("Jack", 11),
Rank("Queen", 12),
Rank("King", 13),
]

self.cards = []

for suite in suites:
for rank in ranks:
self.cards.append(Card(solitaire=self, suite=suite, rank=rank))

卡牌组已准备好发牌,现在我们需要为它创建布局。

创建槽位

Klondike纸牌游戏的布局应如下所示:

让我们在solitaire.create_slots()中创建所有这些槽位:

def create_slots(self):

self.stock = Slot(top=0, left=0, border=ft.border.all(1))
self.waste = Slot(top=0, left=100, border=None)

self.foundations = []
x = 300
for i in range(4):
self.foundations.append(Slot(top=0, left=x, border=ft.border.all(1, "outline")))
x += 100

self.tableau = []
x = 0
for i in range(7):
self.tableau.append(Slot(top=150, left=x, border=None))
x += 100

self.controls.append(self.stock)
self.controls.append(self.waste)
self.controls.extend(self.foundations)
self.controls.extend(self.tableau)
self.update()
备注

注意:某些槽位应该有可见的边框,而其他槽位则不需要,所以我们在创建Slot对象时将边框添加到参数列表中。

发牌

先让我们洗牌并将牌添加到控件列表中:

def deal_cards(self):
random.shuffle(self.cards)
self.controls.extend(self.cards)
self.update()

然后,我们从左到右将牌发到台面纸牌组中,使得每个纸牌组中的牌比上一个纸牌组多一张,将剩余的牌放到库存纸牌组:

def deal_cards(self):
random.shuffle(self.cards)
self.controls.extend(self.cards)

# 发到台面
first_slot = 0
remaining_cards = self.cards

while first_slot < len(self.tableau):
for slot in self.tableau[first_slot:]:
top_card = remaining_cards[0]
top_card.place(slot)
remaining_cards.remove(top_card)
first_slot +=1

# 将剩余的牌放到库存纸牌组中
for card in remaining_cards:
card.place(self.stock)

self.update()

让我们运行程序,看看我们现在的进展:

库存中的卡片被以与场面牌相同的方式展开成扇形堆叠,但实际上应该放在一个普通的堆叠中。为了解决这个问题,让我们将这个条件添加到card.place()方法中:

def place(self, slot):
"""将可拖动的牌堆放在指定位置"""
if slot in self.solitaire.tableau:
self.top = slot.top + len(slot.pile) * self.solitaire.card_offset
else:
self.top = slot.top
self.left = slot.left

现在只有在堆叠到场面牌时才会将牌放置在扇形堆叠中:

现在,如果尝试移动卡片,程序将无法正常工作。原因是在card.drop()方法中迭代了一个我们现在没有的槽列表。

让我们更新该方法分别遍历基础和场面牌堆:

def drop(self, e: ft.DragEndEvent):
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()

揭示放置在场面牌堆的顶部卡片

现在我们已经有了正确的游戏设置,作为最后一步,我们需要揭示放置在场面牌堆的顶部卡片。

Slot类中,创建get_top_card()方法:

def get_top_card(self):
if len(self.pile) > 0:
return self.pile[-1]

Card类中,创建turn_dace_up()方法:

def turn_face_up(self):
self.face_up = True
self.content.content.src=f"/images/{self.rank.name}_{self.suite.name}.svg"
self.update()

最后,在solitaire.deal_cards()中揭示场面牌堆的顶部卡片:

for slot in self.tableau:
slot.get_top_card().turn_face_up()
self.update()

让我们看一下现在的效果:

可以在这里找到本步骤的完整源代码。

恭喜你完成了纸牌游戏的设置!你已经创建了一副完整的52张纸牌,构建了包括库存、浪费区、基础堆和场面牌堆的布局,发牌并揭示了场面牌堆中的顶部卡片。让我们继续进行下一个待办事项,即游戏规则。

游戏规则

如果运行当前版本的纸牌游戏,你会发现你可以做一些疯狂的事情:

现在是时候实现一些规则了。

通用规则

当前可以移动任何卡片,但只有翻开的卡片才能被移动。让我们在卡片的start_dragdragdrop方法中添加这个检查:

def start_drag(self, e: ft.DragStartEvent):
if self.face_up:
self.move_on_top()
self.update()

def drag(self, e: ft.DragUpdateEvent):
if self.face_up:
draggable_pile = self.get_draggable_pile()
for card in draggable_pile:
card.top = max(0, self.top + e.delta_y) + draggable_pile.index(card) * CARD_OFFSET
card.left = max(0, self.left + e.delta_x)
card.update()

def drop(self, e: ft.DragEndEvent):
if self.face_up:
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()

def click(self, e):
if self.slot in self.solitaire.tableau:
if not self.face_up and self == self.slot.get_top_card():
self.turn_face_up()
self.update()

def drop(self, e: ft.DragEndEvent):
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

if len(self.get_draggable_pile()) == 1:
for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()

def check_foundations_rules(self, card, slot):
top_card = slot.get_top_card()
if top_card is not None:
return (
card.suite.name == top_card.suite.name
and card.rank.value - top_card.rank.value == 1
)
else:
return card.rank.name == "Ace"

def drop(self, e: ft.DragEndEvent):
if self.face_up:
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

if len(self.get_draggable_pile()) == 1:
for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
) and self.solitaire.check_foundations_rules(self, slot):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()

现在我们为卡片的on_tap事件指定click方法,如果你点击一个面朝下的牌堆的顶部卡片,将其翻开:

def click(self, e):
if self.slot in self.solitaire.tableau:
if not self.face_up and self == self.slot.get_top_card():
self.turn_face_up()
self.update()

让我们来看看它是如何工作的:

基础规则

目前我们可以将展开的牌堆放到基础牌堆,这是不应该允许的。让我们检查可拖动牌堆的长度修复它:

def drop(self, e: ft.DragEndEvent):
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

if len(self.get_draggable_pile()) == 1:
for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()

然后,当然,并不是任何牌都可以放到基础牌堆中。根据规则,基础牌堆应该以一个ACE开头,然后相同花色的牌可以放在其上构成从ACE到K的牌堆。

让我们在Solitaire类中添加这个规则:

def check_foundations_rules(self, card, slot):
top_card = slot.get_top_card()
if top_card is not None:
return (
card.suite.name == top_card.suite.name
and card.rank.value - top_card.rank.value == 1
)
else:
return card.rank.name == "Ace"

我们将在drop()方法中,在将牌放入基础牌堆槽位之前,检查此规则:

def drop(self, e: ft.DragEndEvent):
if self.face_up:
for slot in self.solitaire.tableau:
if (
abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return

if len(self.get_draggable_pile()) == 1:
for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
) and self.solitaire.check_foundations_rules(self, slot):
self.place(slot)
self.update()
return

self.bounce_back()
self.update()
class Card(ft.Container):
...

def get_draggable_pile(self):
if self.slot in self.solitaire.tableau:
return self.slot.get_pile_above(self)
else:
return [self]

继续完成"Foundation rules":

class Card(ft.Container):
...

def doubleclick(self, e):
if self.face_up:
self.move_on_top()
for slot in self.solitaire.foundations:
if self.solitaire.check_foundations_rules(self, slot):
self.place(slot)
self.page.update()
return

...


class Solitaire:
...

def check_foundations_rules(self, card, slot):
top_card = slot.get_top_card()
if top_card is not None:
return (
card.suite == top_card.suite
and card.rank.value - top_card.rank.value == 1
)
else:
return card.rank.name == "Ace"

...

接下来实现"Tableau rules":

class Card(ft.Container):
...

def drop(self, e: ft.DragEndEvent):
...

if len(self.get_draggable_pile()) == 1:
for slot in self.solitaire.foundations:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
) and self.solitaire.check_foundations_rules(self, slot):
self.place(slot)
self.update()
return

...

self.bounce_back()
self.update()


class Solitaire:
...

def check_tableau_rules(self, card, slot):
top_card = slot.get_top_card()
if top_card is not None:
return (
card.suite.color != top_card.suite.color
and top_card.rank.value - card.rank.value == 1
and top_card.face_up
)
else:
return card.rank.name == "King"

...

最后实现"Stock and waste"部分:

class Card(ft.Container):
...

def click(self, e):
if self.slot in self.solitaire.tableau:
if not self.face_up and self == self.slot.get_top_card():
self.turn_face_up()
self.update()
elif self.slot == self.solitaire.stock:
self.move_on_top()
self.place(self.solitaire.waste)
self.turn_face_up()
self.solitaire.update()


class Slot(ft.Container):
...

def click(self, e):
if self == self.solitaire.stock:
self.solitaire.restart_stock()


class Solitaire:
...

def restart_stock(self):
while len(self.waste.pile) > 0:
card = self.waste.pile.pop()
card.turn_face_down()
card.move_on_top()
card.place(self.stock)
self.update()
def get_draggable_pile(self):
"""返回将一起拖动的卡片列表,从当前卡片开始"""
if self.slot is not None and self.slot != self.solitaire.stock and self.slot != self.solitaire.waste:
return self.slot.pile[self.slot.pile.index(self):]
return [self]

完成了!可以在这里找到本步骤的完整源代码。

接下来进入游戏本身的最后一部分 - 检测胜利的情况。

胜利条件

根据维基百科,有人认为赢得游戏的机会是30分之一。

考虑到胜率相当低,当玩家最终赢得游戏时,我们应该给玩家一个令人兴奋的展示。

首先,在Solitaire类中添加胜利条件的检查。如果所有四个基础牌堆中的牌总数为52,那么你就赢了:

def check_win(self):
cards_num = 0
for slot in self.foundations:
cards_num += len(slot.pile)
if cards_num == 52:
return True
return False

每次将卡片放置在基础牌堆时,我们将检查是否满足这个条件:

def place(self, slot):
"""将可拖动的卡片堆放置到指定位置"""

draggable_pile = self.get_draggable_pile()

for card in draggable_pile:
if slot in self.solitaire.tableau:
card.top = slot.top + len(slot.pile) * CARD_OFFSET
else:
card.top = slot.top
card.left = slot.left

# 从原始位置移除卡片
if card.slot is not None:
card.slot.pile.remove(card)

# 将卡片的位置更改为新位置
card.slot = slot

# 将卡片添加到新位置的堆中
slot.pile.append(card)

if self.solitaire.check_win():
self.solitaire.winning_sequence()

self.solitaire.update()

最后,如果满足胜利条件,将会触发一个胜利动画,其中包含位置动画

def winning_sequence(self):
for slot in self.foundations:
for card in slot.pile:
card.animate_position=1000
card.move_on_top()
card.top = random.randint(0, SOLITAIRE_HEIGHT)
card.left = random.randint(0, SOLITAIRE_WIDTH)
self.update()
self.controls.append(ft.AlertDialog(title=ft.Text("恭喜!你赢了!"), open=True))

可以想象,赢得游戏并采取这个视频之前,我花费了一段时间,但是在这里: winning_game

哇!我们成功了。你可以在这里找到Solitaire游戏第一部分的完整源代码。

在第二部分中,我们将添加顶部菜单,包括重新开始游戏、查看游戏规则和更改游戏设置(如浪费堆大小、通过浪费堆的次数和卡片背面图像)的选项。

现在,由于我们已经有了一个不错的桌面版游戏,让我们部署它作为一个网络应用,与朋友和同事分享。

部署应用

恭喜!你已经使用Flet在Python中创建了Solitaire游戏应用,并且它看起来非常棒!

现在是时候与世界分享你的应用了!

按照这些说明将你的Flet应用部署为基于Fly.io或Replit的网络应用。

总结

在本教程中,你学会了如何:

  • 创建一个简单的Flet应用程序;
  • 使用GestureDetector拖放卡片;
  • 创建自己的类,继承自Flet控件;
  • 使用Stack中的绝对布局定位控件来设计UI布局;
  • 实现隐式动画;
  • 将Flet应用部署到Web。

有关更多阅读材料,您可以浏览控件示例仓库

控件

在这个部分,您可以找到有关控件的文档和指南。控件是xxx。

示例仓库

该示例仓库包含了一系列使用Python的示例代码。您可以从这些示例中学习xxx。

请随时提出您对以上内容的任何问题。