战斗中的怪兽:用D&D探索应用指标

2019年夏天,我在看我的孩子们玩。不管是在室内还是室外,不管有没有道具,他们(现在仍然如此)总是在发明某种游戏。当时,他们正在重演宠物小精灵的战斗,并配以音效、特殊动作和疯狂的声音。我开始思考如何在青少年时期悄悄到来之前保留这种创造精神。龙与地下城》。

我自己玩过两次D&D:一次是在我11岁的时候,和隔壁的男孩一起玩,那基本上是毫无意义的角色创造练习;另一次是在我长大成人后,我差点死于尴尬。但是对于我的儿子和他的朋友,我可以这样做;我提出要为他们做地下城主(DM)。然而,我犯了一个重要的新手错误。我没有限制我儿子可以带多少玩家来参加游戏。最后,我和六个10岁的男孩(有时还有一个6岁的女孩)一起开始我的DM冒险。我低估了这项工作和混乱的程度。

你可能会问自己:”好吧,但这到底和指标有什么关系?”答案是。时间性。

特定领域的应用指标很重要。测量http吞吐量(这几乎是所有应用指标描述的重点)并不能充分说明你的应用在做什么。例如,如果我的代码中的一个错误导致它跳过整个代码路径,我可以得到非常棒的吞吐量。一个成功的响应代码不能告诉你,你的应用程序正在做一个微妙的错误的事情。写单元测试太容易了,因为它不会浮现出错误。因此,我做了开发者倡导者做的那件事:我提交了一个演讲的摘要,以证明我关于度量的观点。然后,我在整个夏天都推迟了工作(尽管在我的事情清单上有它)。

九月初的时候,我出现了轻微的恐慌:我必须吸收关于D&D的大量信息,以便能够带领我的儿子和他的朋友们玩这个游戏,而且我有一个全新的申请和演讲需要从头开始准备。为了节省时间,我把它们放在一起。下面是《怪物战斗》的故事,这个应用程序使用D&D 5E的战斗规则和应用指标来分析所发生的事情,让经典的D&D怪物互相对抗。

对于SpringOne 2019,我创建了一个带有一些应用端点的Spring Boot应用程序,以驱动战斗遭遇,并使用Micrometer和Spring Boot执行器,使用Prometheus端点定义和浮现应用指标。我在iPad上使用Game Master 5(Lion’s Den)来管理我的活动,它有一个可以以XML格式导出的汇编。我的应用程序从该文件中检索了1063个怪物。我为战斗创建了一个游戏引擎,设置了2-5个怪物的遭遇战,让它们轮番对决,直到剩下一个怪物。我测量了回合和遭遇战的数量和持续时间,以及一些关于攻击的有效性和最致命和最不致命的怪物的统计数据。我熬过了这次谈话,但我对最后的应用并不满意。

时间过去了,是时候重新审视2020年DevNexus的申请了,因为(像任何一个好的倡导者一样)我又提交了演讲。我打算尝试一些不同的指标库并比较它们的能力,这给了我一个借口去做我最喜欢的事情之一:重构我的代码!我决定从不那么冗长且更一致的维基页面中获取怪物定义,并将消化后的结果存储在一个JSON文件中。我把游戏引擎移到了一个核心库中,并对其进行了重组,以便与这个更干净的数据源一起工作。尽管有更少的怪物(215个)可供处理,但我对最终的结果更加满意。

D&D的简要概述

D&D是一种角色扮演游戏。管理员创造了环境和背景,有问题需要解决,有挑战需要克服,有生物需要互动。然后,玩家讲述他们那一半的故事,描述他们的角色在该环境中的行为。掷骰子是维持这个联合叙事中双方平衡的关键机制,从生成怪物的统计数据到决定鬼鬼祟祟的流氓是做出史诗般的发现还是抠脚趾头摔下楼梯。

我担心的是战斗。保持六个孩子的注意力已经很困难了;停止行动去检查规则不太可能有帮助。虽然战斗的结构很简单,但要弄清不同种类的攻击的具体行为却很费劲。这将会变得非常呆板,但我希望这能帮助那些没有玩过D&D的人跟随我的(错误的)度量冒险。我们将从怪物的特征和能力开始,然后逐步上升到战斗和攻击规则。

