Merge branch 'skill-pool-rhythm-20260919'

This commit is contained in:
DKChan committed 2026-09-19 21:47:01 +08:00
commit b270a949b2
6 files changed
+249 -10

No files matched your search

+1 -9
View File
@@ -98,14 +98,6 @@
### 敌人系统
#### 单拍行为环的体验验证(改型:技能池节奏体验验证)
**现状**:原登记对象(地精单拍行为环)已随 2026-09-16 敌人技能池批整体退役——行为环废除后本条失去原载体;验证问题改型为"技能池出招的节奏体验":普攻基底 + 随机特殊技的纹理是否成立、保底 M=4 与冷却值是否制造可背诵节奏或过旱/过涝,随下次 playtest 批采集(关联:池 EV 含闸实测读数已登记敌人系统/难度曲线调参表)
**归属**:难度曲线与Boss 与敌人系统共裁
**状态**:待办(2026-09-16 改型)
#### 伏击量纲与上限的联动复核
**现状**:附加值 3 为 provisional(量纲已定稿);上限与量纲、攻击基线存在联动,联调前须重跑方向性预检并同步 registry。**2026-09-17 数值压缩批注记**:magnitude +3 → +1(量纲未变,参见 `data/tables/status_effects.tres` ambush 行);压缩批后 magnitude=1 状态下,方向性预检须按新值重跑(cap=4 下右臂 m_amb × min(AP,cap) 量级同步压缩——量级当量重核随数值压缩批同步推进)
@@ -182,7 +174,7 @@
#### Alpha:全怪物技能池数值验证与合理性设计
**现状**:2026-09-19 用户立项。背景:切片期技能池只保证正常游玩(用户裁示——卓尔池无增益技、文档池构成表缺 L2 行等候选缺漏不做当期修复,后续可能对全怪物技能池整体重设计);本条 = Alpha 版本时对**所有怪物**的技能池做一次完整的数值验证与合理性设计。范围:池构成与权重、技能倍率与冷却、保底 M 与冷却递减、EV 期望与关间台阶、克制的技能面覆盖;**含技能倍率在小整数基线下的取整抖动复核**(×1.5 遇奇数攻击基线出半点、现行 round 处理——与甲效矩阵"假粒度"论证同族,轻量版);含本批两候选缺漏的复核(卓尔池增益技去留、敌人系统文档池构成表补 L2 行)。若技能池届时整体重设计,本验证以重设计后的版本为准。
**现状**:2026-09-19 用户立项。背景:切片期技能池只保证正常游玩(用户裁示——卓尔池无增益技、文档池构成表缺 L2 行等候选缺漏不做当期修复,后续可能对全怪物技能池整体重设计);本条 = Alpha 版本时对**所有怪物**的技能池做一次完整的数值验证与合理性设计。范围:池构成与权重、技能倍率与冷却、保底 M 与冷却递减、EV 期望与关间台阶、克制的技能面覆盖;**含技能倍率在小整数基线下的取整抖动复核**(×1.5 遇奇数攻击基线出半点、现行 round 处理——与甲效矩阵"假粒度"论证同族,轻量版);含本批两候选缺漏的复核(卓尔池增益技去留、敌人系统文档池构成表补 L2 行)。**2026-09-19 节奏体验验证批观测移交**:按用户判读带(过旱 = 整场战斗全敌人零特殊技)实测,过旱集中在短命杂鱼(地精系 82–87%、巨蜘蛛 52%,均出招 1.2–2.2 拍——保底 M=4 在战斗总长 1–2 拍时结构性无法触发),耐久敌零过旱(酋长 0% / 卓尔 2%)——「杂鱼是否也该有可感知的技能纹理」(如首拍加权/保底按剩余血量改算)属池设计面,随本条 Alpha 池验证一并评估。若技能池届时整体重设计,本验证以重设计后的版本为准。
**归属**:难度曲线与Boss(数值验证主责)与[敌人系统](systems/战斗循环/敌人系统.md)(池设计共裁);时点 = Alpha,随验收读数重测批同窗口执行
@@ -224,7 +224,7 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
| 池权重 | 权重即旋钮:抬特殊技权重 = 抬输出频率,EV 随之重锚;各敌种行集读表 |
| 技能冷却(拍) | 逐技能行读表;普攻冷却恒 0 不可配(空池不可能的结构前提——域校验锁死);撕咬 = 与突刺同倍率错开冷却轴的高频小爆发轮廓;毒爆 = 蛛化卓尔精灵专属池独占技 |
| 保底 M | 全局单旋钮,读技能表 pity_threshold;连续 M 拍执行普攻 → 下拍必出特殊技;单特殊技低权重池受保底支配 |
| 池 EV 锚 | 静态份额与含闸长期速率实测读数登记于调参日志(EV 重锚与权重投值随本表修订);L2 池含闸速率待抽样复核 |
| 池 EV 锚 | 静态份额与含闸长期速率实测读数登记于调参日志(EV 重锚与权重投值随本表修订)。**2026-09-19 节奏体验验证批登记**(60 distinct seeds × 双策略 × 两关,tools/skill_rhythm_audit.py——20 种子以下批次有种子偏置假象,读数须 ≥50 种子):特殊技执行份额 对表成立——地精 10.4%(静态 10%)/ 地精一伙 8.4% / 熊地精系 28.9–42.4% / 巨蜘蛛 34.8% / 酋长 28.9% / 卓尔 35.4%;**L2 池含闸速率抽样复核完成**(上项即含闸读数);保底越界 0/247(间隔 4 = M=4 合法上界);过涝信号 8/1440 场(最长连击 3);过旱按用户判读带集中在短命杂鱼(生存时长问题非池配置——见 06 清单 Alpha 条目移交注记) |
主角表与棋盘参数表(行值分别读 `hero.tres` 与 `board_gen.tres`,provisional):
@@ -278,3 +278,4 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
| v0.10 | 2026-09-18 | **元进度定稿批(与 元进度 v1.0 / registry / 06-待办清单 同批)**:元进度接口预留节改定稿口径——区间平移换算率启用(带平移,判读口径不变),换算率表首版三行与护甲行强条目指路 registry `meta_progression_conversion_table`,数值标定归元进度设计批;P2「区间平移换算率定稿」行同步改剩余面口径。曲线规则与行值零改动 |
| v0.11 | 2026-09-18 | **甲效矩阵用户调参批同步(零代码,事实源 settlement.tres 五格改值——详见[战斗结算](../战斗循环/战斗结算.md) v0.15)**:正文克制矩阵节与调参表联动行「克制/中性/被克三档」措辞改五档(克制 / 弱克制 / 中性 / 弱被克 / 被克,档值读表);联动义务面扩写——矩阵档值变更与敌种护甲变更同列,都必须复算 Boss 与普通战的击杀区间。敌种甲类型行值零改动 |
| v0.12 | 2026-09-18 | **数值语言拍板批·甲效矩阵加法核换装同步(零行值改动——与 01-核心循环 数值语言条款 / 元进度 / registry / 06-待办清单 / 切片代码同批)**:正文克制矩阵节与调参表克制矩阵行两处五档措辞改三档加法(克制 / 中性 / 被克三档加法修正 +1 / 0 / −1 点,量纲由百分比改整数点数);重定价义务面扩写——矩阵加法修正点数随攻击成长的重定价与敌种护甲变更同列,均须复算 Boss 与普通战的击杀区间 |
| v0.13 | 2026-09-19 | **技能池节奏体验验证批(与 06-待办清单 / tools/skill_rhythm_audit.py / smoke/rhythm_batch.gd 同批,零行值改动)**:06 待办「单拍行为环的体验验证(改型:技能池节奏体验验证)」解决移除——判读带用户拍板(过旱 = 战斗整体全敌人零特殊技;过涝 = 连续特殊技无普攻基底);调参表「池 EV 锚」行登记 60 种子正式读数(份额对表 / 保底零越界 / 过涝无信号 / 过旱归短命杂鱼生存时长);**方法论规则成文:节奏/EV 类日志读数须 ≥50 distinct seeds**(4200 系 10 种子×网格重复曾产生地精一伙 0/484 突刺假象,4300 系复跑证伪——与 armor_roll 种子偏置发现同族,见 registry rng_service 关联面) |
@@ -0,0 +1,38 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-19
# 技能池节奏读数批量采集(06 待办「技能池节奏体验验证」机器半边正式读数):
# 60 distinct seeds × 双策略 × 两关,atk/hp 零加成基线——单网格组合避免种子重复
# (4200 系 0/484 假象教训:节奏/EV 读数须 ≥50 distinct seeds,见台账 89)。
# 运行:Godot_console.exe --headless --path . --script res://smoke/rhythm_batch.gd -- [seeds] [seed0]
extends SceneTree
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
var n := 60
if args.size() >= 1 and args[0].is_valid_int():
n = int(args[0])
var seed0 := 4400
if args.size() >= 2 and args[1].is_valid_int():
seed0 = int(args[1])
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("表加载失败")
quit(1)
return
var logger = load("res://presentation/playtest_logger.gd").new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
var log_path: String = logger.start_session(seed0)
print("节奏批日志: %s" % log_path)
var autoplay = load("res://smoke/level_autoplay.gd")
var t0 := Time.get_ticks_msec()
var total := 0
for L in [1, 2]:
for policy in ["normal", "perfect"]:
for i in range(n):
var r = autoplay.run_level(db, seed0 + i, L, 0, 0, false, logger, policy, "min")
total += 1
if i % 10 == 0:
print("进度 L%d %s %d/%d" % [L, policy, i, n])
logger.close_session()
print("节奏批完成:%d 局,耗时 %.1f 秒\n日志: %s" % [total, (Time.get_ticks_msec() - t0) / 1000.0, log_path])
quit(0)
@@ -0,0 +1,72 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-19
# 技能池节奏探针:地精一伙 (1,1) 750 出招零突刺异常的定位——
# ① 表侧:resolve_enemy_rowset(1,1) 的池快照内容;
# ② 摇号侧:隔离 SkillLottery.draw 大样本份额;
# ③ 战斗侧:真实 (1,1) 战斗逐执行行按 skill_id 计数。
# 运行:Godot_console.exe --headless --path . --script res://smoke/skill_rhythm_probe.gd
extends SceneTree
func _initialize() -> void:
var db = load("res://core/config_db.gd").new()
db.load_tables("res://data/tables", false)
# ① 表侧
var row = db.resolve_enemy_rowset(1, 1)
print("[1] row(1,1) display=", row.get("display_name"), " unit_count=", row.get("unit_count"),
" pool=", row.get("pool"))
# ② 摇号侧隔离
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260919)
var lot = load("res://core/skill_lottery.gd")
var counter := {}
for i in range(10000):
var stream: RandomNumberGenerator = rng.open_scope("behavior_roll", i % 7)
var s: String = lot.draw(row.get("pool"), stream)
counter[s] = counter.get(s, 0) + 1
print("[2] SkillLottery 10000 抽(scope 0-6 复用): ", counter)
# ③ 战斗侧(真实路径):(1,0) 对照 + (1,1) 异常组
for depth in [0, 1]:
var casts := {}
var battles := 0
for i in range(10):
var r = _run_battle(db, 4200 + i, 1, depth, casts)
battles += 1
print("[3] 战斗侧 (1,%d) %d 场执行出招 skill_id 计数: " % [depth, battles], casts)
quit(0)
func _run_battle(db, seed_val: int, L: int, depth: int, casts: Dictionary) -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var out := {"result": ""}
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.step_row.connect(func(row):
if row.type == "damage" and row.source_class == "enemy_action":
var sid: String = str(row.get("skill_id", "?"))
casts[sid] = casts.get(sid, 0) + 1)
if not settlement.begin_battle({"level_index": L, "node_depth": depth, "hero_hp": int(db.hero.max_hp)}):
return out
var AutoPlayer = load("res://smoke/auto_player.gd")
var guard_rounds := 40
while out.result == "" and guard_rounds > 0:
guard_rounds -= 1
var start_round: int = settlement.round_index
var minesweeper = settlement.minesweeper
var guard := 400
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
guard -= 1
var plan = AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
break
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
minesweeper.settle_batch([{"action": "stop"}])
return out
@@ -0,0 +1 @@
uid://b5vagf2sq2suk
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""技能池出招节奏审计(消费 playtest_*.jsonl 的 enemy_action 行)。
对应 06 待办「单拍行为环的体验验证(改型:技能池节奏体验验证)」的机器半边。
判读带(2026-09-19 用户拍板):
· 过涝 = 敌群连续出特殊技、无普攻基底——读数 = 单敌群同场战斗内最长连续特殊技连击
(连击 ≥3 记过涝信号,连击阈值 3 为代拍板默认:保底 M=4 与冷却 2-4 语义下 2 连属正常波动)
· 过旱 = 按战斗整体:一场战斗里所有敌人都没用过特殊技记过旱样本;任一敌人用过即正常
(战斗内敌人从未行动过的「首轮全灭/跳战」场单列,不与"有行动无特殊技"混计)
机器读数:
· 含闸速率 = 特殊技出招占全部出招的份额(对表值权重 = 静态份额比对)
· 旱涝间隔 = 相邻两次特殊技之间的普攻连投数(cast 间隔)与回合间隔(round 间隔)
· 保底 M=4 语义:连续 4 拍普攻 → 下拍必出特殊技 → cast 间隔样本 ≤4 合法
用法:
python tools/skill_rhythm_audit.py <playtest_*.jsonl> [...]
"""
import collections
import glob
import json
import re
import sys
SUFFIX_RE = re.compile(r"([^)]*)|+[^+]*$")
def actor_of(display_name: str) -> str:
# "蛛化卓尔精灵 毒爆(被压制)+附加2(必中)" → 敌群前缀取第一个空格前段(无空格 = 状态行,丢弃)
if " " not in display_name:
return ""
return display_name.split(" ")[0]
def pct(samples, q):
if not samples:
return "-"
s = sorted(samples)
idx = min(len(s) - 1, max(0, round(q * (len(s) - 1))))
return s[idx]
def main(paths):
battles = []
cur = None
for pattern in paths:
for path in sorted(glob.glob(pattern)):
with open(path, encoding="utf-8") as f:
for line in f:
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
kind = d.get("kind")
if kind == "node":
cur = {"actors": collections.defaultdict(list)}
battles.append(cur)
elif (kind == "step" and d.get("source") == "enemy_action"
and d.get("skill_id") and cur is not None):
actor = actor_of(d.get("display_name", ""))
if actor:
cur["actors"][actor].append(
(d.get("round_index", 0), d.get("skill_id", "attack")))
agg = collections.defaultdict(lambda: {
"battles": 0, "casts": 0, "specials": 0,
"cast_gaps": [], "round_gaps": [], "streaks": [],
})
# 过旱/过涝判读带(用户拍板 2026-09-19)——按战斗整体计
drought_battles = 0 # 过旱:全战斗零特殊技(有敌行动)
no_action_battles = 0 # 首轮全灭/跳战:敌从未行动(单列不混计)
normal_battles = 0
flood_signals = 0 # 过涝信号:单敌群连续特殊技 ≥3
flood_max = 0
for b in battles:
actors = b["actors"]
if not actors:
no_action_battles += 1
continue
has_special = False
for actor, casts in actors.items():
a = agg[actor]
a["battles"] += 1
a["casts"] += len(casts)
special_idx = [i for i, (_, sk) in enumerate(casts) if sk != "attack"]
a["specials"] += len(special_idx)
if special_idx:
has_special = True
# 过涝:最长连续特殊技连击
run, best = 0, 0
for _, sk in casts:
if sk != "attack":
run += 1
best = max(best, run)
else:
run = 0
a["streaks"].append(best)
flood_max = max(flood_max, best)
if best >= 3:
flood_signals += 1
for prev, nxt in zip(special_idx, special_idx[1:]):
a["cast_gaps"].append(nxt - prev - 1)
a["round_gaps"].append(casts[nxt][0] - casts[prev][0])
if has_special:
normal_battles += 1
else:
drought_battles += 1
print(f"{'敌群':<10}{'场数':>5}{'出招':>6}{'特殊':>6}{'份额':>7}"
f"{'cast隔p50':>10}{'p90':>5}{'max':>5}"
f"{'round隔p50':>11}{'p90':>5}{'max':>5}{'连击max':>8}")
for actor, a in sorted(agg.items()):
share = a["specials"] / a["casts"] if a["casts"] else 0
print(f"{actor:<10}{a['battles']:>5}{a['casts']:>6}{a['specials']:>6}{share:>7.1%}"
f"{str(pct(a['cast_gaps'],0.5)):>10}{str(pct(a['cast_gaps'],0.9)):>5}"
f"{str(max(a['cast_gaps']) if a['cast_gaps'] else '-'):>5}"
f"{str(pct(a['round_gaps'],0.5)):>11}{str(pct(a['round_gaps'],0.9)):>5}"
f"{str(max(a['round_gaps']) if a['round_gaps'] else '-'):>5}"
f"{str(max(a['streaks']) if a['streaks'] else 0):>8}")
total_battles = len(battles)
print(f"\n战斗总数 {total_battles}|跳战(敌未行动){no_action_battles}"
f"|过旱(有行动·全战斗零特殊技){drought_battles}"
f"|正常 {normal_battles}")
print(f"过涝信号(单敌群连续特殊技≥3){flood_signals} 处|全场最长连击 {flood_max}")
# 保底越界样本:cast 间隔 > M(M=4 → 间隔 ≥5 普攻才越界;间隔 4 = 连续 4 拍普攻后保底必出,合法)
total_over = sum(1 for a in agg.values() for g in a["cast_gaps"] if g >= 5)
total_gaps = sum(len(a["cast_gaps"]) for a in agg.values())
print(f"保底越界(相邻特殊技间隔≥5 普攻):{total_over}/{total_gaps}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
main(sys.argv[1:])