首页 › 社区 › 公告与更新客户端下载

塔罗 Skill 后端实现公开(洗牌 / 切牌 / 牌义)

作者 Soulmate Agent 开发团队 · 发布于 2026-10-22 · 浏览 24

# 塔罗 Skill 后端实现公开(洗牌 / 切牌 / 牌义)

本篇公开「塔罗」技能的后端实现与核心代码。服务端只做一件事:把一副 78 张的牌洗好、切好、定好正逆位;抽牌和解牌在客户端本地完成。

一、整体链路

`` 客户端技能(xjx-tarot-cn / -global,指导型) ↓ 客户端核心内置工具 xjx_fetch_tarot ↓ (凭证由客户端核心代管) Node 路由 POST /api/tarot/v1/shuffle ↓ 本机调用 Python 计算服务 127.0.0.1:8098 /api/tarot/shuffle ↓ 返回整副牌堆的顺序、正逆位、牌义 ``

之后:模型按技能要求逐张问用户「抽第几张」,再按牌堆序号取牌、解读。抽牌动作不经过服务端,服务端也永远不知道用户抽了哪几张。

二、牌库结构

78 张固定牌序:

每张牌的数据结构:

``python { "id": 1, "name": "愚人", "arcana": "大阿尔卡纳", "suit": "", "element": "", "rank": "", "upright": "新的开始,冒险,天真,率性,充满潜力,……", "reversed": "鲁莽,冒险,粗心,失去控制,……", "key_up": ["新的开始", "冒险", "天真"], "key_rev": ["鲁莽", "冒险", "粗心"], "nature": "一切尚未定型的起点", "advice": "先迈步再校正,别等万事俱备", "advice_rev": "先看脚下,把风险边界划清再动", "symbols": "悬崖、白玫瑰、小狗、行囊", } ``

字段分工:

| 字段 | 用途 | |---|---| | upright / reversed | 正位、逆位牌义(原文,不做改写) | | key_up / key_rev | 关键词,供解码时抓重点 | | nature | 一句定性 | | advice / advice_rev | 正位、逆位的行动指向 | | symbols | 牌面象征(大阿尔卡纳 22 张) |

三、洗牌与切牌算法

原则:用密码学安全的随机源,不用 random.random() 这类可预测序列。

```python import random

def shuffle_deck(body=None): rnd = random.SystemRandom() deck = list(tarot_deck.CARDS) rnd.shuffle(deck)

n = len(deck) first_cut = rnd.randint(1, n - 1) part, rest = deck[:first_cut], deck[first_cut:]

if len(part) > 1: second_cut = rnd.randint(1, len(part) - 1) else: second_cut = 1 a, b = part[:second_cut], part[second_cut:]

blocks = [a, b, rest] rnd.shuffle(blocks) merged = [c for blk in blocks for c in blk] merged = _riffle(merged, rnd) ... ```

具体动作对应一次真实的洗牌流程:

1. 整副洗牌:78 张随机排序; 2. 第一次切牌:从牌堆上方抓走随机张数,分成「切出的部分」与「剩下的部分」; 3. 第二次切牌:在切出的那部分里再抓一次,得到 A、B 两叠,加上剩下的 C 叠,共三叠; 4. 随机合堆:A/B/C 三叠的顺序也随机; 5. 交错合并:对合并后的牌堆做一次 riffle(交错洗牌)。

riffle 的实现里,下一张从左还是从右取,有 70% 概率换手,避免机械的严格交替:

``python def _riffle(cards, rnd): """对半分开后交错合并,交错起点随机""" n = len(cards) if n < 4: return cards mid = n // 2 left, right = cards[:mid], cards[mid:] out = [] i = j = 0 pick_left = rnd.random() < 0.5 while i < len(left) and j < len(right): if pick_left: out.append(left[i]); i += 1 else: out.append(right[j]); j += 1 if rnd.random() < 0.7: pick_left = not pick_left out.extend(left[i:]) out.extend(right[j:]) return out ``

四、正逆位

洗完之后逐张独立判定,正位与逆位各 50%:

``python for pos, c in enumerate(merged, 1): upright = rnd.random() < 0.5 cards.append({ "pos": pos, "id": c["id"], "name": c["name"], "arcana": c["arcana"], "suit": c["suit"], "element": c["element"], "rank": c["rank"], "orientation": ORIENT_UPRIGHT if upright else ORIENT_REVERSED, "meaning": c["upright"] if upright else c["reversed"], "keywords": c["key_up"] if upright else c["key_rev"], "nature": c["nature"], "advice": c["advice"] if upright else c["advice_rev"], "symbols": c.get("symbols", ""), }) ``

正逆位不是「整副一起翻」,而是每张独立——这是洗牌后牌堆的真实状态。

五、为什么抽牌不放在服务端

服务端返回的是一整副已经洗好的牌堆(pos 1–78)。随后由客户端模型按技能要求:

1. 逐张问用户「抽第几张」; 2. 按用户报的序号从牌堆里取对应那张; 3. 同一张不能重复抽; 4. 未抽到的牌不提前解读,也不向用户透露牌堆顺序。

这样做的原因很直接:抽牌是用户在现场做的动作,服务端没有理由知道,也没有必要记录。服务端只保证「这副牌在洗好的那一刻是随机且完整的」。

返回体里同时给一句 text:

`` 牌堆共 78 张,已洗牌并切牌完毕。 请让用户逐张报序号(1-78,同一张不能重复)。 未抽到的牌不要提前解读,也不要向用户透露牌堆顺序。 ``

六、API 层与计费

七、这套实现的特点

1. 随机源是 SystemRandom:取自操作系统熵池,不是伪随机序列。 2. 洗 + 切 + 交错三段都在服务端完成:客户端拿到的是最终牌堆,不能挑牌、不能重排。 3. 正逆位逐张独立:符合真实牌堆状态。 4. 服务端不知道你抽了什么:抽牌只在本地发生,隐私边界清楚。 5. 可复现的计费:同一问题同一牌阵在窗口内重复请求,回放同一副牌,不多收钱。

参与讨论