mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
earned_time 0.12.0: shutdown rungs equal gaming pay from 2026-10-18 (workout 120, LeetCode 60, reading 60, tutor 60; floor 18:00)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3f01c36e1
commit
ef244889fd
@ -29,16 +29,8 @@ never a same-day cut):
|
||||
earner name; all of a counted earner's capped units), so doing everything
|
||||
lands exactly on the ceiling. Since 0.6.1 `TUTOR_FROM == LADDER_FROM`, so
|
||||
every real ladder day uses `TUTOR_LADDER` (below); `LADDER`, the pre-tutor
|
||||
split, is never in force on a real day:
|
||||
|
||||
| done | shutdown |
|
||||
|---|---|
|
||||
| nothing | 19:00 |
|
||||
| workout (110) | 20:50 |
|
||||
| + LeetCode (50) | 21:40 |
|
||||
| + reading (30) | 22:10 |
|
||||
| + Anki (25) | 22:35 |
|
||||
| + Automation (25) | 23:00 |
|
||||
split (workout 110, LeetCode 50, reading 30, Anki 25, Automation 25; floor
|
||||
19:00), is never in force on a real day.
|
||||
|
||||
A second workout earns nothing more (`Rung.extra` defaults to 0).
|
||||
Move the alarm (`WAKE_MINUTES = 6 * 60`) and the whole ladder shifts an hour
|
||||
@ -48,6 +40,11 @@ never a same-day cut):
|
||||
The two floors are derived separately (`_resolve._shutdown_floor`), so the
|
||||
ladder cannot move a gaming minute.
|
||||
|
||||
**Unified ladder (0.12.0, `UNIFIED_FROM` 2026-10-18):** each rung pays what
|
||||
its earner pays in gaming (workout 120, LeetCode 60, reading 60, tutor 1/unit
|
||||
to 60), so the floor drops 18:50 -> 18:00 under the same 23:00 ceiling:
|
||||
nothing 3 h / 18:00, workout 5 h / 20:00, +LeetCode 21:00, +reading 22:00.
|
||||
|
||||
**Sick days (0.11.0, `_sick.py`).** `resolve(..., sick=True)` floors the
|
||||
*final* shutdown at 20:00 (`SICK_SHUTDOWN_FLOOR_MINUTES`), capped at the
|
||||
ceiling: 18:50 becomes 20:00, 21:30 stays 21:30 (nothing stacks on the floor).
|
||||
@ -243,7 +240,7 @@ on the day itself is logged and does not count.
|
||||
## Install
|
||||
|
||||
```console
|
||||
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.11.0#subdirectory=earned_time"
|
||||
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.12.0#subdirectory=earned_time"
|
||||
```
|
||||
|
||||
The root `steam-backlog-enforcer.service` sets `HOME=/home/kuhy`, so it imports
|
||||
|
||||
@ -46,6 +46,8 @@ from earned_time._ladder import (
|
||||
SHUTDOWN_CEILING_MINUTES,
|
||||
TUTOR_FROM,
|
||||
TUTOR_LADDER,
|
||||
UNIFIED_FROM,
|
||||
UNIFIED_LADDER,
|
||||
WAKE_MINUTES,
|
||||
Rung,
|
||||
extra_shutdown_minutes_for,
|
||||
@ -106,6 +108,8 @@ __all__ = [
|
||||
"TUTOR_LADDER",
|
||||
"TUTOR_QUOTA_FROM",
|
||||
"TUTOR_QUOTA_MINUTES",
|
||||
"UNIFIED_FROM",
|
||||
"UNIFIED_LADDER",
|
||||
"WAKE_MINUTES",
|
||||
"WORKOUT",
|
||||
"Base",
|
||||
|
||||
@ -9,7 +9,9 @@ frozen ceiling, so the switch never rewrites how an old day resolved. Gaming
|
||||
is not on the ladder.
|
||||
|
||||
From ``TUTOR_FROM`` the rungs are :data:`TUTOR_LADDER`: Anki is retired and
|
||||
its 25 minutes fold into the tutor's, paid per active minute.
|
||||
its 25 minutes fold into the tutor's, paid per active minute. From
|
||||
``UNIFIED_FROM`` they are :data:`UNIFIED_LADDER`: each rung equals its
|
||||
earner's gaming pay.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -47,6 +49,10 @@ TUTOR_FROM: Final = date(2026, 10, 10)
|
||||
# floor rises by exactly what the two could have paid back), so it may land
|
||||
# on its deploy day: 0.5.0 landed 2026-10-09.
|
||||
ANKI_WAIVED_FROM: Final = date(2026, 10, 9)
|
||||
# From this day every rung pays exactly the gaming time its earner pays
|
||||
# (:data:`UNIFIED_LADDER`, 0.12.0). It lowers the floor 18:50 -> 18:00, so it
|
||||
# lands the day after the deploy (2026-10-17), never on it.
|
||||
UNIFIED_FROM: Final = date(2026, 10, 18)
|
||||
_SLEEP_MINUTES: Final = 8 * 60
|
||||
_DAY_MINUTES: Final = 24 * 60
|
||||
|
||||
@ -109,6 +115,19 @@ TUTOR_LADDER: Final[Mapping[str, Rung]] = MappingProxyType(
|
||||
)
|
||||
|
||||
|
||||
# From ``UNIFIED_FROM``: shutdown and gaming pay the same -- workout 120,
|
||||
# LeetCode 60, reading 60, tutor 1 per unit up to 60. The rungs sum to 300:
|
||||
# the ceiling stays 23:00 and the floor drops to 18:00.
|
||||
UNIFIED_LADDER: Final[Mapping[str, Rung]] = MappingProxyType(
|
||||
{
|
||||
"workout": Rung(120),
|
||||
"leetcode": Rung(60),
|
||||
"reading": Rung(60),
|
||||
"tutor": Rung(1, extra=1),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def on_ladder(day: date) -> bool:
|
||||
"""Whether ``day`` resolves shutdown on the sleep ladder."""
|
||||
return day >= LADDER_FROM
|
||||
@ -118,6 +137,8 @@ def ladder_for(day: date) -> Mapping[str, Rung] | None:
|
||||
"""The rungs in force on ``day``; ``None`` before the ladder."""
|
||||
if not on_ladder(day):
|
||||
return None
|
||||
if day >= UNIFIED_FROM:
|
||||
return UNIFIED_LADDER
|
||||
return TUTOR_LADDER if day >= TUTOR_FROM else LADDER
|
||||
|
||||
|
||||
|
||||
63
earned_time/earned_time/tests/test_unified.py
Normal file
63
earned_time/earned_time/tests/test_unified.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright (c) 2026 Krzysztof Rudnicki
|
||||
"""The unified ladder (0.12.0): every shutdown rung equals its gaming pay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from earned_time import (
|
||||
TUTOR_EARNERS,
|
||||
TUTOR_LADDER,
|
||||
UNIFIED_FROM,
|
||||
UNIFIED_LADDER,
|
||||
ladder_for,
|
||||
resolve,
|
||||
shutdown_ceiling_for,
|
||||
)
|
||||
|
||||
EVE = UNIFIED_FROM - timedelta(days=1)
|
||||
|
||||
|
||||
def _hhmm(minutes: int) -> str:
|
||||
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
||||
|
||||
|
||||
def _day(day: date, **done: int) -> tuple[int, str]:
|
||||
answers = {e.name: done.get(e.name, 0) for e in TUTOR_EARNERS}
|
||||
out = resolve(answers, day, TUTOR_EARNERS)
|
||||
return out.gaming_minutes, _hhmm(out.shutdown_minutes)
|
||||
|
||||
|
||||
def test_switch_is_the_day_after_the_deploy() -> None:
|
||||
assert date(2026, 10, 18) == UNIFIED_FROM
|
||||
assert ladder_for(EVE) is TUTOR_LADDER # deploy day keeps the old rungs
|
||||
assert ladder_for(UNIFIED_FROM) is UNIFIED_LADDER
|
||||
|
||||
|
||||
def test_each_rung_pays_what_gaming_pays() -> None:
|
||||
for e in TUTOR_EARNERS:
|
||||
units = e.max_units or 1
|
||||
assert UNIFIED_LADDER[e.name].minutes(units) == e.gaming_for(units), e.name
|
||||
assert {n: r.minutes(60) for n, r in UNIFIED_LADDER.items()} == {
|
||||
"workout": 120,
|
||||
"leetcode": 60,
|
||||
"reading": 60,
|
||||
"tutor": 60,
|
||||
}
|
||||
|
||||
|
||||
def test_ceiling_stays_and_floor_drops_to_18() -> None:
|
||||
assert shutdown_ceiling_for(UNIFIED_FROM) == 23 * 60
|
||||
assert _day(UNIFIED_FROM) == (180, "18:00")
|
||||
assert _day(UNIFIED_FROM, workout=1) == (300, "20:00")
|
||||
assert _day(UNIFIED_FROM, workout=2) == (300, "20:00") # 2nd unit pays 0
|
||||
assert _day(UNIFIED_FROM, workout=1, leetcode=1) == (360, "21:00")
|
||||
assert _day(UNIFIED_FROM, workout=1, leetcode=1, reading=1) == (420, "22:00")
|
||||
full = _day(UNIFIED_FROM, workout=1, leetcode=1, reading=1, tutor=60)
|
||||
assert full == (480, "23:00")
|
||||
assert _day(UNIFIED_FROM, tutor=30) == (210, "18:30") # per unit, 1:1
|
||||
|
||||
|
||||
def test_old_days_keep_the_tutor_ladder() -> None:
|
||||
assert _day(EVE) == (180, "18:50")
|
||||
assert _day(EVE, workout=1) == (300, "20:40")
|
||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "earned-time"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
description = "Penalty-then-reward registry: what every gate app earns in gaming time and shutdown time"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
|
||||
Loading…
Reference in New Issue
Block a user