我不是游戏开发者,我有一个与游戏无关的工作。几个星期前,我生病了,结果我有了大量的空闲时间,开始创建一个逻辑谜题来保持我的头脑清醒。
427次提交和23天的编码后,它变成一个真正的东西,真正的陌生人可以玩。关于62k行的手写代码加上另外12k行的翻译表格,224个测试,9台机器,月度成本不足100美元。
从中产生了两个东西。第一个是我自豪的谜题生成器。第二个是几乎作为一个附加功能而建造的分析通道,它最终成为整个仓库中最有价值的部分,我在数据到达之前无法快速阅读它,无法及时行动。第二个部分是你应该偷走的部分。
在这里没有链接,我不是在卖东西,我有一个真正的工作,不需要这个来赚钱。
什么是这个东西
一个房间的平面图,包含房间和家具,以及一个演员阵容,其中一个演员是受害者。每个嫌疑人都提供证词,限制他们可能在哪里。"他和箱子在同一列"。"她正好在鲁本上五行北"。你把每个嫌疑人放在一个与所有证词一致的独特方格,然后你就可以命名凶手了。在更难的板块中,一名嫌疑人的整个证词都是谎言,你不知道哪一个。
第一部分:生成器,简要说明,因为它是表现良好的部分
暴力解决方案不是在桌面上。专家板是9名嫌疑人在81个方格上的阵容,这是94,670,977,328,928,000次放置。因此,引擎是一个小的约束求解器:每个嫌疑人的可能方格是位掩码,提示编译成掩码操作,交叉点直到没有变化。
生成器从后往前运行。构建一个真实的真理,生成每个嫌疑人可以对其说出的每个真实陈述,然后添加陈述直到板子被锁定到一个答案:
for (const clue of addOrder) {
chosen.push(clue);
if (chosen.length < P + 3) continue;
const { count, capped } =
countSolutions(compile(ctx, chosen, P), 2, NODE_CAP);
if (!capped && count === 1) { unique = true; break; }
}
countSolutions(..., 2)在两种情况下停止:因为我永远不会需要知道一个板子有40,000种解决方案,只要它有多于一个。
NODE_CAP是一个微妙的部分。搜索被限制,并且被限制的计数被评估为"多个"而不是它达到的部分数字。达到限制只能导致板子被拒绝,而不是被接受。倒退这个顺序,你将在发表谜题时将两个答案一起发表,并且玩家如果正确会被告知他们是错误的。
在困难和专家级别中,一名嫌疑人是一个同谋,所有他们说的话都是假的。这使得独特性更强:玩家不知道谁在撒谎,所以必须是嫌疑人和他们的位置的对,跨越每个假设:
function liarTotal(ctx, clues, P, suspects, limit = 2) {
let total = 0;
for (const L of suspects) {
const { count, capped } =
countSolutions(compile(ctx, clues, P, L), limit, LIAR_NODE_CAP);
total += capped ? limit : count;
if (total >= limit) return total;
}
return total;
}
除非计数为1,否则板子将被拒绝。
难度不是配置的,而是测量的。求解器有第二种模式:传播直到卡住,假设一个嫌疑人的一个方格,传播再次寻找矛盾。一个板子需要多少个假设是什么,难度就是它的数字。每个等级宣布一个被接受的范围,生成器重新尝试直到板子落入这个范围。
真实数字,六个新板子:
| cast | cells | clues | what-if | props | generate |
|---|---|---|---|---|---|
| beginner | 5 | 25 | 6.8 | 0.0 | 2.0 |
| easy | 6 | 36 | 7.7 | 0.0 | 3.2 |
| medium | 7 | 49 | 9.5 | 0.3 | 4.0 |
| hard | 8 | 64 | 12.8 | 1.0 | 5.3 |
| expert | 9 | 81 | 18 | 2.0 | 5.8 |
初学者和容易是纯粹的传播,没有猜测。专家成本884ms,因为它拒绝了大量的板子,直到找到一个在范围内的板子,这是为什么谜题会在预先生成时在一个银行中。
家具是真的。属性是阻塞者,求解器会推理,这意味着添加一个可以静默地改变难度或破坏独特性。因此,每个属性都试验:将其放置,重新求解,从头开始,只有当测量的假设轮数与原始轮数相同时才保留它:
const baseline = roundsWith();
data.objects.push(
{ id: nextId++, type,
class: 'blocker',
cells: [cell],
dress: 1
});
if (roundsWith() === baseline) {
taken.add(cell);
added++; break;
}
data.objects.pop();
nextId--; //它改变了难度:废除它
这是我熟悉的部分。现在是未知的部分。
第二部分:通道,实际上是这个帖子的重点
我将游戏部署在一个大型HTML5门户上。流量从零到21,500人在一周内增加了。并且我立即遇到了一个我认为几乎没有人警告你的事实。
门户玩家发送给你零分析事件。
不是少数。零。我的遥测是被同意的,一个门户构建在一个iframe中,显示一个同意弹出窗口既是违反规则的,也是商业上不合理的。没有弹出窗口意味着没有同意,这意味着没有事件。单个我最需要了解的人口是门户玩家,恰恰是没有数据的那一群。每个我建造的分析仪表板都在测量我的网站上友好的已经转化的少数人。
解决方案是我最希望另一个开发者从这个帖子中收获的东西。
你的游戏已经在写数据。使用这个写入。
客户在每个有意义的操作时自动保存游戏状态。服务器已经在更新状态。因此,我只在玩家的计数器实际移动时才在同一语句中追加一个紧凑的元组:
[time_ms, placed, xmarks, hints, mistakes]
限制在150个元组以便一个长时间的游戏不能无限增长行。没有第二次回程。没有新表格。没有跟踪像素。没有第三方。没有同意问题,因为这是一个关于游戏本身的操作数据,而不是关于人。并且它在每个平台上都工作得一样好,包括那些剥夺了你的分析堆栈的平台。
UPDATE games SET state = $1, time_ms = $2, mistakes = $3, hints = $4, trace = CASE
WHEN coalesce(jsonb_array_length(trace), 0) >= 150 THEN trace
WHEN trace IS NULL THEN jsonb_build_array($6::jsonb)
--只有当玩家自己的计数器移动时
WHEN trace -> -1 -> 1 IS DISTINCT FROM $6::jsonb -> 1
OR trace -> -1 -> 2 IS DISTINCT FROM $6::jsonb -> 2
OR trace -> -1 -> 3 IS DISTINCT FROM $6::jsonb -> 3
OR trace -> -1 -> 4 IS DISTINCT FROM $6::jsonb -> 4
THEN trace || jsonb_build_array($6::jsonb)
ELSE trace END
WHERE id = $5
这是一个游戏的跟踪实际上是从列中读取的,每个元组都是 [time_ms, placed, xmarks, hints, mistakes]
[[ 22000,0, 3,0,0],[ 41000,0, 7,0,0],[ 58000,1, 7,0,0],[ 76000,2, 9,0,0], [103000,2,14,0,0],[118000,3,14,0,0],[155000,3,18,0,0],[249000,3,18,1,0], [268000,4,18,1,0],[287000,4,21,1,0],[301000,5,21,1,1],[333000,5,24,1,1], [352000,6,24,1,1],[371000,7,24,1,1]]
十四个元组,大约200个字节,你可以读取整个会话。三十秒的纯粹的线索阅读,然后没有人被放置。稳定的进展到三个嫌疑人。然后一个94秒的间隔在索引7处,什么都没有移动,一个提示被取出,这是阻碍的墙。一个错误在提示之后。然后最后四个在70秒内打开。将这个乘以每个游戏在每个平台上,你就可以自行建造这个通道。
这个陈述中有两个陷阱,都是花费了我的真实时间。
jsonb || jsonb在两个数组上进行连接会将右侧的元素拆分在一起。一个裸露的trace || $6会将所有元组之后的元组拆分成松散的数字,直到你试图读取它时才会注意到。包裹它,追加一个元素,它恰好是一个数组。
计数器是由服务器端从状态中计算的,而不是从客户端取的。跟踪必须描述行,因为它是唯一的东西你将在之后有。
四个问题,以及为什么答案的形状比数字更重要
一旦每个游戏都带有跟踪,你就可以问出一个页面浏览通道无法表达的问题。最终我有了四个,每个需要一个不同的答案形状。
"他们停在哪里?" 不是平均值。是众数。"大多数人在第三个嫌疑人停下"是你可以采取行动的东西。"平均值的放弃游戏达到4.2个嫌疑人"是一个数字,描述了没有人,暗示了什么也没有。
"停在哪里看起来是什么样的?" 每个步骤之间的间隔,作为百分位数,单独地是最后一个间隔:玩家在钟表停止之前停留多久。最后一个是停滞的形状。一个长尾在最后一个间隔上是一个玩家在无法解决的板子上凝视。
"游戏多长?" 四分位数,分开结果。赢得游戏和放弃游戏是两种不同的分布,平均值描述了两种情况都没有。
"他们完成后会不会跳回来?" 是否一个完成的游戏是接着一个新的游戏的开始,30分钟内,根据谁拥有游戏。这个是最锐利的工具,因为如果玩家获胜不会开始第二个板子,你的结果屏幕就是漏洞,且这是一个完全不同的bug与板子太难。没有这个分开,我会花一周时间调节难度,解决一个问题,它生活在模式中。
然后是板子进度桶,它将"他们放弃"转化为四种不同的诊断:
-
从未放置。打开板子,放置了没有人。这是一个理解或第一次印象的失败,而不是难度。
-
不到半数
-
半数或更多
-
所有人放置了,未被指控。他们解决了它,并且没有按下按钮。这是一个纯粹的UI失败,它是不可见的,除了这个一个。
还有一个,门户通常会忽略的:一个活跃的游戏,在一天后沉默了,是一个放弃,谁也没有标记。如果你只计算显式结果,你的通道会给你打扮得体。计算沉默的也要。
它实际上告诉我什么,以及我为什么会生气
这是两个数字,它重新定义了每切。每个板子都达到一个终端状态时,98%都以胜利告终。游戏不是太难了。几乎所有完成的板子都是赢家。
但是,每个初学者板子都被启动时,只有49%才会完成。每个容易板子都有78%的完成率。
容易比初学者难得多,完成率提高了29个百分点。这看起来疯狂,直到你看到通道真正测量的是什么:初学者是新到的玩家在跳来跳去,而容易是那些已经决定留下的玩家。你的入口点测量的是你的跳来跳去率,而不是你的难度曲线。我花了时间使初学者更温和。初学者从来不是问题。
另一个,我花了太晚才发现:门户会计算一个"玩家"从开始游戏时开始。我的主屏幕、难度选择器和模式选择器都在板子之前。每一个屏幕都是门户计算的载入人数中的一员,但从来没有玩过游戏。删除所有三个,以便在启动时直接发牌,是在三周内发出的任何特性值得的。
最好的bug,通道找到了,而任何玩测试都无法找到的:在第一次访问时,应用程序可能会在白屏上卡住,刷新可以解决它。应用程序标记自己准备好时,会解析或拒绝session fetch。一个卡住的fetch既不会解析,也不会拒绝。它在我发出的每个版本中都是。第一次访问者是唯一的访问者,他们是唯一一个你无法见到的用户。
这是我的复制卡片上写的,30天,每个源:
- 他们停在哪里最多:
1个嫌疑人放置 - 他们停滞前最后的间隔:
10s中位数,54s在 p90 - 板子被填满并且从未被指控:
30 - 玩家完成一个案子并且在30分钟内开始另一个:
76%
如果你即将发射你的第一个游戏,请在发射之前构建这个
这是推荐,整个原因我写了这个帖子。
-
在你有玩家之前构建通道。流量潮流到达一次。你无法再次运行它。每个你没有在前面建造的你都没有测量的东西都是你无法回答的问题。建造了大多数我的通道是在潮流到达时,我正在阅读一个星期前的数据,一个我无法回去的星期。
-
测量特定于你的游戏的名词。不是浏览次数,不是会话。对于我来说,它是放置索引:他们在停止时放置了哪个嫌疑人。选择一个数字,它在你的游戏中意味着进步,并且在每次保存时记录它。通道中所有好东西都来自这个单个整数。
-
在你正在写的写入上附加。不要添加一个遥测管道。你的游戏已经持久化了状态。追加到该行,在该语句中。它花费了零钱,它无法与它描述的状态脱离同步,它在每个平台上都工作得一样好,包括那些剥夺了你的分析堆栈的平台。
-
假设你的最佳流量来源将发送给你零事件。同意遥测,广告拦截器,iframe,门户,隐私默认值。设计为只有一个东西可以从一个玩家中获得:你的服务器写入的行。
-
将每个指标按结果和来源分开之前不要看。赢得游戏与放弃游戏。门户与网站。每次我看了一个混合数字,它告诉我一个假的,舒适的东西。
-
在数据到达之前就知道每个答案对应什么动作。这是我犯了一个错误的,花了时间的昂贵的错误。我有了通道。它是正确的。它坐在那里,告诉我入口点泄漏了50%的玩家,我不知道如何快速阅读它,以便在流量到达时采取行动。写下,在前面:"如果未放置桶大于25%,我会做X"。然后它是一个触发器,而不是一个图表。
而实际的教训
引擎是舒适的部分。我知道如何编写一个求解器。然而,我不知道差异在哪里:一个游戏没有玩家和一个游戏有玩家之间的差异几乎完全是由发生在前15秒内的事件组成的,并且你无法看到它们,除非你故意在前面测量它们。
我没有在门户上达到我需要的玩家数量,主要是因为我在门户流量到达时发现了所有这些,差了一个星期。它有一点刺激,但我不伤心。这个构建花了几个小时的好时光,托管很便宜,东西完成了,我无论如何都会在一年内将其留在这里。
对每个做这个工作的人表示尊敬。求解器是容易的部分。
评论 (0)