ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Markdown+Python+Git构建跑团Replay工程化记录流程

2026/8/31 12:15:33 拓冰建站 浏览量
用Markdown+Python+Git构建跑团Replay工程化记录流程 如果你正在追更一档跑团 replay或者自己就是那个负责整理跑团记录的玩家你大概率遇到过这种状态剧情推进到第七话伏笔和人物关系已经多到记不住上一场的关键骰点究竟是成功还是失败翻聊天记录翻到凌晨三点主持人写的场景描述、几个玩家的即兴台词、战斗中的临时判定全混在一起最后整理出来的内容像一锅粥。以“虚舟之村07黑死牟很满意”这类章节为例。表面看这是《鬼灭之刃》题材 TRPG 跑团的一段叙事记录但真正做过连载内容的人会意识到它本质上是一个内容生产项目有固定角色、有连续剧情、有随机骰点、有章节发布节奏。只要超过三期单靠记忆和聊天记录来维护必然出问题。本文要给出的判断是跑团 replay 的记录难点不是“文笔不好”而是缺少工程化流程。解决它不靠更复杂的工具靠一套轻量的 Markdown Python Git 流程就够了。读完这篇文章你会得到一整套可复用的跑团 replay 工作流包括章节目录设计、角色卡模板、骰点记录脚本、自动汇总脚本以及多人协作时避免冲突的工程建议。这套流程不只适用于鬼灭题材任何长篇连载式内容创作都能用。1. 跑团 replay 记录为什么值得工程化先定义一下“跑团 replay”。TRPG 玩家在跑团时主持人负责描述剧情、控制 NPC玩家操控自己的角色行动系统通过骰子决定关键事件的成败。“replay”是把这场游戏过程重新呈现出来的内容形态它可以是文字小说、图文记录也可以在此基础上制作成视频。很多玩家以为 replay 就是“把跑团过程记录下来”实际上难点远不止记录。一场三小时的跑团录音转文字可能有三万字其中有大量闲聊、规则争辩、空场等待真正对剧情有推动作用的可能是三千字这三千字里主持人叙述、玩家台词、NPC 对话、规则判定又需要区分。如果你在连载一个系列比如“虚舟之村”已经到第七话那么你还面临另一个问题第七话里出现的某个道具可能是在第三话埋下的伏笔黑死牟这个 BOSS 的能力数值可能和第五话的某次判定有关。把这些信息全部塞进人的大脑结果就是更新速度越来越慢质量越来越不稳定。最常见的情况是玩家在跑团群里说“上次我过了体质检定应该没事”但翻聊天记录发现那次检定其实失败了只是主持人给了剧情补偿。这类分歧一旦出现后续内容就失去可信度。工程化的价值不在于替代创作而在于解决三件事可检索、可回溯、可协作。可检索是指你随时能找到“某一次骰点是什么结果”可回溯是指你能从第一话开始追溯角色成长线索可协作是指主持人、玩家、剪辑者可以同时在一个内容仓库里工作互不覆盖。2. 从“跑团章节”到“内容项目”核心概念与数据模型要设计流程先把一个跑团章节拆成数据。以“虚舟之村07黑死牟很满意”为例这个章节包含四类核心信息第一元信息。包括系列名、章节号、章节标题、发布日期、参与玩家。这些信息用于系列归档和发布页展示。第七话的标题“黑死牟很满意”在元信息里就是章节标题字段。第二场景。一场跑团由若干连续场景构成例如“月光下的芦苇原”“村口遭遇战”“夜色中的对峙”。“虚舟之村07”至少包含一个核心战斗场景因为黑死牟作为强敌登场。第三角色与台词。玩家角色、主持人 NPC、本话登场的敌对角色都需要有持续存在的角色卡。台词要标明说话者否则整理成文字后无法阅读。第四骰点记录。这是跑团 replay 和普通小说最大的区别。骰子结果直接决定剧情走向例如一次敏捷检定失败可能让玩家在 BOSS 面前失去先手。记录骰点时至少需要包含场景编号、骰子表达式、结果、备注。把它们组合成一个数据模型章节 元信息 若干场景 场景 场景编号 地点描述 角色 台词 骰点记录 角色 角色名称 类型 属性 背景信息 骰点记录 时间 场景编号 表达式 结果 备注这个模型很朴素但它有两个关键优点一是所有内容都能用纯文本表示不需要数据库二是每个字段都对应一个文件或一行记录方便脚本处理。这就是后面工程实现的基础。如果只靠人工整理流程是跑团结束 → 打开聊天记录 → 把消息逐条复制 → 按剧情顺序排列 → 补充描写 → 发布。这套流程在章节少的时候没问题但一旦进入第七话、第八话就会出现两个典型问题第一聊天记录中的骰点消息经常被玩家消息打断漏记概率高第二不同玩家对同一剧情的记忆不一致返工成本大。引入工程化流程后顺序变成跑团时用统一格式快速记录 → 场景写成分散的 Markdown 文件 → 脚本自动读取场景文件和骰点日志 → 生成一篇完整的 replay 博文 → 发布。这样做的核心变化是记录和发布分离写场景时不用考虑最终排版发布时不用重新整理场景。3. 环境准备与目录结构设计接下来进入实操。整套流程只需要三个基础工具Git、Python 3、一个文本编辑器。VSCode 或者 Obsidian 都可以如果用 Obsidian还能顺便获得双向链接和关系图谱对跑团系列的长期维护有帮助。Python 版本建议使用 3.8 及以上本文的脚本没有依赖第三方库用标准库即可完成。创建项目之前先设计目录结构。这里给出一套适合长篇跑团系列的目录trpg-replay/ ├── README.md ├── characters/ │ └── kokushibo.md ├── scenes/ │ ├── 07-01-moonlight.md │ ├── 07-02-battle.md │ └── 07-03-aftermath.md ├── logs/ │ └── rolls.jsonl ├── templates/ │ ├── character_template.md │ └── scene_template.md ├── scripts/ │ ├── dice.py │ └── build_replay.py └── export/ └── 虚舟之村07.md每个目录的职责characters/角色卡目录。每个角色一个 Markdown 文件文件名建议使用角色名称的英文或拼音避免中文文件名在某些平台上的兼容性问题。scenes/场景文件目录。文件名规则是“章节号-场景序号-场景名.md”。例如07-01-moonlight.md表示第七话第一个场景。这个命名规则决定了后续脚本的排序逻辑。logs/存放骰点日志。使用 JSONL 格式每行一条 JSON 记录。templates/模板目录。新建角色卡或场景时从模板复制避免手写格式不一致。scripts/Python 脚本目录。export/脚本自动生成的发布文件目录。这个目录下的内容可以由脚本覆盖不建议手动编辑。创建目录的 Git 命令mkdir -p trpg-replay/{characters,scenes,logs,templates,scripts,export} cd trpg-replay git init这里真正容易踩坑的地方是场景文件的命名一定要统一。如果第一个文件叫07-1-moonlight.md第二个叫07-02-battle.md脚本按字符串排序时会把07-1排在07-10之后造成顺序错乱。建议场景序号统一用两位数补零01、02、03不要混用一位数和两位数。4. 用 Markdown 模板固化场景、角色卡与章节信息4.1 角色卡模板角色卡的作用是保证每个角色在每话中的属性、背景、关系一致。尤其是黑死牟这类持续出场的 Boss玩家会在意他的行为是否前后矛盾。templates/character_template.md示例--- name: 角色名称 role: PLAYER / NPC / BOSS chapter_first_seen: 07 status: active abilities: - 能力一 - 能力二 --- ## 角色背景 这里写角色背景。对于 NPC可以记录他与主线的关系、当前目标、隐藏动机。 ## 关键事件 - 第 07 话本角色首次登场/态度转变/受伤/获胜。实际创建黑死牟的角色卡时可以在characters/kokushibo.md中这样写--- name: 黑死牟 role: BOSS chapter_first_seen: 07 status: active abilities: - 月之呼吸 - 血鬼术 --- ## 角色背景 黑死牟是《鬼灭之刃》世界观中的上弦之壹本章作为“虚舟之村”剧情的核心威胁登场。对于 TRPG 记录来说角色卡不需要写成维基百科只需要记录本团设定下的事实。 ## 关键事件 - 第 07 话于月光下与主角团对峙进入战斗场景。这段话是模板示例不是要求你去照抄原著设定。重点是只要每次跑团前先看角色卡主持人就不会说出和之前剧情矛盾的角色行为。4.2 场景模板场景模板是 replay 中最常用的模板。它需要承载位置、出场角色、台词、场景描述。templates/scene_template.md示例--- scene_id: 07-01-moonlight chapter: 07 title: 月光下的芦苇原 location: 虚舟之村·村外芦苇原 characters: - 黑死牟 - 主角团 --- ## 场景描述 用两三句话描述场景画面、气氛和镜头感。 ## 对话记录 **主持人**对场景的叙述和 NPC 台词。 **玩家A**玩家角色的行动或台词。 ## 本场景骰点备注 - 在台词中提及的检定统一通过 dice.py 写入日志并在汇总时自动生成表格。为什么要用 YAML front matter因为脚本需要读取scene_id来匹配骰点记录读取chapter来判断属于哪一话。用 front matter 比在正文第一行写# 07-01更稳定即使正文格式调整元信息也不会丢。实际使用中场景文件不一定要写成完整文章。跑团刚结束时的记录可以很碎比如--- scene_id: 07-02-battle chapter: 07 title: 黑死牟出手 location: 虚舟之村·村口 characters: - 黑死牟 - 主角团 --- 黑死牟拔出刀。月光被刀光切碎。 主持人敏捷检定。 玩家Adice.py 1d20 -s 07-02-battle -n 敏捷检定发布前再润色即可。这对应了之前说的“记录和发布分离”场景文件先保真发布时再优化。4.3 章节索引建议在仓库根目录维护一个README.md作为整个系列的索引# 鬼灭角色桌 TRPG Replay 系列 ## 虚舟之村篇 - 第 01 话至第 06 话scenes/ 目录中的 01- 至 06- 前缀文件。 - 第 07 话黑死牟很满意场景文件见 scenes/07-*.md导出见 export/虚舟之村07.md。索引文件不需要每次跑团后手动大改只需要在发布新章节时增加一行。这样整个系列的结构始终有入口不会因为文件越来越多而失序。5. 用 Python 脚本记录骰点dice.py跑团中会滚动各种面数的骰子。常见的有 d20二十面骰用于判定、d6六面骰用于伤害、d100百分比用于稀有事件。手动记录骰点的问题是玩家容易漏写或者只记录“成功/失败”不记录原始数值导致后续无法复盘。更好的方案是写一个脚本输入骰子表达式自动生成结果并追加到日志文件。日志使用 JSONL 格式每行一个 JSON 对象方便后续脚本读取。scripts/dice.py完整代码#!/usr/bin/env python3 # 文件路径scripts/dice.py TRPG 骰点记录脚本。 用法示例 python scripts/dice.py 1d20 -s 07-02-battle -n 敏捷检定 python scripts/dice.py 2d6 -s 07-02-battle -n 伤害骰 import argparse import json import random import re from datetime import datetime from pathlib import Path from typing import Optional, Tuple DEFAULT_LOG Path(logs/rolls.jsonl) def parse_dice(expr: str) - Optional[Tuple[int, int]]: 解析类似 1d20、2d6、1d100 的表达式。 match re.fullmatch(r(\d)d(\d), expr.lower()) if not match: return None return int(match.group(1)), int(match.group(2)) def roll( expr: str, scene_id: str, note: str , log_file: Path DEFAULT_LOG, ) - dict: 执行一次骰点并写入 JSONL 日志。 parsed parse_dice(expr) if parsed is None: raise ValueError(f无法解析骰子表达式: {expr}请使用类似 1d20 的格式) count, sides parsed results [random.randint(1, sides) for _ in range(count)] record { time: datetime.now().isoformat(timespecseconds), scene_id: scene_id, expr: expr, results: results, total: sum(results), note: note, } log_file.parent.mkdir(parentsTrue, exist_okTrue) with log_file.open(a, encodingutf-8) as fp: fp.write(json.dumps(record, ensure_asciiFalse) \n) print(json.dumps(record, ensure_asciiFalse, indent2)) return record if __name__ __main__: parser argparse.ArgumentParser(descriptionTRPG 骰点记录脚本) parser.add_argument(expr, help骰子表达式例如 1d20、2d6、1d100) parser.add_argument(-s, --scene, requiredTrue, help场景编号例如 07-02-battle) parser.add_argument(-n, --note, default, help本次骰点备注) parser.add_argument(-l, --log, defaultstr(DEFAULT_LOG), help日志文件路径) args parser.parse_args() roll(args.expr, args.scene, args.note, Path(args.log))脚本的关键逻辑parse_dice用正则表达式解析数字d数字格式返回骰子数量和面数。random.randint(1, sides)模拟一次掷骰。每次掷骰后脚本把时间、场景编号、表达式、每个骰子的结果、总和、备注写入一行 JSON。追加模式打开文件意味着不会覆盖历史记录。这里真正需要理解的是 JSONL 的设计它比 Excel 更轻量比 TXT 更结构化。每行一条记录即使文件增长到几千行Python 也可以用简单的 for 循环逐行读取。后续如果要生成统计报表只需要按scene_id或expr字段分组即可。运行示例python scripts/dice.py 1d20 -s 07-02-battle -n 黑死牟登场时的先攻检定 python scripts/dice.py 2d6 -s 07-02-battle -n 月之呼吸造成的伤害预期输出是格式化后的 JSON 对象{ time: 2025-06-01T21:30:00, scene_id: 07-02-battle, expr: 1d20, results: [ 15 ], total: 15, note: 黑死牟登场时的先攻检定 }注意运行两次相同的命令会生成两条记录因为每次掷骰都应该是独立事件。如果你需要可复现的测试结果可以在脚本中添加随机种子但跑团场景下不需要。6. 用 Python 脚本自动生成整篇 replaybuild_replay.py有了场景文件和骰点日志下一步就是自动汇总。这个脚本是整套流程的核心价值所在它把分散的 Markdown 场景文件按章节号读取出来把对应的骰点记录插入到每个场景末尾然后生成一篇可以直接发布的 Markdown 博文。scripts/build_replay.py完整代码#!/usr/bin/env python3 # 文件路径scripts/build_replay.py 根据 scenes/ 目录和 logs/rolls.jsonl 生成整篇 replay 文档。 用法示例 python scripts/build_replay.py 07 import argparse import json import sys from pathlib import Path SCENE_DIR Path(scenes) LOG_FILE Path(logs/rolls.jsonl) OUTPUT_DIR Path(export) def load_rolls_by_scene(scene_ids: set[str], log_file: Path) - dict[str, list[dict]]: 从 JSONL 日志中读取指定场景的骰点记录按 scene_id 分组。 if not log_file.exists(): return {} rolls: dict[str, list[dict]] {} with log_file.open(r, encodingutf-8) as fp: for line in fp: line line.strip() if not line: continue record json.loads(line) scene_id record.get(scene_id) if scene_id in scene_ids: rolls.setdefault(scene_id, []).append(record) return rolls def build(chapter: str, scene_dir: Path, log_file: Path, output_dir: Path) - Path: 构建单个章节的 replay 文档。 prefix f{chapter}- scene_files sorted(scene_dir.glob(f{prefix}*.md)) if not scene_files: raise FileNotFoundError(f在 {scene_dir} 中没有找到以 {prefix} 开头的场景文件) scene_ids {path.stem for path in scene_files} rolls load_rolls_by_scene(scene_ids, log_file) parts [ !-- 本文件由 build_replay.py 自动生成请勿手动修改 --, , ] for scene_file in scene_files: content scene_file.read_text(encodingutf-8) parts.append(content) parts.append() scene_rolls rolls.get(scene_file.stem, []) if scene_rolls: parts.append(### 本场景骰点记录) parts.append() parts.append(| 时间 | 表达式 | 结果 | 合计 | 备注 |) parts.append(| --- | --- | --- | --- | --- |) for record in scene_rolls: result_str , .join(str(r) for r in record[results]) parts.append( | {} | {} | {} | {} | {} |.format( record[time], record[expr], result_str, record[total], record[note], ) ) parts.append() output_dir.mkdir(parentsTrue, exist_okTrue) output_file output_dir / f虚舟之村{chapter}.md output_file.write_text(\n.join(parts), encodingutf-8) return output_file if __name__ __main__: parser argparse.ArgumentParser(description生成 replay 章节文档) parser.add_argument(chapter, help章节号例如 07) parser.add_argument( --scene-dir, typePath, defaultSCENE_DIR, help场景目录路径, ) parser.add_argument( --log-file, typePath, defaultLOG_FILE, help骰点日志路径, ) parser.add_argument( --output-dir, typePath, defaultOUTPUT_DIR, help导出目录路径, ) args parser.parse_args() try: target build(args.chapter, args.scene_dir, args.log_file, args.output_dir) except FileNotFoundError as exc: print(f构建失败: {exc}, filesys.stderr) sys.exit(1) print(f构建成功: {target})脚本的工作流程分四步第一步读取scenes/目录所有以07-开头的 Markdown 文件第二步从骰点日志中筛选出这些场景对应的记录第三步把每个场景的原文写入输出文件如果该场景有骰点记录就追加一个 Markdown 表格第四步将结果写到export/虚舟之村07.md。这里有一个容易被忽略的细节场景文件的scene_id是文件名去掉.md后得到的字符串例如07-02-battle。脚本必须依靠scene_id字段把骰点挂到正确的场景下面。如果某一句话的骰点备注写错了场景编号汇总时就会挂到别的场景检查起来非常难受。所以在跑团过程中-s参数一定要填对。运行命令python scripts/build_replay.py 07如果一切正常预期输出构建成功: export/虚舟之村07.md生成的文件可以在任意 Markdown 编辑器中打开也可以直接发布到支持 Markdown 的博客平台。如果你用的是 CSDN把生成文件内容复制到编辑器即可如果需要微调建议先在本地预览再粘贴避免因为平台解析差异造成格式错乱。7. 运行结果与效果验证任何脚本写完之后都要验证不能“能跑就认为没问题”。针对这套流程建议按以下顺序检查。第一步验证骰点日志是否追加成功。运行两次dice.py后打开logs/rolls.jsonl应该看到两行 JSON。如果文件不存在或者只有一行说明脚本可能没找到正确的日志路径或者运行目录不对。第二步验证场景文件是否齐全。运行python scripts/build_replay.py 07前先确认scenes/下至少有07-01、07-02、07-03中的任意一个文件。如果脚本提示找不到场景文件大概率是目录结构或前缀写错了。第三步检查导出文件的骰点表格是否按场景分组。打开export/虚舟之村07.md滚动到战斗场景应该能看到“本场景骰点记录”表格。如果某个场景没有表格有两种可能这个场景没有写入骰点记录或者记录中的scene_id和场景文件名不一致。第四步确认编码。在 Windows 上使用 Git Bash 时中文文件名偶尔会出现乱码。生成脚本已经用encodingutf-8读写文件但如果你用旧版本的文本编辑器打开建议统一使用 UTF-8 编码保存。下面是一个验证示例假设scenes/07-02-battle.md存在并且你运行了python scripts/dice.py 1d20 -s 07-02-battle -n 敏捷检定 python scripts/dice.py 2d6 -s 07-02-battle -n 伤害骰 python scripts/build_replay.py 07打开export/虚舟之村07.md在“07-02-battle”场景的末尾你会看到类似这样的表格时间表达式结果合计备注2025-06-01T21:30:001d201515敏捷检定2025-06-01T21:31:002d64, 59伤害骰如果表格里的数值和你刚才掷出的结果对不上第一件事不是怀疑脚本而是检查rolls.jsonl里是否有重复记录。因为脚本是追加写入重复运行dice.py会产生多条记录这是预期行为但如果你不小心运行了多次表格里会出现重复行。如果运行build_replay.py时报错先看错误类型。文件不存在检查路径JSON 解析失败检查rolls.jsonl是否被手动改成非 JSON 格式排序不对检查场景文件名编号是否补零。8. 常见问题与排查思路这套流程看似简单实际使用中会遇到一些共性问题。把它们整理成一张排查表方便你直接对照。问题现象可能原因排查方式解决方案运行 dice.py 后日志文件不存在当前工作目录不在项目根目录检查命令执行路径确认logs/是相对于项目根目录的路径在项目根目录下运行脚本或使用--log参数指定绝对路径中文内容在导出文件中乱码文件保存编码不是 UTF-8用支持 UTF-8 的编辑器打开源文件统一所有文件编码为 UTF-8场景文件顺序不对文件名编号没有补零查看scenes/目录下的文件名使用01、02、09、10这种两位数命名某个场景的骰点表格为空scene_id不匹配对比场景文件名和rolls.jsonl中的scene_id运行dice.py时传入正确的-s参数同一场景出现重复骰点重复运行了 dice.py检查rolls.jsonl的追加记录如果误运行手动删除多余行养成运行前确认场景编号的习惯build_replay.py 报 JSON 解析错误rolls.jsonl被手动编辑破坏打开最后几行检查是否出现换行缺失从备份恢复或删除日志后重建其中最高频的问题是场景编号不匹配。推荐的做法是在跑团开始前主持人先把本话所有场景文件名列出来跑团过程中按照场景编号逐个记录骰点。这样比跑完再补录要靠谱得多。9. 最佳实践与工程建议这套流程从能用到好用还有几个关键实践值得注意。第一命名规范要在一开始就定死。章节号用两位数场景序号用两位数全部小写单词之间用连字符。比如07-01-moonlight.md不要写第7话场景1月光.md。英文文件名的好处是跨平台兼容也方便脚本排序。章节标题可以用中文放在 front matter 的title字段里不影响最终展示效果。第二跑团现场记录和后期润色要分开。跑团结束后的一小时内直接打开场景模板把能记得的台词、行动、判定结果快速写下来不要追求文笔。这时候记录的是事实越原始越好。等发布前再润色场景描述补充氛围描写。如果跳过这一步隔一天再写很多细节就丢了。第三骰点日志是整条内容链的可信来源不要随意篡改。rolls.jsonl使用追加模式写入天然保留历史记录。如果遇到玩家质疑“上次是不是过了检定”直接查日志文件按scene_id过滤答案一目了然。在设计上日志文件不应该是 Excel 或数据库因为文本文件的审计性最好任何改动都能通过 Git 历史追踪。第四用 Git 管理整个仓库。每次跑团结束后创建一次提交提交信息写成“第07话跑团记录黑死牟登场”。发布前如果修改了场景描写再次提交。这样整个系列的演进过程都有版本记录。多人协作时建议每位玩家只编辑自己的角色文件场景文件由主持人维护骰点日志由主持人统一写入避免多人写同一个 JSONL 造成冲突。第五发布到 CSDN 时注意标题和标签。标题建议沿用系列名称和章节号例如“【鬼灭角色桌replay】虚舟之村07黑死牟很满意”这样读者能通过系列标签检索到全部分期。正文中可以保留场景模板中的 YAML front matter也可以删除如果你的博客平台会自动渲染 Markdown 表格保留骰点表格更直观如果平台对 YAML 支持不友好发布前去掉 front matter 即可。第六不要为了工具而工具。如果只是一次性的跑团记录用备忘录就够了但只要你打算长期更新一个系列就值得花半小时搭建这套目录。它真正降低的不是“写”的成本而是“找”和“对”的成本。到第十话的时候你能三分钟定位“黑死牟在第一话是否见过主角团”这才是工程化最大的收益。10. 总结与后续学习方向这套流程的核心并不复杂用 Markdown 管理场景和角色卡用 JSONL 管理骰点用 Python 脚本做自动汇总用 Git 做版本管理。每个环节都可以替换比如用 YAML 代替 JSONL用其他脚本语言代替 Python但数据模型和流程思想是通用的。下一步实践建议是不要急着追求完美先在下一次跑团中跑通最小闭环。即使只坚持了一话你也能感受到“记录与发布分离”带来的轻松感。跑团时只需要在关键判定点敲一条命令结束后把场景记录补全运行一次构建脚本就有了发布素材。如果还想继续深入可以从这几个方向扩展给脚本增加--web参数把 Markdown 段落直接转成 HTML在logs/rolls.jsonl基础上做骰点统计计算每个玩家整季的先攻平均分引入 Obsidian 或 VitePress把角色卡、场景、章节索引连成知识网络或者给build_replay.py增加模板引擎让生成文件更贴近你常发布的内容格式。对长期连载的跑团系列来说最重要的不是每一话都写得多华丽而是当你写到第十话、第二十话的时候还能准确知道第七话的黑死牟在月光下做了什么事、玩家掷出的那一次 1d20 到底是成功还是失败。工程化不能替你想出精彩的剧情但它能确保这些精彩不被遗忘。