From 559888cc0c9e4700f1264e3f85fe1abc82d513d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Thu, 20 Aug 2026 13:56:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD=D1=81=20=D1=81=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=83=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=BA=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...test_build87_penalty_strength_rebalance.py | 24 +++++++++++++++++++ ui_builder/static/app.js | 14 ++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/test_build87_penalty_strength_rebalance.py diff --git a/tests/test_build87_penalty_strength_rebalance.py b/tests/test_build87_penalty_strength_rebalance.py new file mode 100644 index 0000000..4e48df2 --- /dev/null +++ b/tests/test_build87_penalty_strength_rebalance.py @@ -0,0 +1,24 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8") + + +def test_authoritative_strength_change_rebalances_penalty_targets(): + start = APP_JS.index("function hockeyStoreGameControl") + end = APP_JS.index("async function hockeyLoadGameControl", start) + block = APP_JS[start:end] + assert "const strengthChanged" in block + assert "hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload)" in block + assert "rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })" in block + assert "Penalty strength rebalance error" in block + + +def test_penalty_plan_still_routes_two_vs_one_to_advantage_side(): + start = APP_JS.index("function penaltyDisplayEntriesByTargetSide") + end = APP_JS.index("function hockeyPenaltySideMappingDetail", start) + block = APP_JS[start:end] + assert 'if (advantageSide === "home")' in block + assert 'return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };' in block + assert 'if (advantageSide === "away")' in block + assert 'return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };' in block diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js index 9a750f8..38058d9 100644 --- a/ui_builder/static/app.js +++ b/ui_builder/static/app.js @@ -11132,8 +11132,20 @@ function renderHockeyPenaltyDashboard(node, component, runtime) { { render } ); hockeyBackupPlayerSelectionValues(gameId, payload?.values || {}); - if (!previousControl || hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload)) { + const strengthChanged = !previousControl + || hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload); + if (strengthChanged) { hockeyRefreshVmixMappingForStrength(gameId, previousControl, payload).catch(() => {}); + // BUILD87: penalty routing depends on the recalculated numerical strength. + // The local penalty editor can rebalance a few milliseconds before the + // timer save response arrives, while state.data still contains the old + // 4x4/5x5 strength. Re-run the routing as soon as the authoritative + // control payload is stored so 2 penalties vs 1 immediately becomes + // 3x4/4x3 and the plate moves to the team with the advantage. + if (state.activeHockeyVmixTimerSteps.size) { + rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }) + .catch((error) => console.error("Penalty strength rebalance error", error)); + } } if (previousControl && hockeyTeamStateFlagSignature(previousControl) !== hockeyTeamStateFlagSignature(payload) && hockeyTeamStateScoreboardIsLive()) { hockeySyncTeamStateOverlays({ force: true }).catch((error) => console.error("vMix team-state overlay sync error", error));