兽皮书中的每只怪物都有一个类型、一个大小、一个装甲等级(AC)和打击点数(HP)。类型和大小都是有界限的列表,基本上是枚举。装甲等级是一个简单的整数值,表示击中(成功攻击)一个生物的难度。低AC值的生物比高AC值的生物更容易被击中。命中率代表一个生物可以承受多少伤害。一些例子。

  • 伪龙是一条护甲等级为13的小龙,它有7(2d4+2)个命中点,这意味着DM可以使用平均值(7)或者掷两个4面骰子并在结果上加2。
  • 魔法师是一个中等天体,AC值为17,HP值为136(16d8+64)。
  • Terrasque,D&D 5e的史诗级怪物,是一个巨大的怪物,AC值为25,HP值为676(33d20+330)。

每个怪物都有六个核心能力分数:力量、灵巧、体质、智力、智慧和魅力。它也可能精通某些技能。总的来说,技能和熟练程度是修饰的基础,使生物更容易(或有时更难)进行某种攻击。

大多数情况下,怪物都有或携带武器,用来对对手造成不同类型的伤害。掷骰子,特别是在D&D中用于大多数测试和检查的标志性的20面骰子,用修改器进行增强,并将结果与对手的AC进行比较。如果攻击值符合或超过AC,则攻击成功。然而,有两个 “关键 “注意事项:如果掷出1,则攻击自动失败(关键失误或关键失败);如果掷出20,则攻击自动成功(关键击中)。关键命中的成功率很高,攻击者可以获得双倍的武器伤害。

还有另一种攻击,比如说一个法术被施放,或者一个生物喷火。这种攻击类型只是发生了,然后由目标做出拯救动作:掷出D20(加上修饰符)来躲避或拯救自己免受伤害。其结果与难度等级(DC)而不是护甲等级相比较。在某些情况下,为攻击指定了一个任意的DC;在其他情况下,DC是基于攻击者的技能或能力。

举例来说,古代绿龙可以用不同的方式对对手造成伤害。它可以。

  • 使用它的爪子–D20卷+15(根据龙的能力和熟练程度进行修改),击中10英尺内的一个目标。如果它击中了,目标会受到22的平均伤害或4d6+8点的砍伤。22(4d6+8).
  • 使用它的咬力–D20卷+15,击中15英尺内的一个目标。如果命中,目标将受到19(2d10+8)的穿刺伤害和10(3d6)的中毒伤害。
  • 使用它的尾巴–一个D20卷轴+15,击中20英尺内的一个目标。如果击中,目标将受到17(2d8+8)重击伤害。
  • 使用它的毒气–一种特殊的龙的能力。它的文字遵循大多数需要蓄力的攻击的模式:”龙在90英尺的范围内呼出毒气。该区域内的每个生物都必须进行DC 22宪法规定的蓄力,蓄力失败会受到77(22d6)毒气伤害,蓄力成功会受到一半的伤害。”

古代绿龙是一种可怕的野兽;它还能在一个回合内进行两次爪击和一次咬合攻击。冒险党都很疯狂。我只是说说而已。

一个战斗遭遇战有一个简单的总体结构。

  1. 如果攻击者是隐藏的或隐秘的,可能会有一个突击回合。
  2. 参与者按主动权顺序排列,从谁先到谁后,通过滚动D20。
  3. 开始一轮战斗,每个生物按主动权顺序轮流上场。
  4. 重复上一步骤,直到战斗结束。

在桌面上,战斗可以因为任何原因而结束。如前所述,我的应用遵循高地人模式:战斗在只剩下一个生物时结束。

提出问题

在决定衡量什么以及如何衡量时,我从我丈夫那里得到了一些一流的建议,他对统计数据和仪表盘的感受都比我强烈。”任何人都可以把无意义的数据在仪表盘上变得漂亮。专注于你想知道的行动。”换句话说,如果你不能根据结果采取行动,就不要测量或创建仪表盘。

