feedback1 WB: R11 bounty drafts as groups + tier-I fill (matches 1 and 6 share a group); solo R11 weights marked assumed

The official R11 bounty drafts are not a free draw under the old rule: matches 1
and 6 show the same 特异III giant and the same four tier-II cards and differ only
in their one tier-I card; 15_7 + 13_8 come together in 3 of the 5 drafts; 10_8,
11_8 and 12_8 never show. A uniform draw under the old rule repeats 5 of 6 cards
in some pair of 5 drafts in about 2.5 % of samples.

- tools/build-data.mjs: BOUNTY_HUNTER_TEMPLATES (the 4 seen R11 groups: giant +
  tier-II cards, fill = the 5 tier-I cards seen) and BOUNTY_HUNTER (the shape an
  unseen slot draws: one giant + 1-4 tier-II cards + tier-I fill, one card per
  faction series 10-15). choices.json bountyDrafts.hunter gains slots (15),
  templates and rule. The R9 note now names the per-match explanation (leader,
  map) beside two versions. Solo 绝境 / 终极 R11 family weights are marked as
  extrapolated from co-op.
- server/match/choices.js: the hunter draft picks one of the 15 event slots: a
  seen group, or a rule group of that shape, then tier-I fill.
- test/match/feedback1-bounty.test.js: each official R11 bounty draft fits
  exactly one seen group and the rule shape; matches 1 / 6 share 5 cards;
  generated drafts: a seen group in about 4 of 15, every seen group and every
  R11 card offered.
- docs/DATA.md, docs/META.md: the R11 structure, the solo weights and the R9
  open question.
