使用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的left
和top
属性用于在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
事件时更新纸牌的top
和left
属性。
以下是在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)
更新卡片的top
和left
位置时,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]