steam-backlog-enforcer/steam_backlog_enforcer/_gaming_days.py
Krzysztof kuhy Rudnicki a09f08701a playtime: drop the Fri-Mon carry-over; log budget on change only
A Sunday ran past the 6.0h the i3 bar showed: 2h47m of Saturday's
leftover was carried into the budget the cutoff used, and the 3-second
"Gaming budget" journal line omitted it. Unspent time is now dropped at
06:00 every day; load_state takes a legacy ledger's share back out of
today's high-water mark. The budget line logs only on change (was 25k
lines/day), fired warnings are logged, and budget.jsonl records carry
budget_seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PrwCnQouKTF4EUMENMxnG
2026-10-18 21:37:29 +02:00

62 lines
2.1 KiB
Python

"""Gaming-day arithmetic: which day *now* is, and what it already granted.
A gaming "day" runs 06:00-05:59 local, so late-night sessions count against
the day they started in.
There is no carry-over: unspent time is dropped at 06:00, every day. The
Friday-Monday carry ledger was removed on 2026-10-18, after a Sunday ran
past the 6.0h the i3 bar showed because 2h47m had been carried into it.
``load_state`` takes a legacy ledger's share back out of the stored
high-water mark.
Everything here is pure: it works on primitives handed to it, so it imports
nothing from the playtime modules and cannot close an import cycle.
"""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING, Final
if TYPE_CHECKING:
from datetime import datetime
from steam_backlog_enforcer._playtime_state import PlaytimeState
# The gaming day starts here, local time. 05:59 belongs to the previous day.
_DAY_BOUNDARY_HOURS: Final = 6
def gaming_day_key(now: datetime) -> str:
"""Return the ``YYYY-MM-DD`` gaming day containing *now*.
The boundary is 06:00 local, so 02:00 on the 5th belongs to the 4th.
Args:
now: Timezone-aware local timestamp.
Returns:
The gaming day key.
"""
return (now - timedelta(hours=_DAY_BOUNDARY_HOURS)).date().isoformat()
def held_today(stored: PlaytimeState | None, now: datetime) -> float:
"""Return the budget already granted earlier in *now*'s gaming day.
Earners reset at calendar midnight but the gaming day runs to 05:59, and a
cold restart starts with empty answer caches, so a live re-resolution can
come in lower than what was granted hours ago. ``rules_for`` holds the day
at this high-water mark so neither can cut a session off unwarned.
Args:
stored: The persisted production state, or ``None`` if unreadable.
now: Local timestamp for this tick.
Returns:
The recorded high-water budget; 0 when the state is another day's.
"""
if stored is None or stored.day_key != gaming_day_key(now):
return 0.0
return stored.budget_seconds