Files
048-the-coockie-factory/tests/unit/test_store.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

254 lines
8.3 KiB
Python

"""@file
@brief Tests of get_store_items, buy_upgrade and get_cps against a fake game.
"""
from decimal import Decimal
import pytest
from cookie_clicker_bot.application.use_cases.store import (
buy_available_upgrades,
buy_upgrade,
get_cps,
get_store_items,
)
from cookie_clicker_bot.constants import (
CLASS_ATTRIBUTE,
COOKIE_COUNTER_ID,
CPS_ID,
FIRST_UPGRADE_ID,
OWNED_CHECK_ATTEMPTS,
PRODUCT_ID_PREFIX,
PRODUCT_OWNED_ID_PREFIX,
PRODUCT_PRICE_ID_PREFIX,
REDRAW_CHECK_ATTEMPTS,
UPGRADE_BUY_LIMIT,
UPGRADE_IDENTITY_ATTRIBUTE,
)
from cookie_clicker_bot.domain.errors import UnreadableNumberError
from cookie_clicker_bot.domain.store_item import StoreItem
from tests.unit.fakes import FakeGame
def make_store(classes: dict[int, str], prices: dict[int, str]) -> FakeGame:
attributes = {
(f"{PRODUCT_ID_PREFIX}{i}", CLASS_ATTRIBUTE): c for i, c in classes.items()
}
texts = {f"{PRODUCT_PRICE_ID_PREFIX}{i}": p for i, p in prices.items()}
return FakeGame(texts=texts, attributes=attributes)
def test_get_store_items_skips_locked_products() -> None:
game = make_store(
{0: "product unlocked enabled", 1: "product locked disabled", 2: "product"},
{0: "15", 1: "100", 2: "1,100"},
)
assert get_store_items(game) == [StoreItem(0, 15, True)]
def test_get_store_items_reads_price_and_affordability() -> None:
game = make_store(
{
0: "product unlocked enabled",
1: "product unlocked disabled",
2: "product unlocked disabled",
},
{0: "15", 1: "1,100", 2: "1.5 million"},
)
assert get_store_items(game) == [
StoreItem(0, 15, True),
StoreItem(1, 1100, False),
StoreItem(2, 1_500_000, False),
]
def test_get_store_items_is_empty_when_nothing_is_unlocked() -> None:
assert get_store_items(FakeGame()) == []
def test_get_store_items_propagates_an_unreadable_price() -> None:
game = make_store({0: "product unlocked"}, {0: "???"})
with pytest.raises(UnreadableNumberError):
get_store_items(game)
def test_buy_upgrade_returns_true_when_the_owned_amount_grows() -> None:
game = FakeGame(texts={f"{PRODUCT_OWNED_ID_PREFIX}3": "2"})
def buy(element_id: str) -> None:
game.texts[f"{PRODUCT_OWNED_ID_PREFIX}3"] = "3"
game.on_click = buy
assert buy_upgrade(game, StoreItem(3, 15, True)) is True
assert game.clicked == [f"{PRODUCT_ID_PREFIX}3"]
def test_buy_upgrade_treats_an_empty_owned_text_as_zero() -> None:
game = FakeGame(texts={f"{PRODUCT_OWNED_ID_PREFIX}0": ""})
def buy(element_id: str) -> None:
game.texts[f"{PRODUCT_OWNED_ID_PREFIX}0"] = "1"
game.on_click = buy
assert buy_upgrade(game, StoreItem(0, 15, True)) is True
def test_buy_upgrade_returns_false_when_the_owned_amount_is_unchanged() -> None:
game = FakeGame(texts={f"{PRODUCT_OWNED_ID_PREFIX}3": "2"})
assert buy_upgrade(game, StoreItem(3, 15, True)) is False
assert game.clicked == [f"{PRODUCT_ID_PREFIX}3"]
def _owned_game(after: list[str]) -> FakeGame:
"""Owned text "2" before the click, then the given texts, one per read."""
owned = f"{PRODUCT_OWNED_ID_PREFIX}3"
return FakeGame(texts={owned: "2"}, text_sequences={owned: ["2", *after]})
def test_buy_upgrade_waits_for_a_late_redraw() -> None:
game = _owned_game(["2", "3"])
assert buy_upgrade(game, StoreItem(3, 15, True)) is True
assert game.reads[f"{PRODUCT_OWNED_ID_PREFIX}3"] == 3
assert game.clicked == [f"{PRODUCT_ID_PREFIX}3"]
def test_buy_upgrade_gives_up_after_the_attempt_limit() -> None:
game = _owned_game([])
assert buy_upgrade(game, StoreItem(3, 15, True)) is False
assert game.reads[f"{PRODUCT_OWNED_ID_PREFIX}3"] == 1 + OWNED_CHECK_ATTEMPTS
assert game.clicked == [f"{PRODUCT_ID_PREFIX}3"]
def test_buy_upgrade_accepts_a_change_on_the_last_attempt() -> None:
game = _owned_game(["2"] * (OWNED_CHECK_ATTEMPTS - 1) + ["3"])
assert buy_upgrade(game, StoreItem(3, 15, True)) is True
def test_buy_upgrade_rejects_a_change_one_attempt_too_late() -> None:
game = _owned_game(["2"] * OWNED_CHECK_ATTEMPTS + ["3"])
assert buy_upgrade(game, StoreItem(3, 15, True)) is False
def test_buy_upgrade_ignores_the_cookie_count() -> None:
game = FakeGame(
texts={
f"{PRODUCT_OWNED_ID_PREFIX}3": "",
COOKIE_COUNTER_ID: "100 cookies",
}
)
game.on_click = lambda _id: game.texts.update({COOKIE_COUNTER_ID: "90 cookies"})
assert buy_upgrade(game, StoreItem(3, 15, True)) is False
def test_buy_upgrade_propagates_an_unreadable_owned_amount() -> None:
game = FakeGame(texts={f"{PRODUCT_OWNED_ID_PREFIX}3": "???"})
with pytest.raises(UnreadableNumberError):
buy_upgrade(game, StoreItem(3, 15, True))
def _crate_game(identities: list[str], enabled: int) -> FakeGame:
"""Crates numbered from upgrade0; the first `enabled` crates are affordable."""
game = FakeGame()
crates = list(identities)
affordable_left = [enabled]
class_key = (FIRST_UPGRADE_ID, CLASS_ATTRIBUTE)
identity_key = (FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)
def render() -> None:
game.attributes.pop(class_key, None)
game.attributes.pop(identity_key, None)
if crates:
is_affordable = affordable_left[0] > 0
game.attributes[class_key] = (
"crate upgrade enabled" if is_affordable else "crate upgrade disabled"
)
game.attributes[identity_key] = crates[0]
def click(element_id: str) -> None:
if element_id == FIRST_UPGRADE_ID:
crates.pop(0)
affordable_left[0] -= 1
render()
game.on_click = click
render()
return game
def test_buy_available_upgrades_buys_nothing_without_crates() -> None:
game = FakeGame()
assert buy_available_upgrades(game) == 0
assert game.clicked == []
def test_buy_available_upgrades_buys_one_affordable_crate() -> None:
game = _crate_game(["7"], enabled=1)
assert buy_available_upgrades(game) == 1
assert game.clicked == [FIRST_UPGRADE_ID]
def test_buy_available_upgrades_buys_several_in_a_row() -> None:
game = _crate_game(["7", "8", "9", "10"], enabled=3)
assert buy_available_upgrades(game) == 3
assert game.clicked == [FIRST_UPGRADE_ID] * 3
def test_buy_available_upgrades_stops_at_a_disabled_first_crate() -> None:
game = _crate_game(["7", "8"], enabled=0)
assert buy_available_upgrades(game) == 0
assert game.clicked == []
def test_buy_available_upgrades_is_bounded_by_the_limit() -> None:
count = UPGRADE_BUY_LIMIT + 5
game = _crate_game([str(i) for i in range(count)], enabled=count)
assert buy_available_upgrades(game) == UPGRADE_BUY_LIMIT
assert game.clicked == [FIRST_UPGRADE_ID] * UPGRADE_BUY_LIMIT
def test_buy_available_upgrades_waits_for_the_redraw() -> None:
game = _crate_game(["7", "8"], enabled=2)
key = (FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)
inner = game.on_click
def click_then_lag(element_id: str) -> None:
assert inner is not None
inner(element_id)
if len(game.clicked) == 1:
game.attribute_sequences[key] = ["7", "7"]
game.on_click = click_then_lag
assert buy_available_upgrades(game) == 2
assert game.clicked == [FIRST_UPGRADE_ID] * 2
def test_buy_available_upgrades_stops_when_the_crate_never_changes() -> None:
game = FakeGame(
attributes={
(FIRST_UPGRADE_ID, CLASS_ATTRIBUTE): "crate upgrade enabled",
(FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE): "7",
}
)
assert buy_available_upgrades(game) == 0
assert game.clicked == [FIRST_UPGRADE_ID]
key = (FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)
assert game.attribute_reads[key] == 1 + REDRAW_CHECK_ATTEMPTS
@pytest.mark.parametrize(
("text", "expected"),
[
("per second: 0.1", Decimal("0.1")),
("per second: 1,234.5", Decimal("1234.5")),
("per second: 1.234 million", Decimal("1234000")),
("per second: 0", Decimal(0)),
],
)
def test_get_cps_reads_the_amount_after_the_colon(text: str, expected: Decimal) -> None:
assert get_cps(FakeGame(texts={CPS_ID: text})) == expected
def test_get_cps_rejects_a_line_without_amount() -> None:
with pytest.raises(UnreadableNumberError):
get_cps(FakeGame(texts={CPS_ID: "per second"}))