一方面,我很好奇。我想知道这个系统的不同部分是如何一起工作的。一次遭遇战有多少回合(平均)?攻击成功的频率是多少?什么类型的武器伤害最大?什么是最(或最少)致命的怪物?

另一方面,实施选择可以影响遭遇战的结果。骰子的行为、遭遇战中包含的生物、目标选择、平均或可变的伤害。应用监控的原则之一是通过只看外部化的测量来了解你的应用正在发生什么。哪些因素会影响一个遭遇战的长度?如果我对目标的选择方式做了改变,是否会产生可观察到的差异?

核心机制。掷骰子

我首先想到要测量的东西之一是骰子的行为。鉴于掷骰子对游戏流程的重要性,我觉得观察掷骰子的分布很重要。对于我丈夫的观点,如果骰子的滚动非常不均匀,我可以改变随机性的来源或使用一些其他的算法来滚动骰子。当我重构游戏引擎的时候,我创建了一个实用类来执行所有的掷骰子。一个简单的注入点允许记录每次掷出的结果。以千分尺为例,掷骰子的结果是用一个计数器测量的。

Dice.setMonitor((k, v) -> registry.counter(“dice.rolls”, “die”, k, “face”, label(v)) .increment())。

标签方法将掷出的整数值(9)转换为填充的字符串(09),用作 “脸 “的标签值。从该计数器收集到的骰子滚动的普罗米修斯数据看起来像这样。

dice_rolls_total{die=”d10″,face=”08″,} 16750.0

dice_rolls_total{die=”d10″,face=”09″,} 16724.0

dice_rolls_total{die=”d10″,face=”06″,} 16804.0

使用这些数据创建的第一个Grafana仪表板如图1所示。

图片[1]-战斗中的怪兽:用D&D探索应用指标-一一网
图1.滚动频率的图表,采取1

d12的图表应该引起你的注意;它与其他所有的图表都不同。这里面有一个错误。虽然我写了测试来确保我可以解析所有类型的掷骰子公式,但我没有写测试来验证每个骰子都被正确使用。在一个打脸的时刻,我发现在一个switch语句中缺少一个case。这是聚合指标的第一次胜利。由于数据量太大,我不可能用日志条目来发现这个错误,而我的测试显然错过了这个问题。平心而论,结对编程会发现这个问题,但我是独自编码的。从我正在写的演讲的角度来看,我不可能计划得更好。

图2显示了该仪表板的更新版本。请注意这些图表中Y轴之间的比例差异。用来决定每次攻击是否命中的D20,比其他任何一个都使用得更频繁。

图片[2]-战斗中的怪兽:用D&D探索应用指标-一一网
图2.滚动频率图,第2次

战斗的遭遇

Monster Combat有两个应用程序,一个是Spring Boot应用程序,一个是Quarkus应用程序。两者都使用核心引擎来创建和驱动遭遇战和回合。这两个应用程序中的相关代码看起来都是这样的。

Encounter encounter = beastiary.buildEncounter()
        .setHowMany(howMany)
        .setTargetSelector(pickOne(howMany))
        .build();

List results = new ArrayList<>();
while (!encounter.isFinal()) {
    RoundResult result = encounter.oneRound();
    metrics.endRound(result);
    results.add(result);
}
metrics.endEncounter(encounter, results.size());

复制代码

解释一下该片段中的一些元素:兽皮书保存了所有的生物;howMany是一个自我解释的参数;metrics是一个对象,允许应用程序以它喜欢的任何方式衡量结果。我本想使用Micrometer、MP Metrics和OpenTelemetry,但由于其中一些库的限制,我没有完全达到目的。轮次结果列表在HTTP响应中返回给客户端。一轮遭遇战的日志条目看起来像这样。

: : oneRound:
    Troll(LARGE GIANT){AC:15,HP:84(8d10+40),STR:18(+4),DEX:13(+1),CON:20(+5),INT:7(-2),WIS:9(-1),CHA:7(-2),CR:5,PP:12}(31/86.0)
    Pit Fiend(LARGE FIEND){AC:19,HP:300(24d10+168),STR:26(+8),DEX:14(+2),CON:24(+7),INT:22(+6),WIS:18(+4),CHA:24(+7),SAVE:[DEX(+8),CON(+13),WIS(+10)],CR:20,PP:14}(313/313.0)

