一段时间前,我一直觉得很想实现像《王国之征途》(Reigns)一样的游戏风格:加权牌抽取、基于旗帜的故事链条、当counter计数器达到0或100时会导致游戏结束的计数器。每次我开始这项工作,结果总是被JSON淹没了,或者编写了一个复杂的电子表格。

所以,我建立了一个自定义的DSL (Domain-Specific Language)。这个语言叫做 TahtLang。这个语言中的卡片定义看起来像这样 :

税收提议  (card:tax)
    持有者: 角色:顾问
    权重: 1.0
    权重: 3.0 当财政部计数器< 20 时
    >  商人们要求降低税率,陛下。
    * 减少税收:财政部计数器 -15, 人民计数器 +10
    * 提高税收:财政部计数器 20, 人民计数器 -20

语言的格式是纯文本,使用制表符进行缩进,易于编写并阅读。这个语言可以在多个文件中分散保存卡片,按游戏的不同部分来保存。

TahtLang 的各种特征包括:

  • 权重(可以根据条件或者是固定的)
  • 需要通过条件来激活的牌组(gate)
  • 计数器上的锁转防止卡片显示
  • 基于条件的故事链条

下面是一个更复杂的例子 :

战争宣言(card:war)
    持有者:角色:将军
    权重: 0.5
    权重: 2.0 当军队计数器 > 70 时
    需要: !flag:战争, 军队计数器 > 40
    锁转: 15
    > 敌人威胁国界!
    * 发动战争: +flag:战争,军队计数器 -10,卡片:《战斗》@5 时
    * 求和:财政部计数器 -30

战争卡片权重: 0.5 是其初始抽取的概率,但是它得到加权,当军队强大时会得到加强。 require 则是阻止卡片抽取,除非没有战争在进行,并且军队人口数必须大于40。 锁转: 15则会在15个回合内阻止这张牌再次显示。发动战争会设置 +flag:战争 并设置 卡片:《战斗》@5,一个环状卡片,会在5个回合后再次呈现,之后会展开一个故事链条。

下面是这个语言的核心概念:

  • 屏层:代表游戏中的角色。
  • 旗帜:用来存储游戏状态的标识,对卡片的抽选和显示有影响。
  • 权重卡片:加重卡片的概率,条件有条件或无条件。
  • 门控卡片:只在特定条件下才抽取的卡片。

TahtLang 有一个名为 TahtLang-Toolchain 的工具链,它包含了以下内容:

pip install git+https://github.com/tahtlang/tahtlang.git

之后可以使用以下命令 :

tahtlang game.taht              # 在终端中试玩一下
tahtlang compile game.taht      # 将TahtLang编译为JSON,用于Unity或Godot或Web
tahtlang stats game.taht        # 运行100次模拟,分析游戏平衡

compile命令中,可以将所有的计数器、旗帜、角色、卡片、条件和权重全部转化为JSON格式。游戏中的实际逻辑和显示应该自己根据TahtLang-Toolchain生成的结构进行实现。

TahtLang-Toolchain 还包含了一个名为 stats的统计命令,它有助于分析游戏的平衡,在100次模拟中计算出各种指标 :

从未出现在游戏中的卡片:       card:forest_ambush, card:peace_treaty (0/100 运行)
最致命的计数器:                 counter:people (被杀 61% 的情况)
平均统治时间:                23.4 次变换
出现在5次模拟中的卡片:      card:volcano_event, card:tax_reform

如果你有60个卡片而其中一个在100次模拟中根本没有出现,说明你设置的权重不正确。这个工具有助于我多次推迟上线的游戏版本。

以下是其他额外的TahtLang语言的内容 :

  • 语法树(Tree-sitter)加上LLSP服务(用来进行语法高亮、自动补全、诊断)
  • tahtlang stats --runs 500 如果100次不是足够的
  • tahtlang init 可以在不了解TahtLang的基础上生成一个starter game

如果你曾经想过制作一个Reigns风格的游戏或者故事卡片游戏或者包含加权随机抽取和状态旗帜的游戏,希望你能尝试了解一下TahtLang有多少帮助。