From 073cf446ce99edbfe3ce3ed5eaafff71511fdfe9 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:22:59 -0600 Subject: [PATCH] =?UTF-8?q?feat(draw):=20generational=20TOP-OFF=20?= =?UTF-8?q?=E2=80=94=20every=20wave=20fills=20from=20the=20lowest=20genera?= =?UTF-8?q?tion=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither extreme was right. Ignoring generations lets a card that has failed five waves compete with one nobody has ever drafted; filtering to a single generation starved the fleet to 46 cards. So generation becomes the PRIMARY ORDERING and the existing mass/count criterion breaks ties, at both levels of the assembly: * gate groups holding never-drafted cards rank ahead of all-retry groups (before the --max-bins truncation, so an untouched group is never cut for a fat retry group); * within a group, untouched cards are taken before retries. Wave SIZE is untouched — only the order changes — so the fleet stays full while the scarce never-drafted work always goes out first. Verified on a live draw: gen0 2 · gen1 2 · gen2 14 · gen3 2 · gen4 3 · gen5+ 340. It took EVERY card below generation 5 (all 23 available) and filled the remaining 340 slots from the 5+ pile, which is the whole point. The gen0 count is 2 because wave dw drew the last 51 untouched skeletons an hour ago. That is the campaign's real state: essentially everything drawable has now been drafted at least once, and ~635 distinct skeletons have refused. The remaining work is levers, not draws. --- tools/build_wave_atlas.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tools/build_wave_atlas.py b/tools/build_wave_atlas.py index c77400f56..aaa1d5bb0 100644 --- a/tools/build_wave_atlas.py +++ b/tools/build_wave_atlas.py @@ -597,6 +597,14 @@ if a.one_per_gid: by_tu = collections.defaultdict(list) for c in cands: by_tu[(c['binary'], c['tu'])].append(c) +# GENERATIONAL TOP-OFF (P31 S60, Drew): a wave fills from the LOWEST generation available and +# only then moves up, instead of either ignoring generations (retries compete with never-drafted +# work) or filtering to one generation (which starved the fleet to 46 cards — see --generational). +# Applied at BOTH levels: gate groups holding never-drafted cards rank ahead of all-retry groups, +# and within a group the untouched cards are taken first. Generation is the primary key and the +# existing mass/count criterion breaks ties, so wave SIZE is untouched — only the ORDER changes. +_mingen = {k: min(draw_count.get(c['fn'], 0) for c in v) for k, v in by_tu.items()} + if a.rank == 'total': # P31 S54: rank by the mass a card actually DELIVERS -- its own instructions plus the same-gid # siblings the post-bank remap banks for free. Measured on the wave-T draw: the 70 selected @@ -606,12 +614,13 @@ if a.rank == 'total': if not a.one_per_gid: sys.exit("--rank total requires --one-per-gid (there are no deferred siblings otherwise)") _sibins = {g: sum(c['nins'] for c in v) for g, v in siblings.items()} - ranked = sorted(by_tu, key=lambda k: -sum(c['nins'] + _sibins.get(c['gid'], 0) - for c in by_tu[k]))[:a.max_bins] + ranked = sorted(by_tu, key=lambda k: (_mingen[k], + -sum(c['nins'] + _sibins.get(c['gid'], 0) + for c in by_tu[k])))[:a.max_bins] elif a.rank == 'mass': - ranked = sorted(by_tu, key=lambda k: -sum(c['nins'] for c in by_tu[k]))[:a.max_bins] + ranked = sorted(by_tu, key=lambda k: (_mingen[k], -sum(c['nins'] for c in by_tu[k])))[:a.max_bins] else: - ranked = sorted(by_tu, key=lambda k: -len(by_tu[k]))[:a.max_bins] + ranked = sorted(by_tu, key=lambda k: (_mingen[k], -len(by_tu[k])))[:a.max_bins] # principle 3 (P31 S52): SIZE A WAVE BY INSTRUCTIONS, NOT BY CARDS. The public metric is # instruction-weighted, so a wave is worth what its instructions are worth: the 12-42-ins card @@ -656,7 +665,7 @@ if _jt_pool: f"card(s) (quota; one per binary, each one extra rebuild at gate time)", file=sys.stderr) for k in ranked: # principle 2: within a group, mass first - for c in sorted(by_tu[k], key=lambda c: -_deliver(c)): + for c in sorted(by_tu[k], key=lambda c: (draw_count.get(c['fn'], 0), -_deliver(c))): if _full(): break if id(c) in _in_wave: continue # already seeded by a quota # QUOTA-ONLY LEVERS. A quota is a FLOOR unless it is also a ceiling: with the tell levers in