: attack: miss: Troll(36) -> Pit Fiend(100)
: attack: miss: Troll(36) -> Pit Fiend(100)
: attack: hit> Troll(36) -> Pit Fiend(97) for 9 damage using Claws[7hit,11(2d6+4)|slashing]
: attack: hit> Pit Fiend(97) -> Troll(10) for 22 damage using Bite[14hit,22(4d6+8)|piercing]
: attack: MISS: Pit Fiend(97) -> Troll(10)
: attack: HIT> Pit Fiend(97) -> Troll(0) for 34 damage using Mace[14hit,15(2d6+8)|bludgeoning]

: oneRound: survivors
    Pit Fiend(LARGE FIEND){AC:19,HP:300(24d10+168),STR:26(+8),DEX:14(+2),CON:24(+7),INT:22(+6),WIS:18(+4),CHA:24(+7),SAVE:[DEX(+8),CON(+13),WIS(+10)],CR:20,PP:14}(304/313.0)

复制代码

输出显示了一个巨魔和一个坑道恶魔的能力得分,包括他们在回合开始时的当前命中率。巨魔开始时的86点命中率只剩下31点(36%健康),而地煞到目前为止还没有受到任何伤害(100%健康)。在爪子攻击之前,巨魔对坑妖的19AC两次失手。坑妖对巨魔的15AC进行了一次成功的咬合攻击,然后掷出了1的关键性失误,接着又掷出了20的关键性击中。巨魔无法在这次攻击中存活下来,而坑妖赢得了这次遭遇战。

命中和失误

如前所述,我想了解的事情之一是攻击成功的频率。命中率和失误率哪个更常见?上面的日志片段刚好显示了我们前面提到的四种结果中的每一种。

  • HIT>– 一个关键的命中(滚动20)。
  • MISS:– 一个关键性的失误(掷出1)。
  • hit> – 命中:要么攻击值(卷轴+修改器)大于或等于目标的AC,要么是需要蓄力的法术攻击(总是命中)。
  • miss:– 失败:攻击值小于目标的AC值。

为了衡量命中和未中,我用下面的方法将三个布尔值(命中、关键和保存)转换为一个字符串值,用于一个hitOrMiss 标签。

String hitOrMiss() {
    return (isCritical() ? "critical " : "")
            + (isSaved() ? "saved " : "")
            + (isHit() ? "hit" : "miss");
}

复制代码

我创建了一个测量攻击难度的分布摘要:要么是武器式攻击的AC,要么是需要蓄力的攻击的DC。我给这个测量值附加了两个标签:hitOrMiss 标签,以及另一个用来捕捉攻击类型,要么是针对目标护甲等级的武器式攻击(attack-ac ),要么是需要蓄力的法术式攻击(attack-dc )。

registry.summary("attack.success",
                    "attackType", event.getAttackType(),
                    "hitOrMiss", event.hitOrMiss())
                    .record((double) event.getDifficultyClass());

复制代码

这个分布摘要的普罗米修斯数据看起来像这样。

# HELP attack_success
# TYPE attack_success summary
attack_success_count{attackType="attack-ac",hitOrMiss="miss",} 65.0
attack_success_sum{attackType="attack-ac",hitOrMiss="miss",} 1124.0
attack_success_count{attackType="attack-ac",hitOrMiss="critical hit",} 13.0
attack_success_sum{attackType="attack-ac",hitOrMiss="critical hit",} 229.0
attack_success_count{attackType="attack-ac",hitOrMiss="critical miss",} 10.0
attack_success_sum{attackType="attack-ac",hitOrMiss="critical miss",} 179.0
attack_success_count{attackType="attack-dc",hitOrMiss="hit",} 6.0
attack_success_sum{attackType="attack-dc",hitOrMiss="hit",} 92.0
attack_success_count{attackType="attack-dc",hitOrMiss="saved hit",} 9.0
attack_success_sum{attackType="attack-dc",hitOrMiss="saved hit",} 134.0
attack_success_count{attackType="attack-ac",hitOrMiss="hit",} 133.0
attack_success_sum{attackType="attack-ac",hitOrMiss="hit",} 2050.0
# HELP attack_success_max
# TYPE attack_success_max gauge
attack_success_max{attackType="attack-ac",hitOrMiss="miss",} 22.0
attack_success_max{attackType="attack-ac",hitOrMiss="critical hit",} 22.0
attack_success_max{attackType="attack-ac",hitOrMiss="critical miss",} 20.0
attack_success_max{attackType="attack-dc",hitOrMiss="hit",} 22.0
attack_success_max{attackType="attack-dc",hitOrMiss="saved hit",} 19.0
attack_success_max{attackType="attack-ac",hitOrMiss="hit",} 20.0