This commit is contained in:
sganggs committed 2026-10-03 05:08:25 +08:00
1 parent e60067a9f3
commit 7c13e38422
6 files changed
+134 -43

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"events":{"artifact_paid_1":{"id":"artifact_paid_1","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_1_s":{"id":"artifact_paid_1_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_2":{"id":"artifact_paid_2","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_2_s":{"id":"artifact_paid_2_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_3":{"id":"artifact_paid_3","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_3_s":{"id":"artifact_paid_3_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_4":{"id":"artifact_paid_4","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_4_s":{"id":"artifact_paid_4_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_5":{"id":"artifact_paid_5","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_5_s":{"id":"artifact_paid_5_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"bossInitial_1":{"id":"bossInitial_1","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_1_s":{"id":"bossInitial_1_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_2":{"id":"bossInitial_2","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_2_s":{"id":"bossInitial_2_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_3":{"id":"bossInitial_3","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_3_s":{"id":"bossInitial_3_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_4":{"id":"bossInitial_4","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_4_s":{"id":"bossInitial_4_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"Line truncated
{"events":{"artifact_paid_1":{"id":"artifact_paid_1","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_1_s":{"id":"artifact_paid_1_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_2":{"id":"artifact_paid_2","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_2_s":{"id":"artifact_paid_2_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_3":{"id":"artifact_paid_3","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_3_s":{"id":"artifact_paid_3_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_4":{"id":"artifact_paid_4","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_4_s":{"id":"artifact_paid_4_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"artifact_paid_5":{"id":"artifact_paid_5","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#35d8b4","family":"shop","solo":false,"training":false},"artifact_paid_5_s":{"id":"artifact_paid_5_s","choiceType":"EQUIP_FREE","effectType":"EQUIP","name":"机密商店","desc":"无需消耗资金,获得装备补给。","descRaw":"无需消耗资金,获得装备补给。","color":"#f2bd3e","family":"shop","solo":true,"training":false},"bossInitial_1":{"id":"bossInitial_1","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_1_s":{"id":"bossInitial_1_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_2":{"id":"bossInitial_2","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_2_s":{"id":"bossInitial_2_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_3":{"id":"bossInitial_3","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_3_s":{"id":"bossInitial_3_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":true,"training":false},"bossInitial_4":{"id":"bossInitial_4","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"选定悬赏目标,获取额外奖励。","descRaw":"选定悬赏目标,获取额外奖励。","color":"#35d8b4","family":"bounty","solo":false,"training":false},"bossInitial_4_s":{"id":"bossInitial_4_s","choiceType":"BOUNTY_HUNT","effectType":"ENEMY_GAIN","name":"悬赏决策","desc":"Line truncated
+2 -2
View File
@@ -321,8 +321,8 @@ Types: `BAND_INITIAL` 41, `BOND` 23, `EQUIP` 115, `ENEMY_GAIN` 129 (悬赏), `BU
| `format` | `{"multi":{"cards":6,"pickOrder":"random","firstPickSec":30,"otherPickSec":16,"onTimeout":"autoPickRandom","eachPlayerPicks":1},"solo":{"cards":3,"timer":null},"opensAfterIncome":true}` | |
| `cards.bounty[]` | `{"effectId":"enemyeffect_10_4","name":"悬赏·飞行I","tier":1,"coin":1,"payout":"kill","rounds":2,"multiRound":false,"enemyKey":"enemy_1005_yokai","count":1,"adds":[…],"draft":true,"draftExcluded":null,"draftPool":"initial","series":10}` | `payout` `kill` (killer gets coins, 联防 helpers too) / `perfect` (chooser's own phase perfect); `rounds` = battles affected (99 = all remaining); `tier` from the I/II/III suffix (else coin value); `enemyKey` = the effect's `enemy_id` (fixed per card; the title only names category and tier); `draft` = offered by a 悬赏决策 draft (86 cards); else `draftExcluded` `perfect` (战术特训: "仅由法术教鞭生成" — the 教鞭 Art's cards; 20) / `hidden` (鸭爵 / 高普尼克 / 流泪小子 / 圆仔; 4) / `unseen` (in none of the 27 official bounty drafts of the user's 33 screenshots: the 7 multi-round cards, enemyeffect_3_*, the boss bounties of no seen R9 group; 19, [ASSUMED] not offered) — build-data `bountyDraftExclusion`, `server/match/choices.js draftBounty`, user playtest #6 item 4, player feedback after 0.1.0 report #2; `draftPool` = the kind of draft that offers it: `initial` (R3: the 42 "接下来两场作战" cards), `boss` (R9: 19 boss bounties + 源石虫·特训), `hunter` (R11: the faction _7 / _8 and 特异 series 16 "下场战斗" cards, 24), null when not drafted; `series` = N of `enemyeffect_N_M` (null otherwise) |
| `cards.tactic[]` | `{"effectId":"map_m01_1","name":"模拟战场演变·模式一","kind":"terrain","stageId":"act1autochess_m01","team":false}` | `kind` `ally` / `enemyDebuff` / `terrain` (only for the match stage); `team` = also given to teammates |
| `bountyDrafts[kind]` | `{"initial":{"events":[…],"slots":10,"sets":[{"cards":[…6],"seen":[1,3,5,9]},…],"mixed":{"series":[10,11,12,13,14,15,20],"count":6,"tiers":[1,1,1,2,2,3]},"count":6},"boss":{"events":[…],"templates":[{"featured":[…],"fill":[…],"seen":[1,5]},…],"count":6},"hunter":{"events":[…],"anchors":[…],"anchorCount":1,"rest":[…],"onePerSeries":[10,…,15],"count":6}}` | the official 悬赏决策 structures, read from 33 screenshots of 11 official co-op 绝境 / 终极 matches (player feedback after 0.1.0 report #2; build-data `BOUNTY_INITIAL_SETS`, readings in `test/fixtures/official-bounty-drafts.json`, `seen` = its match numbers): `initial` = one of `slots` fixed sets, the seen `sets` or for an unseen slot `mixed` (one card from each of `count` of `series`, with `tiers`) [ASSUMED]; `boss` = a `templates` entry: its `featured` bosses + `fill` drawn to 6 (uniform among the seen groups [ASSUMED]); `hunter` = `anchorCount` of `anchors` (the 特异III giants) + `rest`, at most one per `onePerSeries` series |
| `schedule[modeId]` | `{"spRounds":[3,6,9],"rounds":{"3":{"families":[{"family":"bounty","weight":50},…],"cards":6,"supplyTiers":[1,4],"bountyDraft":"initial","events":{"bounty":[…],"supply":[…],"shop":[…],"tactic":[…]},"assumed":true}}}` | research 01 A4 defaults: pick a family by weight; `supplyTiers` = item tier window for 道具补给; 机密商店 draws any tier; `bountyDraft` = the `bountyDrafts` kind of the round's 悬赏决策 (R3 / 险境 R6 `initial`, R9 `boss`, R11 `hunter`); 绝境 / 终极 R11 = 悬赏决策 45 / 机密商店 35 / 战术决策 20 (the official R11s: 5 / 4 / 2 of 11, no 道具补给; weights [ASSUMED]); `events` = the official event ids of that kind (`enemy_initial_*`, `bossInitial_*`, `bounty_hunter_*`), usable as the phase header |
| `bountyDrafts[kind]` | `{"initial":{"events":[…],"slots":10,"sets":[{"cards":[…6],"seen":[1,3,5,9]},…],"mixed":{"series":[10,11,12,13,14,15,20],"count":6,"tiers":[1,1,1,2,2,3]},"count":6},"boss":{"events":[…],"templates":[{"featured":[…],"fill":[…],"seen":[1,5]},…],"count":6},"hunter":{"events":[…],"slots":15,"templates":[{"featured":[…],"fill":[…],"seen":[1,6]},…],"rule":{"giants":[…],"groupCards":[…],"groupSizes":[1,2,3,4],"fill":[…]},"onePerSeries":[10,…,15],"count":6}}` | the official 悬赏决策 structures, read from 33 screenshots of 11 official co-op 绝境 / 终极 matches (player feedback after 0.1.0 report #2; build-data `BOUNTY_INITIAL_SETS`, readings in `test/fixtures/official-bounty-drafts.json`, `seen` = its match numbers): `initial` = one of `slots` fixed sets, the seen `sets` or for an unseen slot `mixed` (one card from each of `count` of `series`, with `tiers`) [ASSUMED]; `boss` = a `templates` entry: its `featured` bosses + `fill` drawn to 6 (uniform among the seen groups [ASSUMED]); `hunter` = one of `slots` events: a seen `templates` group — its `featured` 特异III giant + tier-II cards, always together (matches 1 and 6 share the whole group, only the tier-I card differs) — or for an unseen slot a `rule` group (one of `giants` + `groupSizes` of `groupCards`), then tier-I `fill` to 6, at most one card per `onePerSeries` series (the group / fill split by tier, the unseen slots and the uniform pick among the events [ASSUMED]) |
| `schedule[modeId]` | `{"spRounds":[3,6,9],"rounds":{"3":{"families":[{"family":"bounty","weight":50},…],"cards":6,"supplyTiers":[1,4],"bountyDraft":"initial","events":{"bounty":[…],"supply":[…],"shop":[…],"tactic":[…]},"assumed":true}}}` | research 01 A4 defaults: pick a family by weight; `supplyTiers` = item tier window for 道具补给; 机密商店 draws any tier; `bountyDraft` = the `bountyDrafts` kind of the round's 悬赏决策 (R3 / 险境 R6 `initial`, R9 `boss`, R11 `hunter`); 绝境 / 终极 R11 = 悬赏决策 45 / 机密商店 35 / 战术决策 20 (the official co-op R11s: 5 / 4 / 2 of 11, no 道具补给; weights [ASSUMED]; solo takes the same weights [ASSUMED: no solo screenshot — extrapolated from co-op, the data has solo R11 悬赏决策 events `bounty_hunter_*_s`]); `events` = the official event ids of that kind (`enemy_initial_*`, `bossInitial_*`, `bounty_hunter_*`), usable as the phase header |
| `pools[poolId]` | `{"kind":"equip","items":[…]}` / `{"kind":"chess","tier":2,"rule":"shopEligible"}` | server-side reward pools referenced by effects (`pool_equip_*`, `pool_chess_shop_N_reward`, `pool_char_later`, …), all [ASSUMED]; `rule:"shopEligible"` = visible/non-hidden entries, `maxTier:"shopLevel"` = ≤ current shop level, `weighted` = `[[id, weight]]`. `pool_equip_vict` (维多利亚, every 25 layers: "获得一件带有随机特殊效果的维式重锤") and `pool_equip_rockr` (洛洛's 定制品; user playtest #4 — the research 04 §8 guess 有限加速器 / 激光发射器 / 护盾无人机 / 双模机械臂 / 蜂鸣器 is superseded) = the 4 special 维式重锤, uniform |
## 9. `enemies.json` — `{ [enemyKey]: Enemy }` (249 keys that can appear)
+8 -4
View File
@@ -115,12 +115,16 @@ screenshots of 11 official co-op 绝境 / 终极 matches, readings in `test/fixt
one of the 10 official sets of six "接下来两场作战" cards, 3 × I + 2 × II + 1 × III — 7 seen (a whole series 17 / 18 / 19,
or one card from each of 6 of the series 10–15 / 20), the 3 unseen slots built by that rule [ASSUMED]; **R9** boss
bounties + 源石虫·特训: a seen group of 0–3 named bosses that always come together, filled to 6 with the cheap ones (W /
碎骨 / 弑君者, 大鲍勃, 庞贝 or 鼠王, 源石虫·特训); **R11** (绝境 / 终极: 悬赏决策, 机密商店 or 战术决策, never 道具补给)
one 特异III giant + 5 "下场战斗" cards, at most one per faction series. 险境 R6 drafts like R3 [ASSUMED]. A card's enemy
is fixed by its effect (`enemy_id`): the title only names category and tier (悬赏·损伤I = 底海滑动者 or 临时收音师).
碎骨 / 弑君者, 大鲍勃, 庞贝 or 鼠王, 源石虫·特训) — 7 groups for 6 events: two game versions or a group set per match
(leader, map), open; **R11** (绝境 / 终极 co-op: 悬赏决策, 机密商店 or 战术决策, never 道具补给; solo the same weights
[ASSUMED]) six "下场战斗" cards like R9: a group — one 特异III giant + 1–4 tier-II cards, always together (matches 1 and 6
share the whole group, only the tier-I card differs) — filled to 6 with tier-I cards, at most one per faction series;
4 groups seen for the 15 events, the unseen slots draw a group of that shape [ASSUMED]. 险境 R6 drafts like R3
[ASSUMED]. A card's enemy is fixed by its effect (`enemy_id`): the title only names category and tier (悬赏·损伤I =
底海滑动者 or 临时收音师).
No official draft showed a multi-round card, a pre-series card (enemyeffect_3_*) or the boss bounties 凋零骑士 / “遗弃者”
/ 锏 / 扎罗 / 迷路的巨像: `draftExcluded: 'unseen'` [ASSUMED], flipped by adding them to build-data
`BOUNTY_INITIAL_SETS` / `BOUNTY_BOSS_TEMPLATES` once a screenshot shows one. The card positions are shuffled; solo
`BOUNTY_INITIAL_SETS` / `BOUNTY_BOSS_TEMPLATES` / `BOUNTY_HUNTER_TEMPLATES` once a screenshot shows one. The card positions are shuffled; solo
shows 3 of the 6 [ASSUMED]. The mode's inactive enemy list does not thin the draft (PRTS
11/18 note "不影响悬赏决策出场"). Every bounty card carries the effect's official rich text `descRaw` (the battles in blue
"下场作战" / "两场作战"; the overlay and the effects column render it; the effects column also says "还剩 N 场作战").
+21 -3
View File
@@ -11,7 +11,9 @@
// set, or for an unseen slot one card from each of 6 of the series 10–15 / 20 with the
// tiers I I I II II III;
// boss (R9): a seen group of named bosses + the group's cheap boss bounties / 源石虫·特训 to 6;
// hunter (R11): one 特异III giant + 5 "下场战斗" cards, at most one per faction series;
// hunter (R11): one of the 15 official events — a seen group (a 特异III giant + its tier-II
// "下场战斗" cards, always together) or for an unseen slot a group of that shape
// [ASSUMED] — filled to 6 with tier-I cards, at most one card per faction series;
// only `draft` cards of that `draftPool` (never 战术特训 — 法术教鞭 only —, the 鸭爵 set, the 7
// multi-round cards or the pre-series cards: no official draft shows them); positions shuffled; the
// mode's inactive enemy list does not apply (PRTS 卫戍协议:盟约 11/18 note "以上调整仅针对战术特训敌人,
@@ -232,9 +234,25 @@ function structuredBounty(rng, kind, spec, byId) {
return [...featured, ...drawDistinct(rng, get(t.fill), count - featured.length, taken)].slice(0, count);
}
if (kind === 'hunter') {
// one of the official events: a seen group, or for an unseen slot a group of the same shape; then tier-I fill to 6
const onePer = new Set(Array.isArray(spec.onePerSeries) ? spec.onePerSeries : []);
const anchors = drawDistinct(rng, get(spec.anchors), Number.isInteger(spec.anchorCount) ? spec.anchorCount : 1, taken);
return [...anchors, ...drawDistinct(rng, get(spec.rest), count - anchors.length, taken, onePer)];
const templates = Array.isArray(spec.templates) ? spec.templates : [];
const slots = Math.max(templates.length, Number.isInteger(spec.slots) ? spec.slots : 0);
const slot = slots > 0 ? rng.int(slots) : 0;
let group;
let fill;
if (slot < templates.length) {
group = get(templates[slot].featured);
group.forEach((c) => taken.add(c));
fill = get(templates[slot].fill);
} else {
const r = spec.rule || {};
const sizes = Array.isArray(r.groupSizes) && r.groupSizes.length ? r.groupSizes : [1];
const giant = drawDistinct(rng, get(r.giants), 1, taken);
group = [...giant, ...drawDistinct(rng, get(r.groupCards), rng.pick(sizes), taken, onePer)];
fill = get(r.fill);
}
return [...group, ...drawDistinct(rng, fill, count - group.length, taken, onePer)];
}
return [];
}
+52 -16
View File
@@ -5,8 +5,8 @@
// R3 six "接下来两场作战" cards, 3 × I + 2 × II + 1 × III, one of the official fixed sets (a whole series 17 / 18 / 19,
// or one card from each of 6 of the series 10–15 / 20) — 7 of the 10 events seen;
// R9 boss bounties + 源石虫·特训: a group of named bosses that come together + cheap ones to 6;
// R11 悬赏决策 / 机密商店 / 战术决策 (never 道具补给); its bounty: one 特异III giant + 5 "下场战斗" cards, at most one per
// faction series;
// R11 悬赏决策 / 机密商店 / 战术决策 (never 道具补给); its bounty: a group (one 特异III giant + 1–4 tier-II "下场战斗" cards,
// always together — matches 1 and 6 share the whole group) + tier-I cards to 6, at most one per faction series;
// no draft shows a multi-round card, a pre-series card (enemyeffect_3_*), 战术特训 or the 鸭爵 set; no card twice.
// Each card's enemy is fixed by its effect; the title only names category and tier (悬赏·损伤I = 底海滑动者 in
// enemyeffect_12_4, 临时收音师 in enemyeffect_18_1). Real data, real draft code, the real match path for the players' case.
@@ -63,13 +63,21 @@ function initialFit(ids) {
/** The R9 groups a set of ids fits (its featured bosses all there, the rest from the group's fill). */
const bossFits = (ids) => SPEC.boss.templates.map((t, i) => [t, i]).filter(([t]) => ids.length === SPEC.boss.count
&& t.featured.every((id) => ids.includes(id)) && ids.filter((id) => !t.featured.includes(id)).every((id) => t.fill.includes(id))).map(([, i]) => i);
/** Whether a set of ids follows the R11 rule (one giant, the rest from the next-battle cards, one per faction series). */
function hunterFits(ids) {
const h = SPEC.hunter;
const anchors = ids.filter((id) => h.anchors.includes(id));
const rest = ids.filter((id) => !h.anchors.includes(id));
const series = rest.map((id) => CARD.get(id).series).filter((s) => h.onePerSeries.includes(s));
return ids.length === h.count && anchors.length === h.anchorCount && rest.every((id) => h.rest.includes(id)) && new Set(series).size === series.length;
/** At most one card per faction series 10–15 (every official R11 draft). */
function onePerSeries(ids) {
const series = ids.map((id) => CARD.get(id).series).filter((s) => SPEC.hunter.onePerSeries.includes(s));
return new Set(series).size === series.length;
}
/** The seen R11 groups a set of ids fits (the group's giant + tier-II cards all there, the rest from its fill). */
const hunterFits = (ids) => SPEC.hunter.templates.map((t, i) => [t, i]).filter(([t]) => ids.length === SPEC.hunter.count && onePerSeries(ids)
&& t.featured.every((id) => ids.includes(id)) && ids.filter((id) => !t.featured.includes(id)).every((id) => t.fill.includes(id))).map(([, i]) => i);
/** The R11 shape an unseen slot is built by: one tier-III giant, 1–4 tier-II group cards, tier-I fill, one per faction series. */
function hunterShape(ids) {
const r = SPEC.hunter.rule;
const giants = ids.filter((id) => r.giants.includes(id)).length;
const group = ids.filter((id) => r.groupCards.includes(id)).length;
const fill = ids.filter((id) => r.fill.includes(id)).length;
return ids.length === SPEC.hunter.count && giants === 1 && r.groupSizes.includes(group) && giants + group + fill === ids.length && onePerSeries(ids);
}
/** Bounty drafts generated for `modeId` at `round` over `seeds` seeds. */
@@ -181,31 +189,59 @@ test('#2 R9 (12 of 12 official drafts): boss bounties + 源石虫·特训 — a
}
});
test('#2 R11 (11 matches: 悬赏决策 5, 机密商店 4, 战术决策 2, 道具补给 0): the families, and the bounty rule — one 特异III giant + 5 "下场战斗" cards, one per faction series', () => {
test('#2 R11 (11 matches: 悬赏决策 5, 机密商店 4, 战术决策 2, 道具补给 0): the families, and the bounty — a group (one 特异III giant + its tier-II cards, always together) + tier-I fill, one card per faction series', () => {
const r11 = officialDrafts().filter((d) => d.round === 11);
const fam = {};
for (const d of r11) fam[d.family] = (fam[d.family] || 0) + 1;
assert.deepEqual(fam, { bounty: 5, shop: 4, tactic: 2 });
const hits = SPEC.hunter.templates.map(() => 0);
const byMatch = {};
for (const d of r11.filter((x) => x.family === 'bounty')) {
assert.ok(hunterFits(d.ids), `${d.where}: ${d.ids.join(', ')}`);
assert.ok(d.ids.every((id) => CARD.get(id).draftPool === 'hunter' && CARD.get(id).rounds === 1));
assert.ok(d.ids.every((id) => CARD.get(id).draftPool === 'hunter' && CARD.get(id).rounds === 1), `${d.where}: R11 cards, 下场战斗`);
assert.ok(hunterShape(d.ids), `${d.where}: the shape the unseen slots are built by (${tiersOf(d.ids)})`);
const fits = hunterFits(d.ids);
assert.equal(fits.length, 1, `${d.where}: fits exactly one seen group (${fits.join(', ')})`);
hits[fits[0]]++;
byMatch[d.where.split(' ')[1]] = d.ids;
}
assert.deepEqual(hits, [2, 1, 1, 1], 'the 16_4 group in matches 1 and 6, three more groups once');
// the evidence against a free draw: matches 1 and 6 share the giant and all four tier-II cards, only the tier-I card differs
const shared = byMatch['1'].filter((id) => byMatch['6'].includes(id));
assert.equal(shared.length, 5);
assert.deepEqual(byMatch['1'].filter((id) => !shared.includes(id)).concat(byMatch['6'].filter((id) => !shared.includes(id))).map((id) => CARD.get(id).tier), [1, 1]);
for (const id of ['enemyeffect_10_8', 'enemyeffect_11_8', 'enemyeffect_12_8']) assert.ok(!Object.values(byMatch).flat().includes(id), `${id} in no official R11 draft`);
for (const modeId of ['mode_multi_hard', 'mode_multi_abyss', 'mode_single_hard', 'mode_single_abyss']) {
const sch = DATA.choices.schedule[modeId].rounds['11'];
assert.deepEqual(sch.families.map((f) => f.family).sort(), ['bounty', 'shop', 'tactic'], `${modeId} R11`);
assert.deepEqual(sch.families.map((f) => f.family).sort(), ['bounty', 'shop', 'tactic'], `${modeId} R11 (solo: extrapolated from co-op, [ASSUMED])`);
assert.equal(sch.bountyDraft, 'hunter');
assert.equal(sch.assumed, true);
}
assert.equal(SPEC.hunter.slots, 15, 'bounty_hunter_1..15');
// generated: a seen group (about 4 in 15) or a group of the same shape; every seen group and every R11 card comes up
const gd = new GameData(DATA, 'mode_multi_hard');
const seen = {};
for (let seed = 1; seed <= 200; seed++) {
const groups = SPEC.hunter.templates.map(() => 0);
const offered = new Set();
let bounty = 0;
let fitSeen = 0;
for (let seed = 1; seed <= 1500; seed++) {
const d = generateDraft(gd, createRng(seed * 13 + 11), 11, { stageId: 'act2autochess_m01' });
seen[d.family] = (seen[d.family] || 0) + 1;
if (d.family !== 'bounty') continue;
assert.ok(hunterFits(d.cards.map((c) => c.id)), d.cards.map((c) => c.name).join(', '));
const ids = d.cards.map((c) => c.id);
assert.equal(new Set(ids).size, 6);
assert.ok(hunterShape(ids), d.cards.map((c) => c.name).join(', '));
assert.match(d.eventId, /^bounty_hunter_\d+$/);
const fits = hunterFits(ids);
if (fits.length) { fitSeen++; groups[fits[0]]++; }
bounty++;
ids.forEach((id) => offered.add(id));
}
assert.deepEqual(Object.keys(seen).sort(), ['bounty', 'shop', 'tactic'], 'no 道具补给 at R11');
assert.ok(seen.bounty > seen.tactic, JSON.stringify(seen));
assert.ok(seen.bounty > seen.shop && seen.shop > seen.tactic, JSON.stringify(seen));
assert.ok(groups.every((x) => x > 0), `every seen group offered (${groups.join(' ')})`);
assert.ok(fitSeen / bounty > 0.17 && fitSeen / bounty < 0.4, `seen groups ${(100 * fitSeen / bounty).toFixed(0)} % of ${bounty}`);
for (const c of CARDS.filter((x) => x.draftPool === 'hunter')) assert.ok(offered.has(c.effectId), `${c.effectId} ${c.name} offered at R11`);
});
test('#2 never offered by a draft (in none of the 27 official bounty drafts): multi-round cards, enemyeffect_3_*, 战术特训, the 鸭爵 set', () => {
+50 -17
View File
@@ -2352,11 +2352,21 @@ function bountyDraftExclusion(e, main) {
* boss R9 (bossInitial_1..6): the boss bounties "X·悬赏" and 源石虫·特训, all "下场作战". All 12 drafts (11 matches +
* the Bahamut 12294 co-op screenshot) are a group of 0–3 named bosses that always come together, filled up to 6
* with cheap ones (W / 碎骨 / 弑君者 1, 大鲍勃 2, 庞贝 or 鼠王 2, 源石虫·特训 0): BOUNTY_BOSS_TEMPLATES. 7 groups
* were seen for 6 events (庞贝 and 鼠王 never in one draft: perhaps two versions); uniform [ASSUMED]. 凋零骑士,
* “遗弃者”, 锏, 扎罗 and 迷路的巨像 never appeared: not offered [ASSUMED].
* hunter R11 (bounty_hunter_*; R11 is a 悬赏决策 in 5 of the 11 matches): six "下场战斗" cards, exactly one 特异III giant
* (enemyeffect_16_1..8, 3 coins) and 5 of the faction _7 / _8 cards and enemyeffect_16_9..12, at most one per
* faction series 10–15 (5 of 5 drafts): BOUNTY_HUNTER.
* were seen for 6 events (庞贝 and 鼠王 never in one draft; 杰斯顿 only in matches 7–11): two game versions,
* or a group set by something of the match (its leader, its map — the 杰斯顿 groups came on both boards) —
* open; uniform among the seen groups [ASSUMED]. 凋零骑士, “遗弃者”, 锏, 扎罗 and 迷路的巨像 never appeared: not
* offered [ASSUMED].
* hunter R11 (bounty_hunter_1..15; R11 is a 悬赏决策 in 5 of the 11 matches): six "下场战斗" cards, exactly one 特异III
* giant (enemyeffect_16_1..8, 3 coins), the others tier II / I faction _7 / _8 cards and enemyeffect_16_9..12,
* at most one per faction series 10–15 (5 of 5 drafts). Not a free draw within that rule: matches 1 and 6
* show the same giant (16_4) and the same four tier-II cards (16_10 15_7 12_7 13_8) and differ only in their
* one tier-I card (10_7 / 11_7); 15_7 + 13_8 come together in matches 1, 3 and 6; 10_8, 11_8 and 12_8 never
* show (a uniform draw under the rule gives some pair of 5 drafts 5 shared cards in about 2.5 % of samples).
* Like R9, a draft is a group — the giant + 1–4 tier-II cards, always together — filled to 6 with tier-I
* cards: BOUNTY_HUNTER_TEMPLATES, 4 groups seen for the 15 events (fill = the 5 tier-I cards seen); an unseen
* event draws a group of that shape over all the R11 cards (BOUNTY_HUNTER) [ASSUMED: the group / fill split
* by tier, uniform among the 15 events]. The fill cards 16_11 and 13_7 only came in matches 10–11 (the
* 杰斯顿 period of R9) — too few drafts to split the fill by period.
* In none of the 27 bounty drafts: the 7 multi-round cards (山海众头目·多轮悬赏, 多轮悬赏·假想敌 ×6), the pre-series cards
* enemyeffect_3_* (法术大师A2·悬赏 …), 战术特训 (法术教鞭 only) and the 鸭爵 set; no card twice in one draft.
*/
@@ -2386,12 +2396,20 @@ const BOUNTY_BOSS_TEMPLATES = [
{ featured: ['b_9', 'b_23'].map(EE), fill: BOSS_FILL_B, seen: [7, 11] }, // 杰斯顿·威廉姆斯 陷落雪祀
{ featured: ['b_9', 'b_14'].map(EE), fill: BOSS_FILL_B, seen: [8, 9, 10] }, // 杰斯顿·威廉姆斯 “自在”
];
/** The R11 rule: `anchorCount` of `anchors` + the rest from `rest`, at most one card per series of `onePerSeries`. */
const BOUNTY_HUNTER = {
anchors: [1, 2, 3, 4, 5, 6, 7, 8].map((i) => EE(`16_${i}`)), anchorCount: 1,
rest: [...[10, 11, 12, 13, 14, 15].flatMap((n) => [EE(`${n}_7`), EE(`${n}_8`)]), ...[9, 10, 11, 12].map((i) => EE(`16_${i}`))],
onePerSeries: [10, 11, 12, 13, 14, 15], count: 6,
};
const HUNTER_FILL = ['10_7', '11_7', '13_7', '14_7', '16_11'].map(EE); // 法术大师A2 沏虹 假想敌:蚀裂 重弩突袭者 心虚设计师
/** The R11 groups seen: `featured` (the 特异III giant + its tier-II cards) always all on the card row, `fill` (tier I) drawn to make 6. */
const BOUNTY_HUNTER_TEMPLATES = [
{ featured: ['16_4', '16_10', '15_7', '12_7', '13_8'].map(EE), fill: HUNTER_FILL, seen: [1, 6] }, // 温顺的武装大驮兽 异质裂兽·α 深池重甲卫士队长 灼藤 尖端萨卡兹枯朽战车
{ featured: ['16_3', '15_7', '13_8'].map(EE), fill: HUNTER_FILL, seen: [3] }, // 狂暴宿主组长 深池重甲卫士队长 尖端萨卡兹枯朽战车
{ featured: ['16_5', '14_8', '12_7', '15_8'].map(EE), fill: HUNTER_FILL, seen: [10] }, // 萨卡兹骸骨拷打者 家族暗影灭迹人 灼藤 愤怒的守墓石像
{ featured: ['16_7', '16_9'].map(EE), fill: HUNTER_FILL, seen: [11] }, // 乌顶巨角卢鲁 拥霜羽兽
];
/**
* An unseen R11 group (the seen groups' shape): one tier-III giant + `groupSizes` (one drawn: the seen 4 / 2 / 3 / 1)
* tier-II cards, filled with tier-I cards, at most one card per series of `onePerSeries` — over the R11 cards by tier
* (buildChoices). [ASSUMED]
*/
const BOUNTY_HUNTER = { groupSizes: [1, 2, 3, 4], onePerSeries: [10, 11, 12, 13, 14, 15], count: 6 };
/** The kind of a round's 悬赏决策: R3 (and 险境 R6, [ASSUMED]) initial, R9 boss, R11 hunter. */
const bountyDraftOf = (r) => (r < 8 ? 'initial' : r < 11 ? 'boss' : 'hunter');
@@ -2472,8 +2490,15 @@ function buildChoices(ctx, effects, items, chess) {
const checkKind = (ids, kind, what) => { for (const id of ids) if (bountyById.get(id)?.draftPool !== kind) warn(`bounty draft ${what}: ${id} is not a "${kind}" card`); };
for (const s of BOUNTY_INITIAL_SETS) checkKind(s.cards, 'initial', 'R3 set');
for (const t of BOUNTY_BOSS_TEMPLATES) checkKind([...t.featured, ...t.fill], 'boss', 'R9 group');
checkKind([...BOUNTY_HUNTER.anchors, ...BOUNTY_HUNTER.rest], 'hunter', 'R11 rule');
for (const c of bounty) if (c.draftPool === 'hunter' && !BOUNTY_HUNTER.anchors.includes(c.effectId) && !BOUNTY_HUNTER.rest.includes(c.effectId)) warn(`bounty ${c.effectId}: an R11 card outside BOUNTY_HUNTER`);
for (const t of BOUNTY_HUNTER_TEMPLATES) {
checkKind([...t.featured, ...t.fill], 'hunter', 'R11 group');
const tiers = t.featured.map((id) => bountyById.get(id)?.tier);
if (tiers.filter((x) => x === 3).length !== 1 || tiers.some((x) => x !== 3 && x !== 2)) warn(`bounty draft R11 group ${t.featured.join(' ')}: not one giant + tier-II cards`);
if (t.fill.some((id) => bountyById.get(id)?.tier !== 1)) warn(`bounty draft R11 group ${t.featured.join(' ')}: a fill card is not tier I`);
}
// the unseen R11 groups draw over the R11 cards by tier: III the giants, II the group cards, I the fill
const hunterTier = (t) => bounty.filter((c) => c.draftPool === 'hunter' && c.tier === t).map((c) => c.effectId);
if (hunterTier(1).length + hunterTier(2).length + hunterTier(3).length !== bounty.filter((c) => c.draftPool === 'hunter').length) warn('bounty: an R11 card of no tier I–III');
// Tactic cards (BUFF_GAIN effects).
const tactic = [];
@@ -2508,7 +2533,9 @@ function buildChoices(ctx, effects, items, chess) {
if (diff === 'TRAINING') families = [{ family: 'supply', weight: 100 }];
else if (diff === 'HARD' || diff === 'ABYSS') {
// R11 of the 11 official co-op matches (player feedback after 0.1.0 report #2, the screenshots of BOUNTY_INITIAL_SETS):
// 悬赏决策 5, 机密商店 4, 战术决策 2, 道具补给 0 — the weights follow those counts [ASSUMED]
// 悬赏决策 5, 机密商店 4, 战术决策 2, 道具补给 0 — the weights follow those counts [ASSUMED]. Solo 绝境 / 终极 take the
// same weights [ASSUMED: no solo screenshot; extrapolated from co-op — the data has solo 悬赏决策 events for R11,
// bounty_hunter_*_s, which the previous supply 50 / shop 50 never used]
families = r === 11 ? [{ family: 'bounty', weight: 45 }, { family: 'shop', weight: 35 }, { family: 'tactic', weight: 20 }] : [{ family: 'bounty', weight: 100 }];
} else if (diff === 'NORMAL') {
families = solo
@@ -2535,7 +2562,7 @@ function buildChoices(ctx, effects, items, chess) {
return {
events,
families: {
bounty: { name: '悬赏决策', desc: '选定悬赏目标,获取额外奖励。', cards: 'bountyDrafts[schedule[*].bountyDraft] — the official draft structure of the round (initial: one of the R3 sets; boss: an R9 group + cheap fill; hunter: one 特异III giant + 5 next-battle cards, one per faction series), over cards.bounty entries with draft: true and that draftPool' },
bounty: { name: '悬赏决策', desc: '选定悬赏目标,获取额外奖励。', cards: 'bountyDrafts[schedule[*].bountyDraft] — the official draft structure of the round (initial: one of the R3 sets; boss: an R9 group + cheap fill; hunter: an R11 group — one 特异III giant + tier-II cards — + tier-I fill, one per faction series), over cards.bounty entries with draft: true and that draftPool' },
supply: { name: '道具补给', desc: '无需消耗资金,获得装备补给。', cards: 'random normal EQUIP items in schedule[*].supplyTiers (duplicates allowed)' },
shop: { name: '机密商店', desc: '无需消耗资金,获得装备补给。', cards: 'random normal EQUIP items of any tier I–VI (duplicates allowed)' },
tactic: { name: '战术决策', desc: '选择战术增益。', cards: 'cards.tactic (terrain cards only for the match stage)' },
@@ -2547,11 +2574,17 @@ function buildChoices(ctx, effects, items, chess) {
},
cards: { bounty, tactic },
// the official 悬赏决策 structures by kind (tools/build-data.mjs BOUNTY_INITIAL_SETS; `seen` = the match numbers of
// test/fixtures/official-bounty-drafts.json; `slots` = the official events, the unseen ones built by `mixed`)
// test/fixtures/official-bounty-drafts.json; `slots` = the official events, the unseen ones built by `mixed` / `rule`)
bountyDrafts: {
initial: { events: eventsOf('bounty', (e) => bountyEventRe.initial.test(e.id) && !e.solo), slots: eventsOf('bounty', (e) => bountyEventRe.initial.test(e.id) && !e.solo).length, sets: BOUNTY_INITIAL_SETS, mixed: BOUNTY_INITIAL_MIXED, count: 6, assumed: ['the unseen slots built by `mixed`', '险境 R6 drafts like R3'] },
boss: { events: eventsOf('bounty', (e) => bountyEventRe.boss.test(e.id) && !e.solo), templates: BOUNTY_BOSS_TEMPLATES, count: 6, assumed: ['a uniform pick among the seen groups', 'boss bounties of no seen group are not offered'] },
hunter: { events: eventsOf('bounty', (e) => bountyEventRe.hunter.test(e.id) && !e.solo), ...BOUNTY_HUNTER, assumed: ['uniform draws within the rule'] },
hunter: {
events: eventsOf('bounty', (e) => bountyEventRe.hunter.test(e.id) && !e.solo), slots: eventsOf('bounty', (e) => bountyEventRe.hunter.test(e.id) && !e.solo).length,
templates: BOUNTY_HUNTER_TEMPLATES,
rule: { giants: hunterTier(3), groupCards: hunterTier(2), groupSizes: BOUNTY_HUNTER.groupSizes, fill: hunterTier(1) },
onePerSeries: BOUNTY_HUNTER.onePerSeries, count: BOUNTY_HUNTER.count,
assumed: ['a group (the giant + tier-II cards) + tier-I fill, like R9', 'the unseen slots built by `rule`', 'a uniform pick among the 15 events'],
},
},
schedule,
pools: {