Files
048-the-coockie-factory/tests/unit/test_run_bot.py
Tirsvad 375cc0b5f3 Buy one product per store visit and upgrades, visit the store every second
The game updates the price and owned text at once but the affordable
flag only at its next redraw, so a second purchase in the same visit
used a stale flag. Buying at most one product per visit cut the failed
purchases from about 40 percent to 1 of 49 and raised the CPS from 16.6
to 29 in the 300 second Docker run. README, screenshots and the wiki in
en, da and ms show the new result.

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Task: MIL-006#5
2026-10-28 17:30:09 +08:00

166 lines
5.4 KiB
Python

"""@file
@brief Tests of the run loop against a fake game and a fake clock.
"""
from decimal import Decimal
import pytest
from cookie_clicker_bot.application.use_cases.run_bot import run_bot
from cookie_clicker_bot.constants import (
BIG_COOKIE_ID,
CLASS_ATTRIBUTE,
CLICKS_PER_BURST,
COOKIE_COUNTER_ID,
CPS_ID,
FIRST_UPGRADE_ID,
PRODUCT_ID_PREFIX,
PRODUCT_OWNED_ID_PREFIX,
PRODUCT_PRICE_ID_PREFIX,
RUN_DURATION_SECONDS,
STORE_CHECK_INTERVAL_SECONDS,
UPGRADE_IDENTITY_ATTRIBUTE,
)
from cookie_clicker_bot.domain.errors import UnreadableNumberError
from tests.unit.fakes import FakeClock, FakeGame
PRODUCT = f"{PRODUCT_ID_PREFIX}0"
def make_game(cookies: int = 100, price: int = 15) -> FakeGame:
"""A store with one affordable product that is spent once, then unaffordable."""
game = FakeGame(
texts={
COOKIE_COUNTER_ID: f"{cookies} cookies",
f"{PRODUCT_PRICE_ID_PREFIX}0": str(price),
CPS_ID: "per second: 4.5",
f"{PRODUCT_OWNED_ID_PREFIX}0": "",
},
attributes={(PRODUCT, CLASS_ATTRIBUTE): "product unlocked enabled"},
)
def spend(element_id: str) -> None:
if element_id == PRODUCT:
game.texts[COOKIE_COUNTER_ID] = f"{cookies - price} cookies"
game.texts[f"{PRODUCT_OWNED_ID_PREFIX}0"] = "1"
game.attributes[(PRODUCT, CLASS_ATTRIBUTE)] = "product unlocked disabled"
game.on_click = spend
return game
def test_run_bot_returns_the_cps() -> None:
assert run_bot(make_game(), FakeClock(), 3) == Decimal("4.5")
def test_run_bot_clicks_in_bursts_until_the_duration_is_over() -> None:
game = FakeGame(texts={CPS_ID: "per second: 1"})
run_bot(game, FakeClock(step=1.0), 3)
assert game.clicked == [BIG_COOKIE_ID] * CLICKS_PER_BURST * 3
def test_run_bot_stops_after_the_duration() -> None:
clock = FakeClock(step=10.0)
game = FakeGame(texts={CPS_ID: "per second: 1"})
run_bot(game, clock, 25)
assert clock.now == 40.0
def test_run_bot_does_not_visit_the_store_before_the_interval() -> None:
game = make_game()
run_bot(game, FakeClock(step=0.25), STORE_CHECK_INTERVAL_SECONDS * 0.75)
assert PRODUCT not in game.clicked
def test_run_bot_buys_at_the_interval() -> None:
game = make_game()
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS + 2)
assert game.clicked.count(PRODUCT) == 1
def test_run_bot_buys_crates_before_products() -> None:
game = make_game()
game.attributes[(FIRST_UPGRADE_ID, CLASS_ATTRIBUTE)] = "crate upgrade enabled"
game.attributes[(FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)] = "7"
previous = game.on_click
def click(element_id: str) -> None:
if element_id == FIRST_UPGRADE_ID:
game.attributes[(FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)] = "8"
game.attributes[(FIRST_UPGRADE_ID, CLASS_ATTRIBUTE)] = "crate disabled"
elif previous is not None:
previous(element_id)
game.on_click = click
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS)
assert [c for c in game.clicked if c in (FIRST_UPGRADE_ID, PRODUCT)] == [
FIRST_UPGRADE_ID,
PRODUCT,
]
def test_run_bot_buys_one_product_per_visit_and_the_next_at_the_next_visit(
caplog: pytest.LogCaptureFixture,
) -> None:
game = FakeGame(
texts={
CPS_ID: "per second: 1",
f"{PRODUCT_PRICE_ID_PREFIX}0": "15",
f"{PRODUCT_PRICE_ID_PREFIX}1": "50",
f"{PRODUCT_OWNED_ID_PREFIX}0": "",
f"{PRODUCT_OWNED_ID_PREFIX}1": "",
},
attributes={
(f"{PRODUCT_ID_PREFIX}0", CLASS_ATTRIBUTE): "product unlocked enabled",
(f"{PRODUCT_ID_PREFIX}1", CLASS_ATTRIBUTE): "product unlocked enabled",
},
)
visits: list[int] = []
def click(element_id: str) -> None:
if element_id.startswith(PRODUCT_ID_PREFIX):
index = element_id.removeprefix(PRODUCT_ID_PREFIX)
visits.append(int(index))
game.texts[f"{PRODUCT_OWNED_ID_PREFIX}{index}"] = "1"
game.attributes[(element_id, CLASS_ATTRIBUTE)] = "product unlocked"
game.on_click = click
with caplog.at_level("WARNING"):
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS * 2)
assert visits == [1, 0]
assert caplog.records == []
def test_run_bot_buys_nothing_more_when_nothing_is_affordable() -> None:
game = make_game()
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS * 3)
assert game.clicked.count(PRODUCT) == 1
def test_run_bot_stops_buying_when_a_purchase_fails() -> None:
game = make_game()
game.on_click = None
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS)
assert game.clicked.count(PRODUCT) == 1
@pytest.mark.parametrize("duration", [0, -1])
def test_run_bot_rejects_a_non_positive_duration(duration: float) -> None:
game = FakeGame()
with pytest.raises(ValueError, match="positive"):
run_bot(game, FakeClock(), duration)
assert game.clicked == []
def test_run_bot_propagates_an_unreadable_cps() -> None:
game = FakeGame(texts={CPS_ID: "nothing"})
with pytest.raises(UnreadableNumberError):
run_bot(game, FakeClock(), 1)
def test_run_duration_default_is_the_constant() -> None:
clock = FakeClock(step=100.0)
game = FakeGame(texts={CPS_ID: "per second: 1"})
run_bot(game, clock)
assert clock.now == 100.0 * (RUN_DURATION_SECONDS // 100 + 1)