复制代码

我第一次为这个数据创建图表时,发现只看图例还有一个问题,如图3所示。你能发现它吗?

图片[3]-战斗中的怪兽:用D&D探索应用指标-一一网
图3.攻击成功率,第1次

你必须是一个有点D&D的书呆子才行,但 “关键的保存命中 “并不存在。我在某个地方还有一个错误。试图找到它是那种6个阶段的调试冒险之一。我绝对达到了 “这怎么能行?”的阶段。经过一些代码重构和修复,我又尝试了一次可视化的攻击成功,如图4所示。

图片[4]-战斗中的怪兽:用D&D探索应用指标-一一网
图4.攻击成功,第2步

遗憾的是,有些东西还是错了。考虑到掷骰子的工作方式,失误应该比关键命中或关键失误更常见。我花了一些时间来确定哪里出了问题,因为这个错误不在应用程序代码中。我在从HTML到JSON的最初转换中犯了一个错误。正则表达式很厉害,但有时也很危险。我在解析盔甲等级时漏掉了一个 “+”,这意味着最高的AC值是9!呜呼。聚合度量的另一个胜利!

在纠正了这个错误之后,我就可以创建图5所示的仪表盘了。

图片[5]-战斗中的怪兽:用D&D探索应用指标-一一网
图5.攻击成功,第3次

这个仪表板告诉我什么?攻击的命中率比不命中率高。正如预期的那样,关键性命中和关键性失误的可能性同样不大。虽然需要蓄力的攻击(attack-dc)并不频繁,但它们被蓄力的次数比不蓄力的多。

看下图,命中和未命中的平均难度或护甲等级与你所期望的相符:命中的攻击对相当低的AC成功,反之,未命中的也是如此。保存命中的频率也可以用一个非常低的平均DC来解释。关键命中和未命中的难度等级与总体平均水平一致,这是有道理的,因为它们完全由滚动决定(20分成功,1分失败)。

伤害

伤害是什么?任何特定攻击的平均伤害量是多少?某些类型的伤害是否一直比其他类型的高?为了回答这些问题,我创建了另一个分布摘要,记录了一次攻击造成的伤害,并标明是否命中,是AC(武器式)还是DC(法术式)攻击,以及是哪种伤害(击打、刺穿等等)。

registry.summary("round.attacks",
        "hitOrMiss", event.hitOrMiss(),
        "attackType", event.getAttackType(),
        "damageType", event.getType())
        .record((double) event.getDamageAmount());

复制代码

由此产生的普罗米修斯数据看起来就像这个非常简略的例子。

 # HELP round_attacks
