跳到主要内容

使用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)

以上是我对Python代码的中文翻译,输出格式与原文保持一致。希望能对你有帮助!

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。

Hello, the above is the English documentation in docusaurus md format provided by me. Please provide the complete Chinese translation, and keep the output content in the docusaurus md document structure format. 有关更多阅读材料,您可以浏览控件示例仓库

控件

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

示例仓库

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

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

Docusaurus 中文文档

您对我们的反馈非常重要!请通过 邮件 将其发送给我们,也欢迎加入我们的 Discord 进行讨论,或在 Twitter 上关注我们。

邮件联系方式

如果您有任何问题、建议或反馈,请随时给我们发送邮件至 hello@flet.dev

Discord 交流

我们在 Discord 上有一个讨论群组,您可以加入并与我们一起进行交流。点击此处即可加入我们的 Discord。

Twitter 关注

我们的 Twitter 账号提供最新的消息和更新,您可以在 Twitter 上关注我们,以便获取最新动态。

谢谢您的支持!