前言
MYScrcpy Github / Gitee
从 1.6.4 版本开始,MYScrcpy为童鞋们提供了一个开放的插件开发环境(Extensions)。童鞋们可以根据需要自行开发插件。
本章主要讲解如何通过面向对象的思想,借助Dearpygui绘制一个记牌器面板,同时如何打包插件进行共享。
注意
插件用于功能测试及教学目的,切勿违法违规使用!
我们开始吧
创建功能类
- 在
ddz
类下创建ddz_cls.py
,新建对象类 - 既然是记牌器,首先要把游戏各个对象抽象出来,形成类
@dataclass
class Card:
"""
Poker Card
"""
name: str
index: int
max_n: int = 4
def __hash__(self):
return hash(self.name)
class Cards:
"""
Poker Card Type
"""
CARD_D = Card("D", index=0, max_n=1)
CARD_X = Card("X", index=1, max_n=1)
CARD_2 = Card("2", index=2)
CARD_A = Card("A", index=3)
CARD_K = Card("K", index=4)
CARD_Q = Card("Q", index=5)
CARD_J = Card("J", index=6)
CARD_T = Card("T", index=7)
CARD_9 = Card("9", index=8)
CARD_8 = Card("8", index=9)
CARD_7 = Card("7", index=10)
CARD_6 = Card("6", index=11)
CARD_5 = Card("5", index=12)
CARD_4 = Card("4", index=13)
CARD_3 = Card("3", index=14)
@classmethod
def ordered_cards(cls) -> list[Card]:
"""
卡牌顺序列表
:return:
"""
return [
cls.CARD_D, cls.CARD_X,
cls.CARD_2, cls.CARD_A,
cls.CARD_K, cls.CARD_Q, cls.CARD_J,
cls.CARD_T, cls.CARD_9, cls.CARD_8, cls.CARD_7, cls.CARD_6, cls.CARD_5, cls.CARD_4, cls.CARD_3
]
@classmethod
def str2cards(cls, cmd: str) -> list[Card] | None:
"""
cmd to card list
B5 means Bomb 4
SQ means qqq
E8 means 88
:param cmd:
:return:
"""
cmd = cmd.upper()
if len(cmd) in [3, 4] and cmd[1] == '-':
try:
card_0 = getattr(cls, f"CARD_{cmd[0]}")
card_1 = getattr(cls, f"CARD_{cmd[2]}")
except Exception as e:
return None
return cls.ordered_cards()[min(card_0.index, card_1.index):max(card_0.index, card_1.index)+1] * (
1 if len(cmd) == 3 else int(cmd[3])
)
cmd = cmd.replace('W', 'DX')
repeat = 0
cards = []
for _ in cmd:
if repeat:
cards.extend([getattr(cls, f"CARD_{_}")] * repeat)
repeat = 0
else:
if _ == 'B':
repeat = 4
continue
elif _ == 'S':
repeat = 3
continue
elif _ == 'E':
repeat = 2
continue
else:
cards.append(getattr(cls, f"CARD_{_}"))
return cards
定义卡槽,存储卡序列
class CardSlot:
"""
卡槽
"""
def __init__(self, max_slots: int):
self.max_slots = max_slots
self.cards = []
def __len__(self):
return len(self.cards)
def add_cards(self, cards: Iterable[Card] | None):
"""
新增卡
:param cards:
:return:
"""
cards and self.cards.extend(cards)
@property
def card_dict(self):
"""
卡字典
:return:
"""
_ = {}
for card in self.cards:
_.setdefault(card, {'n': 0})['n'] += 1
return _
定义玩家、出牌环节及游戏局
class Player:
"""
玩家
"""
def __init__(self, name: str, is_landlord: bool = False, public_cards: list[Card] = None):
self.name = name
self.is_landlord = is_landlord
self.slot_on_table = CardSlot(20)
self.slot_hold = CardSlot(20)
if is_landlord and public_cards:
self.slot_hold.add_cards(public_cards)
@property
def hold_cards_n(self) -> int:
"""
剩卡数
:return:
"""
return (20 if self.is_landlord else 17) - len(self.slot_on_table)
@dataclass
class Round:
"""
一轮游戏
"""
index: int
player: Player
action: int
cards: list[Card] = field(default_factory=list)
class Rounds:
ACTION_PASS = 0
ACTION_PLAY = 1
PLAYER_N = 3
def __init__(self, who_is_landlord: int, public_cards: list[Card], self_cards: list[Card]):
self.players = [
Player(['L', 'Y', 'R'][_], who_is_landlord == _, public_cards) for _ in range(3)
]
self.now = who_is_landlord
self.slot_on_table = CardSlot(54)
self.card_rounds = []
self.player_self.slot_hold.add_cards(self_cards)
@property
def player_self(self) -> Player:
return self.players[1]
@property
def player_current(self) -> Player:
return self.players[self.now]
def play(self, action: int, cards: list[Card]) -> Round:
"""
出牌
:param action:
:param cards:
:return:
"""
_r = Round(len(self.card_rounds), self.players[self.now], action, cards)
self.card_rounds.append(_r)
self.slot_on_table.add_cards(cards)
self.now += (1 if self.now < (self.PLAYER_N - 1) else -(self.PLAYER_N - 1))
return _r
定义一个记牌器组件及记牌器窗口
class CardsBoard:
"""
记牌器
"""
def __init__(self):
self.tag_table = dpg.generate_uuid()
def clear(self):
dpg.delete_item(self.tag_table, children_only=True, slot=1)
def draw(self, parent: str | int):
with dpg.table(tag=self.tag_table, parent=parent):
for card in Cards.ordered_cards():
dpg.add_table_column(label=card.name)
dpg.add_table_column(label='n')
def update(self, rounds: Rounds):
self.clear()
with dpg.table_row(parent=self.tag_table):
cards_out_all = 0
for card in Cards.ordered_cards():
cards_n = rounds.slot_on_table.cards.count(card)
cards_out = card.max_n - cards_n - rounds.player_self.slot_hold.cards.count(card)
cards_out_all += cards_out
msg = ''
if cards_out > 0:
if cards_out == 4:
msg += '@4'
else:
msg += f"{cards_out}"
dpg.add_text(msg)
dpg.add_text(str(cards_out_all))
class DDZBoard:
"""
斗地主记牌器
"""
players = ['Left', 'Your', 'Right']
def __init__(self):
self.tag_win = dpg.generate_uuid()
self.tag_ipt_yc = dpg.generate_uuid()
self.tag_ipt_cards = dpg.generate_uuid()
self.tag_txt_cur_player = dpg.generate_uuid()
self.board_cards = CardsBoard()
def new_round(self, sender, app_data, user_data):
"""
新游戏
:param sender:
:param app_data:
:param user_data:
:return:
"""
self.rounds = Rounds(
self.players.index(dpg.get_value(user_data[0])),
Cards.str2cards(dpg.get_value(user_data[1])),
Cards.str2cards(dpg.get_value(user_data[2]))
)
dpg.set_value(user_data[1], '')
dpg.set_value(user_data[2], '')
self.clear()
self.update()
def play(self, sender, app_data, user_data):
if user_data is None:
self.rounds.play(Rounds.ACTION_PASS, [])
else:
self.rounds.play(Rounds.ACTION_PLAY, Cards.str2cards(dpg.get_value(user_data)))
dpg.set_value(user_data, '')
self.update()
def draw(self):
with dpg.window(tag=self.tag_win, width=700, height=160, label='Cards Board'):
with dpg.group(horizontal=True):
tag_ll = dpg.add_combo(
items=self.players, label='LandLord', default_value=self.players[1], width=60)
tag_pub = dpg.add_input_text(label='Public cards', width=50)
self.tag_ipt_cards = dpg.add_input_text(tag=self.tag_ipt_yc, label='Your cards', width=150)
dpg.add_button(label='Start', user_data=(tag_ll, tag_pub, self.tag_ipt_yc), callback=self.new_round)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_text(tag=self.tag_txt_cur_player, default_value='Ready')
dpg.add_input_text(label='Cards', width=150)
dpg.add_button(label='Play', user_data=dpg.last_item(), callback=self.play)
dpg.add_button(label='Pass', callback=self.play)
dpg.add_separator()
self.board_cards.draw(self.tag_win)
def clear(self):
self.board_cards.clear()
dpg.set_value(self.tag_txt_cur_player, f"Ready")
def update(self):
self.board_cards.update(self.rounds)
dpg.set_value(self.tag_txt_cur_player, f"Wait Player {self.rounds.player_current.name}")
- 在插件中引入记牌器类,并添加相应功能
...
def start(self):
...
# 创建记牌器面板
self.board = DDZBoard()
def predict(self):
...
# 玩家手牌位置
if y0 > 0.6:
name = names[int(box.cls[0])][1]
if name.upper() == 'O':
# 判断大小王
_img = Image.fromarray(
result.orig_img
).crop([
round(_) for _ in box.xyxy.tolist()[0]
])
blue = self.cal_blue(_img)
if blue > 5:
cards += 'D'
else:
cards += 'X'
else:
cards += name.upper()
new_cards = ''
for card in Cards.ordered_cards():
new_cards += card.name * cards.count(card.name)
dpg.set_value(self.board.tag_ipt_cards, new_cards)
@staticmethod
def cal_blue(img: Image.Image) -> int:
pixels = img.size[0] * img.size[1]
colors = img.getcolors(pixels)
return round(sum([
count / pixels for count, pixel_color in
colors if pixel_color[0] < 50 and pixel_color[1] < 50 and pixel_color[2] > 150
]) * 100)
插件打包及共享
实际上,插件打包非常简单
新建一个ddz文件夹,将项目相关的文件拷贝其中
|- ddz
|- __init__.py
|- extension.toml
|- ddz.py
|- ddz_cls.py
|- pts/best.pt
将整个ddz文件压缩成ddz.zip文件
这样,插件就打包完成了。
注意,ZIP文件中需包含顶层文件夹,即打开ZIP文件时目录为 ddz文件夹
运行MYScrcpy,查看效果
将打包好的插件 ddz.zip 拷贝至 myscrcpy 插件目录下
~/.myscrcpy/extensions/
本次直接使用 mysc-cli
命令启动 MYScrcpy
可以看到,已经通过加载插件的方式加载ddz插件
加载日志:
现在,你可以将 ddz.zip 分享给你的小伙伴,通过MYScrcpy直接加载使用了!
注意,在加载插件时,需要提前安装插件运行所需的 PIL/Opencv/ultralytics 等包
总结
至此,我们借助MYScrcpy 插件架构完成了一个基础的记牌器,本系列教程也将告一段落。
因篇幅有限,插件功能简陋,不过正如笔者写这个系列的初衷一样,授之于鱼,不如授之以渔。
希望各位童鞋能通过这个系列了解MYScrcpy,了解Python,进而编写创造属于你自己的MYScrcpy插件!
文中ddz.zip已经放到群共享。
有任何问题欢迎留言或加Q群579618095交流。