# TYPE round_attacks summary
round_attacks_count{attackType="attack-ac",damageType="bludgeoning",hitOrMiss="hit",} 49.0
round_attacks_sum{attackType="attack-ac",damageType="bludgeoning",hitOrMiss="hit",} 684.0
round_attacks_count{attackType="attack-ac",damageType="fire",hitOrMiss="miss",} 6.0
round_attacks_sum{attackType="attack-ac",damageType="fire",hitOrMiss="miss",} 0.0
miss",} 0.0
round_attacks_count{attackType="attack-ac",damageType="slashing",hitOrMiss="critical hit",} 9.0
round_attacks_sum{attackType="attack-ac",damageType="slashing",hitOrMiss="critical hit",} 216.0
round_attacks_count{attackType="attack-ac",damageType="fire",hitOrMiss="hit",} 2.0
round_attacks_sum{attackType="attack-ac",damageType="fire",hitOrMiss="hit",} 41.0
...
# HELP round_attacks_max
# TYPE round_attacks_max gauge
round_attacks_max{attackType="attack-ac",damageType="bludgeoning",hitOrMiss="hit",} 31.0
round_attacks_max{attackType="attack-ac",damageType="fire",hitOrMiss="miss",} 0.0
round_attacks_max{attackType="attack-ac",damageType="slashing",hitOrMiss="miss",} 0.0
round_attacks_max{attackType="attack-ac",damageType="fire",hitOrMiss="critical hit",} 30.0
round_attacks_max{attackType="attack-ac",damageType="slashing",hitOrMiss="critical miss",} 0.0
round_attacks_max{attackType="attack-ac",damageType="slashing",hitOrMiss="critical hit",} 32.0
round_attacks_max{attackType="attack-ac",damageType="fire",hitOrMiss="hit",} 22.0
round_attacks_max{attackType="attack-ac",damageType="slashing",hitOrMiss="hit",} 24.0
...

复制代码

创建这样的数据让你体会到维度时间序列数据中标签的cardinality。虽然损害类型是一个有边界的集合,但它比hitOrMissattackType 有更多的可能值,从而产生更多的独特排列组合。当这个集合与Prometheus为服务/工作实例添加的额外标签结合在一起时,Grafana陷入困境。我最终使用Prometheus的报告规则,发出了一个新的时间序列,其中包含了一段时间内(15分钟)损害量的每秒钟平均增加率。作为一个旁观者,这也证明了聚合的意义:不可能从这个基于多个来源的输入计算的比率倒退到一个单独的记录结果。如果我需要这样做,我就需要使用日志条目或分布式跟踪,这就是为什么它们也是可观察性故事整体中的重要元素。但我想说的是。

图6中的仪表盘有两个图,显示了在过去6个小时内基于攻击类型的15分钟内的损害值的 “增加”。增加值是通过将速率值乘以时间间隔来计算的,这就把数值放回了人类可读/相关的刻度(Y轴)。

图片[6]-战斗中的怪兽:用D&D探索应用指标-一一网
图6.每种攻击类型的伤害

这个仪表板中的图表显示,攻击伤害的平均量与我们对攻击类型的预期相符:关键打击的平均伤害是武器式攻击对目标AC的普通打击平均伤害的两倍。虽然法术式攻击对DC的平均伤害变化较大,但救赎确实会减少一半的伤害,这也符合预期。我以前没有意识到的是,这些攻击基本上是默认的关键命中。哎哟!”。这是一件好事,因为它们都比较少见,而且被救的次数比较多。

图片[7]-战斗中的怪兽:用D&D探索应用指标-一一网
图7.按伤害类型划分的伤害

什么类型的伤害是最常见的?哪些是最有害的?我们可以使用同一数据的不同维度来创建图7所示的仪表板。这些图表显示,虽然毒药和闪电攻击相对较少,但它们的伤害却不成比例。砍击、穿刺和击打类型是最常见的伤害类型,它们聚集在平均值附近,其造成的伤害程度的变化比其他类型小得多。

图片[8]-战斗中的怪兽:用D&D探索应用指标-一一网
图8.按攻击类型划分的毒药和闪电伤害

图8集中显示了造成毒药和闪电伤害的攻击。大多数雷电伤害来自于需要蓄力的攻击(灵巧)。另一方面,毒药伤害在任何一种攻击中都有相当的数量。

图片[9]-战斗中的怪兽:用D&D探索应用指标-一一网
图9.按攻击和命中类型划分的毒药和闪电伤害

图9进一步区分了毒药和闪电伤害攻击的关键和保存命中。数据中存在一些跳动或空白,因为我们正在寻找一个狭窄的因素组合,这些因素并不总是一起出现。我从这张图中得出了一些粗略的结论。带有毒药伤害的关键一击会很疼,而且你对此也无能为力。蓄力一击的平均毒害量是低级别的角色可以承受的。为起始角色提供一个体面的体质修改器是一个好的计划。另外,如果你没有一个像样的灵巧修改器,请避免与投掷照明的怪物发生冲突。

