M6+M11: learned weights with shrinkage, refresh command, cron docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
team3
2026-07-23 00:42:03 +02:00
parent 61127fa7e0
commit 4cae436231
5 changed files with 172 additions and 6 deletions

View File

@@ -29,6 +29,8 @@ def main() -> None:
p_auto.add_argument("--policy", choices=["afk", "econ"], default="econ")
p_auto.add_argument("--games", type=int, default=200)
sub.add_parser("refresh", help="daily job: crawl + extract + build-artifact + calibrate")
args = parser.parse_args()
if args.command == "fetch-static":
@@ -63,14 +65,19 @@ def main() -> None:
print("all ids resolved against static data")
elif args.command == "build-artifact":
from tft import db
from tft.model import artifact as artifact_mod
from tft.model.learn import learn_from_db
from tft.staticdata.fetch import load_static
static = load_static()
learned = {}
art = artifact_mod.build(static, learned)
conn = db.connect()
learned = learn_from_db(conn, static["meta"]["set"])
conn.close()
n_boards = learned.pop("n_boards", 0)
art = artifact_mod.build(static, learned, {"boards_learned_from": n_boards})
path = artifact_mod.save(art)
print(f"artifact written: {path}")
print(f"artifact written: {path} ({n_boards} boards learned from)")
elif args.command == "calibrate":
from tft import db
@@ -98,6 +105,16 @@ def main() -> None:
f"avg placement {result['avg_placement']:.2f}, "
f"top4 {result['top4_rate']:.0%}")
elif args.command == "refresh":
import subprocess
import sys
for stage in ("crawl", "extract", "build-artifact", "calibrate"):
print(f"== {stage} ==", flush=True)
result = subprocess.run([sys.executable, "-m", "tft.cli", stage])
if result.returncode != 0:
raise SystemExit(f"refresh aborted at {stage}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,88 @@
"""Learn multiplicative strength weights from endboards via placement deltas.
All multipliers center at 1.0 with Bayesian shrinkage: with thin data the
score model falls back to the rule baseline (survives set launch).
"""
import json
from collections import defaultdict
MEAN_PLACEMENT = 4.5
PSEUDO_COUNT = 30
# Platzierungs-Delta -> Multiplikator: 1 Platz besser als der Schnitt ≈ +6 %.
DELTA_SCALE = 0.06
MULT_CAP = (0.8, 1.25)
PAIR_MIN_N = 20
PAIR_LIFT_CAP = (-0.9, 1.15)
def _multiplier(placements: list[int]) -> float:
n = len(placements)
mean = (sum(placements) + MEAN_PLACEMENT * PSEUDO_COUNT) / (n + PSEUDO_COUNT)
mult = 1.0 + (MEAN_PLACEMENT - mean) * DELTA_SCALE
return min(max(mult, MULT_CAP[0]), MULT_CAP[1])
def learn(rows: list[tuple]) -> dict:
"""rows: (placement, units_json, traits_json, augments_json)."""
trait_placements = defaultdict(list)
item_placements = defaultdict(list)
augment_placements = defaultdict(list)
unit_top4 = defaultdict(int)
pair_top4 = defaultdict(int)
n_boards = 0
n_top4 = 0
for placement, units_json, traits_json, augments_json in rows:
n_boards += 1
top4 = placement <= 4
n_top4 += top4
units = json.loads(units_json)
names = sorted({u["character_id"] for u in units})
for u in units:
for item in u.get("itemNames", []):
item_placements[item].append(placement)
for t in json.loads(traits_json):
if t.get("tier_current", 0) > 0:
trait_placements[f"{t['name']}@{t['tier_current']}"].append(placement)
for a in json.loads(augments_json):
augment_placements[a].append(placement)
if top4:
for name in names:
unit_top4[name] += 1
for i, a in enumerate(names):
for b in names[i + 1 :]:
pair_top4[(a, b)] += 1
pairs = {}
if n_top4:
for (a, b), n_ab in pair_top4.items():
if n_ab < PAIR_MIN_N:
continue
expected = unit_top4[a] * unit_top4[b] / n_top4
if expected <= 0:
continue
lift = n_ab / expected - 1.0
lift = min(max(lift, PAIR_LIFT_CAP[0]), PAIR_LIFT_CAP[1])
pairs[f"{a}|{b}"] = round(lift, 4)
return {
"traits": {k: round(_multiplier(v), 4) for k, v in trait_placements.items()},
"items": {k: round(_multiplier(v), 4) for k, v in item_placements.items()},
"augments": {k: round(_multiplier(v), 4) for k, v in augment_placements.items()},
"pairs": pairs,
"n_boards": n_boards,
}
def learn_from_db(conn, set_number: int, exclude_holdout: bool = True) -> dict:
where = "WHERE set_number = ?"
if exclude_holdout:
where += " AND rowid % 5 != 0"
rows = conn.execute(
f"SELECT placement, units, traits, augments FROM endboards {where}",
(set_number,),
).fetchall()
return learn(rows)

View File

@@ -62,8 +62,11 @@ def score_board(board_units: list[dict], augments: list[str], artifact: dict) ->
total *= augment_mults.get(augment, 1.0)
names = sorted({u["api_name"] for u in board_units})
for i, a in enumerate(names):
for b in names[i + 1 :]:
total += pair_lifts.get(f"{a}|{b}", 0.0)
lift_sum = sum(
pair_lifts.get(f"{a}|{b}", 0.0)
for i, a in enumerate(names)
for b in names[i + 1 :]
)
total *= 1 + min(max(lift_sum * 0.01, -0.10), 0.10)
return total