fix: HLTB matching uses game_alias for renamed games & hide library on reassign

- _pick_best_hltb_entry: check game_alias in exact-match fallback so
  renamed games (e.g. 'Needy Streamer Overload' -> 'NEEDY GIRL OVERDOSE')
  are not beaten by spinoffs with matching prefixes
- _try_reassign_shorter_game: call hide_other_games after pick_next_game
  so library visibility is updated on reassignment, not only on completion
- Added tests for alias matching and all hiding branches (100% coverage)
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-30 20:06:46 +02:00
parent f793cae3e2
commit faf7f7f46b
4 changed files with 115 additions and 1 deletions

View File

@ -108,6 +108,14 @@ def _try_reassign_shorter_game(
f" (~{playable.completionist_hours:.1f}h vs ~{hours:.1f}h)"
)
pick_next_game(all_games, state, config)
if state.current_app_id is not None:
owned_ids = get_all_owned_app_ids(config)
if owned_ids:
hidden = hide_other_games(owned_ids, state.current_app_id)
if hidden > 0:
_echo(f"\n Library: hid {hidden} games")
return True

View File

@ -185,6 +185,7 @@ def _pick_best_hltb_entry(
reverse=True,
)
if (e.get("game_name") or "").lower() == lower
or (e.get("game_alias") or "").lower() == lower
),
None,
)

View File

@ -56,7 +56,7 @@ class TestTryReassignShorterGame:
_snap(1, "Long", 10, 5, 100.0),
_snap(2, "Short", 10, 5, 5.0),
]
state = State(current_app_id=1, current_game_name="Long")
state = State(current_app_id=2, current_game_name="Short")
short_game = GameInfo(
app_id=2,
name="Short",
@ -73,6 +73,11 @@ class TestTryReassignShorterGame:
return_value=short_game,
),
patch(f"{CMD_DONE_PKG}.pick_next_game"),
patch(
f"{CMD_DONE_PKG}.get_all_owned_app_ids",
return_value=[1, 2, 3],
),
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=5) as mock_hide,
):
result = _try_reassign_shorter_game(
{1: 100.0, 2: 5.0},
@ -82,6 +87,80 @@ class TestTryReassignShorterGame:
Config(),
)
assert result
mock_hide.assert_called_once_with([1, 2, 3], 2)
def test_reassigns_no_hide_when_no_owned_ids(self) -> None:
snap = [
_snap(1, "Long", 10, 5, 100.0),
_snap(2, "Short", 10, 5, 5.0),
]
state = State(current_app_id=2, current_game_name="Short")
short_game = GameInfo(
app_id=2,
name="Short",
total_achievements=10,
unlocked_achievements=5,
playtime_minutes=60,
completionist_hours=5.0,
)
with (
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
patch(f"{CMD_DONE_PKG}._echo") as mock_echo,
patch(
f"{CMD_DONE_PKG}._pick_playable_candidate",
return_value=short_game,
),
patch(f"{CMD_DONE_PKG}.pick_next_game"),
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[1, 2]),
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=0),
):
result = _try_reassign_shorter_game(
{1: 100.0, 2: 5.0},
1,
100.0,
state,
Config(),
)
assert result
# hidden == 0, so "hid N games" should NOT be echoed
for call in mock_echo.call_args_list:
assert "hid" not in str(call)
def test_reassigns_skip_hide_when_no_app_assigned(self) -> None:
snap = [
_snap(1, "Long", 10, 5, 100.0),
_snap(2, "Short", 10, 5, 5.0),
]
state = State(current_app_id=None, current_game_name="")
short_game = GameInfo(
app_id=2,
name="Short",
total_achievements=10,
unlocked_achievements=5,
playtime_minutes=60,
completionist_hours=5.0,
)
with (
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
patch(f"{CMD_DONE_PKG}._echo"),
patch(
f"{CMD_DONE_PKG}._pick_playable_candidate",
return_value=short_game,
),
patch(f"{CMD_DONE_PKG}.pick_next_game"),
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids") as mock_owned,
patch(f"{CMD_DONE_PKG}.hide_other_games") as mock_hide,
):
result = _try_reassign_shorter_game(
{1: 100.0, 2: 5.0},
1,
100.0,
state,
Config(),
)
assert result
mock_owned.assert_not_called()
mock_hide.assert_not_called()
def test_playable_none(self) -> None:
snap = [
@ -157,6 +236,8 @@ class TestTryReassignShorterGame:
) as mock_pick_playable,
patch(f"{CMD_DONE_PKG}.pick_next_game"),
patch(f"{CMD_DONE_PKG}._echo"),
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[]),
patch(f"{CMD_DONE_PKG}.hide_other_games"),
):
result = _try_reassign_shorter_game(
{1: 20.1},

View File

@ -321,3 +321,27 @@ class TestPickBestHltbEntry:
result = _pick_best_hltb_entry("Killing Floor", [(spinoff, 0.7), (base, 1.0)])
assert result is not None
assert result[0]["game_name"] == "Killing Floor"
def test_alias_exact_match_beats_spinoff(self) -> None:
"""Base game found via game_alias beats a spinoff with matching prefix.
When HLTB renames a game (e.g. 'Needy Streamer Overload' ->
'NEEDY GIRL OVERDOSE'), the old name lives in game_alias. A spinoff
like 'Needy Streamer Overload: Typing of The Net' must NOT be
preferred just because its game_name starts with the search term.
"""
base: dict[str, Any] = {
"game_name": "NEEDY GIRL OVERDOSE",
"game_alias": "Needy Streamer Overload",
"comp_100": 43200, # 12 h
}
spinoff: dict[str, Any] = {
"game_name": "Needy Streamer Overload: Typing of The Net",
"comp_100": 3600, # 1 h
}
result = _pick_best_hltb_entry(
"NEEDY STREAMER OVERLOAD",
[(spinoff, 0.7), (base, 0.9)],
)
assert result is not None
assert result[0]["game_name"] == "NEEDY GIRL OVERDOSE"