有些攻击会施加条件而不是造成伤害。条件存在于图7左边的原始发生数中,但由此产生的效果并没有以有用的方式显示在伤害的原始表示中。战斗引擎需要更好地处理条件,这可能会让条件被分离成自己的类别,以更好地理解它们对战斗遭遇的影响。例如,有些强加的条件限制或阻止在一个回合中使用多次攻击。这有什么影响?在未来的雨天里,有很多东西可以玩!

战斗遭遇和回合

除了观察骰子的行为外,在前面的章节中,我丈夫称之为 “可操作 “的数据并不多。现在让我们来看看更直接受实施选择影响的数据:每次遭遇战的回合数。在一次交锋结束时,回合数被记录到另一个分布摘要中。

 registry.summary("encounter.rounds",
                "numCombatants", label(e.getNumCombatants()),
                "targetSelector", e.getSelector(),
                "sizeDelta", label(e.getSizeDelta()))
                .record((double) totalRounds);

复制代码

如果你没有注意到,我大部分时间都在使用分布摘要,因为它提供了一个计数和一个总和,我可以用它来汇总各个来源并计算平均数。我也会得到一个最大值,但从整体趋势的角度来看,我觉得这不太有趣。这个摘要中收集的数据看起来是这样的。

 # HELP encounter_rounds
# TYPE encounter_rounds summary
encounter_rounds_count{numCombatants="05",sizeDelta="05",targetSelector="HighestHealth",} 18.0
encounter_rounds_sum{numCombatants="05",sizeDelta="05",targetSelector="HighestHealth",} 136.0
encounter_rounds_count{numCombatants="04",sizeDelta="00",targetSelector="LowestHealth",} 7.0
encounter_rounds_sum{numCombatants="04",sizeDelta="00",targetSelector="LowestHealth",} 58.0
encounter_rounds_count{numCombatants="05",sizeDelta="02",targetSelector="LowestHealth",} 86.0
encounter_rounds_sum{numCombatants="05",sizeDelta="02",targetSelector="LowestHealth",} 775.0
encounter_rounds_count{numCombatants="06",sizeDelta="03",targetSelector="SmallestFirst",} 91.0
encounter_rounds_sum{numCombatants="06",sizeDelta="03",targetSelector="SmallestFirst",} 935.0
encounter_rounds_count{numCombatants="03",sizeDelta="00",targetSelector="Random",} 22.0
encounter_rounds_sum{numCombatants="03",sizeDelta="00",targetSelector="Random",} 157.0
encounter_rounds_count{numCombatants="04",sizeDelta="02",targetSelector="Random",} 95.0
encounter_rounds_sum{numCombatants="04",sizeDelta="02",targetSelector="Random",} 654.0
encounter_rounds_count{numCombatants="05",sizeDelta="04",targetSelector="Random",} 35.0
encounter_rounds_sum{numCombatants="05",sizeDelta="04",targetSelector="Random",} 261.0
encounter_rounds_count{numCombatants="05",sizeDelta="01",targetSelector="SmallestFirst",} 42.0
encounter_rounds_sum{numCombatants="05",sizeDelta="01",targetSelector="SmallestFirst",} 398.0

复制代码

为了解释这些标签。

  • numCombatants应该是不言自明的,因为它是遭遇中的生物数量。
  • sizeDelta是遭遇中最大的生物和最小的生物之间的大小差异。当一个巨大的生物(5)面对一个微小的生物(0)时,会出现最大值(5)。delta被填充成一个两个字符的数值字符串。
  • targetSelector包含了我允许生物选择攻击目标的几种方式之一的表示。最高健康值、最低健康值、最大优先、最小优先、随机和对峙(当只有两个战斗人员时)。

从这些收集到的数据中,我们可以看一下某些因素是如何影响每场遭遇战的回合数的。我自己的理论是,有更多战斗人员的遭遇,平均会比战斗人员少的遭遇有更多回合。安全的赌注,对吗?让我们来看看图10中的仪表盘。

