@ak--47/dungeon-master 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- gaming.js — v1.6 human-inspection queries (DuckDB)
|
|
3
|
+
--
|
|
4
|
+
-- Every query is keyed to a story id in gaming.js's `stories` export;
|
|
5
|
+
-- the machine-checked verdicts come from:
|
|
6
|
+
-- node scripts/verify-stories.mjs dungeons/vertical/gaming/gaming.js --data-prefix verify-gaming
|
|
7
|
+
-- Generate first:
|
|
8
|
+
-- node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js verify-gaming
|
|
9
|
+
-- Run this file:
|
|
10
|
+
-- duckdb -c ".read dungeons/vertical/gaming/gaming.sql"
|
|
11
|
+
-- ============================================================
|
|
12
|
+
|
|
13
|
+
-- ── identity-resolution prelude ─────────────────────────────
|
|
14
|
+
-- avgDevicePerUser: 2 — resolve device_id → canonical distinct_id via the
|
|
15
|
+
-- USERS shards so uniques are identity-correct even for device-only rows
|
|
16
|
+
-- (pre-auth steps of the first funnel carry device_id only).
|
|
17
|
+
CREATE OR REPLACE VIEW users AS
|
|
18
|
+
SELECT * FROM read_json_auto('data/verify-gaming-USERS*.json', sample_size=-1, union_by_name=true);
|
|
19
|
+
|
|
20
|
+
CREATE OR REPLACE VIEW device_map AS
|
|
21
|
+
-- profiles store the device pool under the legacy "anonymousIds" key
|
|
22
|
+
-- (buildIdentityMap in lib/verify/identity.js reads the same field)
|
|
23
|
+
SELECT unnest("anonymousIds") AS device_id, distinct_id FROM users;
|
|
24
|
+
|
|
25
|
+
CREATE OR REPLACE VIEW ev AS
|
|
26
|
+
-- ::VARCHAR casts — user_id sniffs as UUID, device_id as VARCHAR; DuckDB
|
|
27
|
+
-- refuses to coalesce mixed types
|
|
28
|
+
SELECT coalesce(m.distinct_id::VARCHAR, e.user_id::VARCHAR, e.device_id::VARCHAR) AS uid,
|
|
29
|
+
e.time::TIMESTAMP AS t,
|
|
30
|
+
e.*
|
|
31
|
+
FROM read_json_auto('data/verify-gaming-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
32
|
+
LEFT JOIN device_map m ON e.device_id = m.device_id;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
-- ── H1-compass-heavy-rewards ────────────────────────────────
|
|
36
|
+
-- heavy compass users (2+ "use item" w/ Ancient Compass, ~48%) earn ~1.5x
|
|
37
|
+
-- reward_gold and reward_xp on quest turned in (measured 1.56x / 1.58x —
|
|
38
|
+
-- bonus-quest clones are built at the boosted rate)
|
|
39
|
+
WITH cnt AS (
|
|
40
|
+
SELECT u.distinct_id AS uid,
|
|
41
|
+
count(e.uid) FILTER (WHERE e.event = 'use item' AND e.item_type = 'Ancient Compass') AS n
|
|
42
|
+
FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id GROUP BY 1
|
|
43
|
+
)
|
|
44
|
+
SELECT CASE WHEN c.n >= 2 THEN 'heavy' ELSE 'light' END AS cohort,
|
|
45
|
+
count(DISTINCT c.uid) AS users,
|
|
46
|
+
round(avg(e.reward_gold), 1) AS avg_gold,
|
|
47
|
+
round(avg(e.reward_xp), 1) AS avg_xp
|
|
48
|
+
FROM ev e JOIN cnt c ON e.uid = c.uid
|
|
49
|
+
WHERE e.event = 'quest turned in'
|
|
50
|
+
GROUP BY 1 ORDER BY 1;
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
-- ── H2-cursed-week ──────────────────────────────────────────
|
|
54
|
+
-- injected Curse deaths cluster in days 40-47 of each user's own timeline.
|
|
55
|
+
-- Measured on 48d+ lifetime users: per-day density in/out ≈ 36x; raw in/out
|
|
56
|
+
-- count ≈ 2.7 (out-of-window span is ~10x longer)
|
|
57
|
+
WITH firsts AS (SELECT uid, min(t) AS t0, max(t) AS tN FROM ev GROUP BY 1),
|
|
58
|
+
longlife AS (SELECT uid, t0, tN FROM firsts WHERE tN >= t0 + INTERVAL 48 DAY),
|
|
59
|
+
curse AS (
|
|
60
|
+
SELECT e.uid, epoch(e.t - l.t0) / 86400.0 AS dol
|
|
61
|
+
FROM ev e JOIN longlife l ON e.uid = l.uid
|
|
62
|
+
WHERE e.event = 'player death' AND e.cause_of_death = 'Curse'
|
|
63
|
+
)
|
|
64
|
+
SELECT
|
|
65
|
+
count(*) FILTER (WHERE dol BETWEEN 40 AND 47) AS in_window,
|
|
66
|
+
count(*) FILTER (WHERE dol < 40 OR dol > 47) AS out_window,
|
|
67
|
+
(SELECT count(*) FROM longlife) AS longlife_users,
|
|
68
|
+
round((count(*) FILTER (WHERE dol BETWEEN 40 AND 47) / 8.0)
|
|
69
|
+
/ nullif(count(*) FILTER (WHERE dol < 40 OR dol > 47)
|
|
70
|
+
/ ((SELECT avg(epoch(tN - t0)) / 86400.0 FROM longlife) - 8), 0), 1) AS density_ratio
|
|
71
|
+
FROM curse;
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
-- ── H3-guild-rescue ─────────────────────────────────────────
|
|
75
|
+
-- among users with 3+ week-1 deaths: early guild joiners (first 3 days) are
|
|
76
|
+
-- exempt from the spiral → ~2.5x post-week-1 volume vs spiraled peers
|
|
77
|
+
-- (knob ceiling 1/0.3 ≈ 3.3x; both cohorts select front-loaded players)
|
|
78
|
+
WITH firsts AS (SELECT uid, min(t) AS t0 FROM ev GROUP BY 1),
|
|
79
|
+
flags AS (
|
|
80
|
+
SELECT f.uid,
|
|
81
|
+
count(e.uid) FILTER (WHERE e.event = 'player death' AND e.t < f.t0 + INTERVAL 7 DAY) AS early_deaths,
|
|
82
|
+
count(e.uid) FILTER (WHERE e.event = 'guild joined' AND e.t < f.t0 + INTERVAL 3 DAY) AS early_guild,
|
|
83
|
+
count(e.uid) FILTER (WHERE e.t >= f.t0 + INTERVAL 7 DAY) AS post_events
|
|
84
|
+
FROM firsts f JOIN ev e ON e.uid = f.uid GROUP BY 1
|
|
85
|
+
)
|
|
86
|
+
SELECT CASE WHEN early_guild > 0 THEN 'guild_saved' ELSE 'spiral' END AS cohort,
|
|
87
|
+
count(*) AS users, round(avg(post_events), 1) AS avg_post
|
|
88
|
+
FROM flags WHERE early_deaths >= 3 GROUP BY 1 ORDER BY 1;
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
-- ── H4-death-spiral ─────────────────────────────────────────
|
|
92
|
+
-- 3+ week-1 deaths and no early guild → 70% of post-week-1 events dropped.
|
|
93
|
+
-- spiral post/pre event ratio ~0.11 vs healthy; spiral cohort ~11% of the
|
|
94
|
+
-- guild-free population
|
|
95
|
+
WITH firsts AS (SELECT uid, min(t) AS t0 FROM ev GROUP BY 1),
|
|
96
|
+
flags AS (
|
|
97
|
+
SELECT f.uid,
|
|
98
|
+
count(e.uid) FILTER (WHERE e.event = 'player death' AND e.t < f.t0 + INTERVAL 7 DAY) AS early_deaths,
|
|
99
|
+
count(e.uid) FILTER (WHERE e.event = 'guild joined' AND e.t < f.t0 + INTERVAL 3 DAY) AS early_guild,
|
|
100
|
+
count(e.uid) FILTER (WHERE e.t >= f.t0 + INTERVAL 7 DAY) AS post_events,
|
|
101
|
+
count(e.uid) FILTER (WHERE e.t < f.t0 + INTERVAL 7 DAY) AS pre_events
|
|
102
|
+
FROM firsts f JOIN ev e ON e.uid = f.uid GROUP BY 1
|
|
103
|
+
)
|
|
104
|
+
SELECT CASE WHEN early_guild = 0 AND early_deaths >= 3 THEN 'spiral'
|
|
105
|
+
WHEN early_guild > 0 THEN 'guild' ELSE 'other' END AS cohort,
|
|
106
|
+
count(*) AS users,
|
|
107
|
+
round(avg(post_events)::DOUBLE / nullif(avg(pre_events), 0), 3) AS post_pre
|
|
108
|
+
FROM flags GROUP BY 1 ORDER BY 1;
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
-- ── H5-lucky-charm ──────────────────────────────────────────
|
|
112
|
+
-- Lucky Charm Pack buyers (~9% of spenders) pay 2.5x on the pack itself and
|
|
113
|
+
-- get 35% bonus-purchase clones → avg spend ~3.4x non-buyers. Scoped to
|
|
114
|
+
-- NON-whales so H10's 1.8x multiplier doesn't contaminate the ratio.
|
|
115
|
+
WITH lucky AS (SELECT DISTINCT uid FROM ev WHERE event = 'real money purchase' AND product = 'Lucky Charm Pack')
|
|
116
|
+
SELECT
|
|
117
|
+
round(avg(price_usd) FILTER (WHERE uid IN (SELECT uid FROM lucky)), 2) AS lucky_avg,
|
|
118
|
+
round(avg(price_usd) FILTER (WHERE uid NOT IN (SELECT uid FROM lucky)), 2) AS nonlucky_avg,
|
|
119
|
+
count(DISTINCT uid) FILTER (WHERE uid IN (SELECT uid FROM lucky)) AS lucky_users
|
|
120
|
+
FROM ev WHERE event = 'real money purchase' AND price_usd IS NOT NULL
|
|
121
|
+
AND substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f');
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
-- ── H6-strategic-explorers ──────────────────────────────────
|
|
125
|
+
-- strategic explorers (6+ inspect AND 6+ search for clues, ~42% of users):
|
|
126
|
+
-- 85% dungeon completion vs ~54% baseline, 2x treasure (measured 1.7x blend)
|
|
127
|
+
WITH cnt AS (
|
|
128
|
+
SELECT u.distinct_id AS uid,
|
|
129
|
+
count(e.uid) FILTER (WHERE e.event = 'inspect') AS ins,
|
|
130
|
+
count(e.uid) FILTER (WHERE e.event = 'search for clues') AS sea
|
|
131
|
+
FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id GROUP BY 1
|
|
132
|
+
),
|
|
133
|
+
coh AS (SELECT uid, (ins >= 6 AND sea >= 6) AS strategic FROM cnt)
|
|
134
|
+
SELECT c.strategic, count(DISTINCT c.uid) AS users,
|
|
135
|
+
round(avg(e.treasure_value) FILTER (WHERE e.event = 'find treasure'), 1) AS avg_treasure,
|
|
136
|
+
round((count(*) FILTER (WHERE e.event = 'exit dungeon' AND e.completion_status = 'completed'))::DOUBLE
|
|
137
|
+
/ nullif(count(*) FILTER (WHERE e.event = 'exit dungeon'), 0), 3) AS completion
|
|
138
|
+
FROM coh c JOIN ev e ON e.uid = c.uid GROUP BY 1 ORDER BY 1;
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
-- ── H7-shadowmourne ─────────────────────────────────────────
|
|
142
|
+
-- Shadowmourne Legendary drops only after dataset day 45 (2% roll on find
|
|
143
|
+
-- treasure); zero pre-release sightings; owners win ~96% of combats vs ~55%
|
|
144
|
+
WITH owners AS (SELECT DISTINCT uid FROM ev WHERE event = 'find treasure' AND treasure_type = 'Shadowmourne Legendary'),
|
|
145
|
+
combat AS (
|
|
146
|
+
SELECT uid, count(*) FILTER (WHERE outcome = 'Victory')::DOUBLE / count(*) AS win_rate
|
|
147
|
+
FROM ev WHERE event = 'combat completed' GROUP BY 1
|
|
148
|
+
)
|
|
149
|
+
SELECT
|
|
150
|
+
(SELECT count(*) FROM ev WHERE event = 'find treasure' AND treasure_type = 'Shadowmourne Legendary'
|
|
151
|
+
AND t < (SELECT min(t) FROM ev) + INTERVAL 45 DAY) AS pre_release_drops,
|
|
152
|
+
(SELECT count(*) FROM owners) AS owners,
|
|
153
|
+
(SELECT round(avg(win_rate), 3) FROM combat WHERE uid IN (SELECT uid FROM owners)) AS owner_win,
|
|
154
|
+
(SELECT round(avg(win_rate), 3) FROM combat WHERE uid NOT IN (SELECT uid FROM owners)) AS rest_win;
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
-- ── H8-subscriber-tiers ─────────────────────────────────────
|
|
158
|
+
-- Elite: 1.8x quest gold, completion flip → ~0.90; Premium: 1.4x gold,
|
|
159
|
+
-- completion → ~0.82; Free baseline ~0.68 (completion = f + (1-f)·flip)
|
|
160
|
+
SELECT subscription_tier, count(DISTINCT uid) AS users,
|
|
161
|
+
round(avg(reward_gold) FILTER (WHERE event = 'quest turned in'), 1) AS avg_gold,
|
|
162
|
+
round((count(*) FILTER (WHERE event = 'exit dungeon' AND completion_status = 'completed'))::DOUBLE
|
|
163
|
+
/ nullif(count(*) FILTER (WHERE event = 'exit dungeon'), 0), 3) AS completion
|
|
164
|
+
FROM ev GROUP BY 1 ORDER BY 1;
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
-- ── H9-level-gold-scaling ───────────────────────────────────
|
|
168
|
+
-- reward_gold scales with profile level: mult = 1 + 0.15·(level-1).
|
|
169
|
+
-- formula check: hi/lo gold ratio should equal
|
|
170
|
+
-- (1 + 0.15·(mean_hi - 1)) / (1 + 0.15·(mean_lo - 1)) — measured 1.95 vs 1.94
|
|
171
|
+
WITH bucketed AS (
|
|
172
|
+
SELECT CASE WHEN u.level <= 5 THEN 'lo_1_5' WHEN u.level >= 16 THEN 'hi_16_20' ELSE 'mid' END AS bucket,
|
|
173
|
+
u.level, e.reward_gold
|
|
174
|
+
FROM users u JOIN ev e ON e.uid = u.distinct_id AND e.event = 'quest turned in'
|
|
175
|
+
)
|
|
176
|
+
SELECT bucket, count(*) AS quests, round(avg(level), 2) AS mean_level,
|
|
177
|
+
round(avg(reward_gold), 1) AS avg_gold,
|
|
178
|
+
round((1 + 0.15 * (avg(level) - 1)), 3) AS predicted_mult
|
|
179
|
+
FROM bucketed GROUP BY 1 ORDER BY 1;
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
-- ── H10-whale-purchases ─────────────────────────────────────
|
|
183
|
+
-- whales = distinct_id first hex char in ('0','3','6','9','c','f') → 37.5%
|
|
184
|
+
-- of ids nominally, ~63% of spend rows; 1.8x price_usd. Scoped to NON-lucky
|
|
185
|
+
-- buyers so H5's multiplier doesn't contaminate the ratio.
|
|
186
|
+
WITH lucky AS (SELECT DISTINCT uid FROM ev WHERE event = 'real money purchase' AND product = 'Lucky Charm Pack')
|
|
187
|
+
SELECT
|
|
188
|
+
round(avg(price_usd) FILTER (WHERE substr(uid, 1, 1) IN ('0','3','6','9','c','f')), 2) AS whale_avg,
|
|
189
|
+
round(avg(price_usd) FILTER (WHERE substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f')), 2) AS rest_avg,
|
|
190
|
+
count(DISTINCT uid) FILTER (WHERE substr(uid, 1, 1) IN ('0','3','6','9','c','f')) AS whale_users,
|
|
191
|
+
count(DISTINCT uid) FILTER (WHERE substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f')) AS rest_users
|
|
192
|
+
FROM ev WHERE event = 'real money purchase' AND price_usd IS NOT NULL
|
|
193
|
+
AND uid NOT IN (SELECT uid FROM lucky);
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
-- ── H11-alignment-archetype ─────────────────────────────────
|
|
197
|
+
-- deterministic profile mapping: Good alignments → hero, Evil → villain,
|
|
198
|
+
-- rest → neutral. Counts must match EXACTLY; shares ≈ 26/25/49
|
|
199
|
+
SELECT
|
|
200
|
+
count(*) FILTER (WHERE alignment IN ('Lawful Good', 'Neutral Good')) AS good_aligns,
|
|
201
|
+
count(*) FILTER (WHERE archetype = 'hero') AS heroes,
|
|
202
|
+
count(*) FILTER (WHERE alignment IN ('Chaotic Evil', 'Neutral Evil')) AS evil_aligns,
|
|
203
|
+
count(*) FILTER (WHERE archetype = 'villain') AS villains,
|
|
204
|
+
count(*) FILTER (WHERE archetype = 'neutral') AS neutrals,
|
|
205
|
+
count(*) AS total
|
|
206
|
+
FROM users;
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
-- ── H12-combat-ttc-by-tier ──────────────────────────────────
|
|
210
|
+
-- funnel-post compresses combat initiated → combat completed → use item by
|
|
211
|
+
-- tier (Elite 0.30x, Premium 0.70x, Free 1.40x). The story uses the funnel
|
|
212
|
+
-- emulator (median TTC, 6h window); this cross-event MIN→MIN approximation
|
|
213
|
+
-- shows the same ordering for eyeball purposes.
|
|
214
|
+
WITH ci AS (SELECT uid, min(t) AS t0 FROM ev WHERE event = 'combat initiated' GROUP BY 1),
|
|
215
|
+
ui AS (
|
|
216
|
+
SELECT e.uid, min(e.t) AS t1
|
|
217
|
+
FROM ev e JOIN ci ON e.uid = ci.uid AND e.t >= ci.t0
|
|
218
|
+
WHERE e.event = 'use item' GROUP BY 1
|
|
219
|
+
)
|
|
220
|
+
SELECT u.subscription_tier, count(*) AS converters,
|
|
221
|
+
round(median(epoch(t1 - t0)) / 60, 1) AS median_ttc_min
|
|
222
|
+
FROM ci JOIN ui USING (uid) JOIN users u ON u.distinct_id = ci.uid
|
|
223
|
+
GROUP BY 1 ORDER BY median_ttc_min;
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
-- ── H13-prep-magic-number ───────────────────────────────────
|
|
227
|
+
-- prep actions (inspect + search for clues) between first quest accepted and
|
|
228
|
+
-- first fight boss: sweet band 3-6 → 1.3x treasure (measured 1.24x scoped to
|
|
229
|
+
-- non-strategic users); over-prepared 7+ → boss win flipped to ~0.73x sweet
|
|
230
|
+
WITH anchors AS (
|
|
231
|
+
SELECT uid,
|
|
232
|
+
min(t) FILTER (WHERE event = 'quest accepted') AS qa,
|
|
233
|
+
min(t) FILTER (WHERE event = 'fight boss') AS fb
|
|
234
|
+
FROM ev GROUP BY 1
|
|
235
|
+
),
|
|
236
|
+
prep AS (
|
|
237
|
+
SELECT a.uid, count(e.uid) AS n
|
|
238
|
+
FROM anchors a LEFT JOIN ev e ON e.uid = a.uid
|
|
239
|
+
AND e.event IN ('inspect', 'search for clues') AND e.t > a.qa AND e.t < a.fb
|
|
240
|
+
WHERE a.qa IS NOT NULL AND a.fb IS NOT NULL AND a.fb > a.qa
|
|
241
|
+
GROUP BY 1
|
|
242
|
+
),
|
|
243
|
+
bands AS (SELECT uid, CASE WHEN n BETWEEN 3 AND 6 THEN 'sweet' WHEN n < 3 THEN 'low' ELSE 'over' END AS band FROM prep)
|
|
244
|
+
SELECT b.band, count(DISTINCT b.uid) AS users,
|
|
245
|
+
round(avg(e.treasure_value) FILTER (WHERE e.event = 'find treasure'), 1) AS avg_treasure,
|
|
246
|
+
round((count(*) FILTER (WHERE e.event = 'fight boss' AND e.victory = true))::DOUBLE
|
|
247
|
+
/ nullif(count(*) FILTER (WHERE e.event = 'fight boss'), 0), 3) AS boss_win
|
|
248
|
+
FROM bands b JOIN ev e ON e.uid = b.uid
|
|
249
|
+
GROUP BY 1 ORDER BY 1;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gaming.verify.mjs — thin wrapper around the story runner.
|
|
4
|
+
*
|
|
5
|
+
* All verification logic lives in the `stories` export of ./gaming.js;
|
|
6
|
+
* this script just streams the shards and delegates. It is equivalent to:
|
|
7
|
+
*
|
|
8
|
+
* node scripts/verify-stories.mjs dungeons/vertical/gaming/gaming.js --data-prefix verify-gaming
|
|
9
|
+
*
|
|
10
|
+
* Generate first:
|
|
11
|
+
* node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js verify-gaming
|
|
12
|
+
* Run:
|
|
13
|
+
* node dungeons/vertical/gaming/gaming.verify.mjs [prefix] # default verify-gaming
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import readline from 'node:readline';
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
|
+
import config, { stories } from './gaming.js';
|
|
22
|
+
|
|
23
|
+
const PREFIX = process.argv[2] || 'verify-gaming';
|
|
24
|
+
const prefixPath = path.join('data', PREFIX);
|
|
25
|
+
|
|
26
|
+
async function loadShards(suffix) {
|
|
27
|
+
// streaming load: full-fidelity EVENTS shards exceed the readFileSync cap
|
|
28
|
+
const dir = path.dirname(prefixPath), base = path.basename(prefixPath);
|
|
29
|
+
const out = [];
|
|
30
|
+
if (!fs.existsSync(dir)) return out;
|
|
31
|
+
for (const f of fs.readdirSync(dir).filter(f => f.startsWith(`${base}-${suffix}`) && f.endsWith('.json')).sort()) {
|
|
32
|
+
const rl = readline.createInterface({ input: fs.createReadStream(path.join(dir, f)), crlfDelay: Infinity });
|
|
33
|
+
for await (const line of rl) {
|
|
34
|
+
if (line.trim()) out.push(JSON.parse(line));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const events = await loadShards('EVENTS');
|
|
41
|
+
const profiles = await loadShards('USERS');
|
|
42
|
+
if (!events.length) {
|
|
43
|
+
console.error(`no shards at ${prefixPath}-EVENTS*.json — run: node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js ${PREFIX}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(`gaming — events=${events.length} users=${profiles.length} (${prefixPath})`);
|
|
47
|
+
|
|
48
|
+
const execFileP = promisify(execFile);
|
|
49
|
+
const runSql = async (sql) => {
|
|
50
|
+
const { stdout } = await execFileP('duckdb', ['-json', '-c', sql.replaceAll('{{PREFIX}}', prefixPath)], { maxBuffer: 64 * 1024 * 1024 });
|
|
51
|
+
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// funnels passed raw (unvalidated) — every timeToConvert story carries an
|
|
55
|
+
// explicit conversionWindowMs, so no funnel-default threading is needed
|
|
56
|
+
const results = await evaluateStories(stories, events, {
|
|
57
|
+
profiles,
|
|
58
|
+
funnels: config.funnels,
|
|
59
|
+
identityMap: buildIdentityMap(profiles),
|
|
60
|
+
runSql,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let worst = 'NAILED';
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
console.log(`${r.verdict.padEnd(7)} ${r.id}`);
|
|
66
|
+
for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
|
|
67
|
+
if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
|
|
68
|
+
}
|
|
69
|
+
console.log(`\nworst verdict: ${worst}`);
|
|
70
|
+
process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);
|