Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 11:23:13 -04:00
1 parent 1fedb9368d
commit cc3b2fa4d5
27 files changed
+1988 -83

No files matched your search

+34 -7
View File
@@ -166,9 +166,7 @@ def random_party(builds: Sequence[dict[str, Any]], seed: int) -> list[dict[str,
generator = random.Random(seed)
# Named alternatives remain directly selectable and are covered by "all",
# but ordinary random/filler seats should use the declared default profile.
defaults = [build for build in builds if build.get("defaultTalentBuild")]
if defaults:
builds = defaults
builds = random_party_builds(builds)
by_role: dict[str, list[dict[str, Any]]] = defaultdict(list)
for build in builds:
by_role[build["role"]].append(build)
@@ -178,6 +176,21 @@ def random_party(builds: Sequence[dict[str, Any]], seed: int) -> list[dict[str,
return [generator.choice(by_role["tank"]), generator.choice(by_role["healer"]), *damage]
def random_party_builds(builds: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
defaults = [build for build in builds if build.get("defaultTalentBuild")]
return defaults or list(builds)
def random_party_capacity(builds: Sequence[dict[str, Any]]) -> int:
"""Return the number of unique 1/1/3 compositions in the random pool."""
by_role: dict[str, int] = defaultdict(int)
for build in random_party_builds(builds):
by_role[build["role"]] += 1
if not by_role["tank"] or not by_role["healer"] or by_role["damage"] < 3:
return 0
return by_role["tank"] * by_role["healer"] * math.comb(by_role["damage"], 3)
def readiness_eligible_builds(
builds: Sequence[dict[str, Any]],
policy: dict[str, Any] | None,
@@ -209,14 +222,28 @@ def automatic_parties(
configurations: list[dict[str, Any]] = []
for source in sources:
source_builds = [build for build in builds if build["source"] == source]
capacity = random_party_capacity(source_builds)
if capacity < 1:
raise SystemExit(
f"The {source} eligible pool does not expose enough tank, healer, and damage builds "
"for a 1/1/3 party."
)
target_configurations = min(configurations_per_source, capacity)
if target_configurations < configurations_per_source:
print(
f"[balance-lab] {source} eligible pool has {capacity} unique party "
f"configuration{'s' if capacity != 1 else ''}; running each once instead of "
f"duplicating {configurations_per_source - capacity} identical parties.",
file=sys.stderr,
)
seen: set[tuple[str, str, tuple[str, ...]]] = set()
attempts = 0
while len(seen) < configurations_per_source:
while len(seen) < target_configurations:
attempts += 1
if attempts > max(1_000, configurations_per_source * 100):
if attempts > max(1_000, target_configurations * 100):
raise SystemExit(
f"Could not generate {configurations_per_source} unique {source} parties. "
"Request fewer party configurations."
f"Could not generate {target_configurations} unique {source} parties from a "
f"calculated capacity of {capacity}."
)
party = random_party(source_builds, seed + offsets[source] + attempts - 1)
signature = (party[0]["id"], party[1]["id"], tuple(sorted(member["id"] for member in party[2:])))
+17 -3
View File
@@ -71,8 +71,11 @@ const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-balance-ui
assert(Number(await page.getByLabel('Coverage parties').inputValue()) > 1, 'all-spec coverage should derive multiple parties');
await page.getByLabel('Class/spec selection').selectOption('specific');
await page.getByLabel('Search Balance Lab class specs').fill('Paladin Holy');
await page.getByRole('checkbox', { name: /Paladin · Holy/ }).check();
await page.getByRole('checkbox', { name: /Paladin · Holy.*Holy Playerbots PvE/ }).check();
assert.equal(await page.getByLabel('Coverage parties').inputValue(), '1');
await page.getByLabel('Class/spec selection').selectOption('random');
await page.getByLabel('Parties per source').fill('10');
await page.getByText(/eligible pools provide 1 unique party configuration/i).waitFor();
await page.getByLabel('Runs per party/boss').fill('1');
await page.getByLabel('Fight limit (seconds)').fill('5');
await page.getByLabel('Bosses').selectOption('specific');
@@ -86,7 +89,7 @@ const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-balance-ui
await page.getByRole('button', { name: 'Run on server' }).click();
await page.locator('.balance-lab-job-list em.is-complete').waitFor({ timeout: 240_000 });
await page.getByRole('button', { name: 'Open full report' }).waitFor();
assert.equal(await page.locator('.balance-lab-artifacts > button').count(), 12);
assert((await page.locator('.balance-lab-artifacts > button').count()) >= 12);
await capture('04-server-job-complete');
await page.getByRole('button', { name: 'View graphs and filterable data' }).click();
@@ -100,7 +103,7 @@ const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-balance-ui
assert.match(await page.locator('.balance-results-table-panel tbody').innerText(), /Eck the Ferocious/);
await page.getByRole('button', { name: 'Class / spec', exact: true }).click();
const allClassRows = await page.locator('.balance-results-table-panel tbody tr').count();
assert(allClassRows >= 5, 'the complete five-member party should be present in the class/spec table');
assert.equal(allClassRows, 4, 'the four NPC teammates should be present in the NPC-only class/spec table');
await page.getByLabel('Sort class specs').selectOption('totalDamageTaken');
assert.equal(await page.getByLabel('Sort class specs').inputValue(), 'totalDamageTaken');
await page.getByLabel('Class role').selectOption('damage');
@@ -148,6 +151,17 @@ const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-balance-ui
await page.getByText('Every class/spec ability').scrollIntoViewIfNeeded();
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true);
await capture('07-mobile-abilities');
await page.setViewportSize({ width: 1440, height: 1000 });
await page.getByLabel('Level coverage').selectOption('brackets');
await page.getByLabel('Bracket spacing').selectOption('10');
assert.equal(await page.locator('.balance-lab-estimate span').filter({ hasText: 'Level jobs' }).locator('b').innerText(), '9');
await page.getByRole('button', { name: 'Queue 9 level jobs' }).click();
await page.waitForFunction(() => (
document.querySelectorAll('.balance-lab-job-list em.is-complete').length >= 10
), null, { timeout: 240_000 });
assert.equal(await page.locator('.balance-lab-job-list em.is-failed').count(), 0);
await capture('08-nine-bracket-jobs-complete');
assert.deepEqual(pageErrors, []);
console.log('Balance Lab GM playtest passed');
} finally {
+14
View File
@@ -6,6 +6,7 @@ from scripts.balance_lab import (
automatic_parties,
coverage_parties,
random_party,
random_party_capacity,
readiness_eligible_builds,
resolve_build,
runtime_session_command,
@@ -89,6 +90,19 @@ class BalanceLabControllerTests(unittest.TestCase):
"tank", "healer", "damage", "damage", "damage",
])
def test_automatic_parties_cap_to_the_unique_ready_pool(self) -> None:
builds = [
{"id": "ready-tank", "source": "wow", "role": "tank", "defaultTalentBuild": True},
{"id": "ready-healer", "source": "wow", "role": "healer", "defaultTalentBuild": True},
*({"id": f"ready-damage-{index}", "source": "wow", "role": "damage", "defaultTalentBuild": True}
for index in range(3)),
{"id": "testing-tank-alt", "source": "wow", "role": "tank", "defaultTalentBuild": False},
]
self.assertEqual(random_party_capacity(builds), 1)
configurations = automatic_parties(builds, ("wow",), 10, 29)
self.assertEqual(len(configurations), 1)
self.assertEqual(len({member["id"] for member in configurations[0]["party"]}), 5)
def test_coverage_parties_include_every_build_with_minimum_unique_parties(self) -> None:
builds = [
{"id": "tank-a", "source": "wow", "role": "tank"},