图片[10]-战斗中的怪兽:用D&D探索应用指标-一一网
图10.按战斗人员数量计算的每次遭遇战的回合数

假设被证明是正确的,结果也相当一致。战斗人员数量上的遭遇分布相当均匀,每次遭遇的平均回合数随着你从2个战斗人员进展到6个而增加。

还有哪些因素可能会改变一场遭遇战的回合数?为了研究这些因素,我把重点放在两个子集上:有2名战斗人员的遭遇和有4名战斗人员的遭遇。

在两个战斗人员的情况下,不存在谁攻击谁的问题。然而,我预计,大小之间的巨大差异(一个巨大的生物对一个微小的生物)会导致非常快速的遭遇,数据支持这一假设,如图11所示有一些变化。

图片[11]-战斗中的怪兽:用D&D探索应用指标-一一网
图11.按大小差异划分的2个生物的遭遇回合

首先要注意的是,在频率上有一个极端的倾斜。兽皮书中的大多数生物都是中等大小的,这使得有极端大小差异的相遇相当罕见。我们还再次穿针引线,查看了一个狭窄的数据切片(只有两个生物的相遇),这就造成了一些差距。也就是说,两个大小相同的生物之间的相遇比大小明显不匹配的生物之间的相遇要多很多回合。当有更多的战斗人员时也是如此吗?让我们来看看图12中4个战斗员的遭遇。

图片[12]-战斗中的怪兽:用D&D探索应用指标-一一网
图12.按大小差异划分的4个生物遭遇战的回合数

确实如此。有时能证实自己的直觉是件好事。在频率上仍有一些偏差,但与大部分大小相同的怪物的遭遇始终比不匹配的遭遇花费更多时间。

当我在写我的战斗引擎时,我不得不做一个决定:一个生物应该如何决定攻击其他什么生物?这种选择是否会产生差异?正如前面简单提到的,我创建了5种不同的方法来选择要攻击的生物:选择最大的、最小的、最健康的、受伤最重的,或者随机选择任何其他战斗人员。该算法是为整个遭遇战选择的。收集的数据中的targetSelector标签表明使用了哪种算法。图13显示了4种生物遭遇战中目标选择方法的数据。

图片[13]-战斗中的怪兽:用D&D探索应用指标-一一网
图13.按目标选择的4种生物遭遇战的回合数

这个仪表板向我展示了几件事。首先,各种目标选择算法的分布相当均匀,这使得我们不太可能看到由于稀缺性造成的数据倾斜(看Y轴)。在两轮累计之间看(中间的条形图),以及底部的图,我觉得相当有信心地说,先选择最大的战斗人员并不是一个获胜的策略。所有其他人之间的变化要小得多,但随机选择一个目标始终会导致更短的遭遇。

综上所述

我在编写这个应用程序时学到了很多东西,无论是作为一个试图了解D&D如何运作的新DM,还是作为一个开发人员,都超越了剪切和粘贴的例子,以使用和理解我所收集的数据。特定于应用的指标发现了我的测试没有发现的错误,并让我看到我所做的实施选择的影响(或缺乏影响)。在现实世界中,实时服务的指标收集一直在进行,提供了一个统计基线,可以用来发现应用程序更新时的行为变化。

我提到,我希望做的一件事是比较指标库的能力。我很快就发现,我无法做到这一点。用OpenTelemetry做度量的Java库在年初的时候还没有准备好。我将在2020年9月的J4K上给出这个演讲的更新版本,所以我有足够的时间再次尝试。

我还遇到了MicroProfile Metrics的麻烦,因为它只发出预先消化的直方图值,使得我无法使用Prometheus和Grafana来计算不同来源聚集的数据的比率或平均数。因此,Quarkus应用程序也在使用micrometer。起初,它直接使用micrometer库,但后来我为Quarkus创建了一个Micrometer扩展,看看我可以在多大程度上提供使用Micrometer与Quarkus的一流体验。

如果你有兴趣搞这个,所有的源代码都在github的仓库里:https://github.com/ebullient/monster-combat。

欢迎大家提供意见和反馈。

The postMonsters in combat: exploring application metrics with D&Dappeared first onDevOps Conference.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享