MIL-008: smarter purchase rule #49

Merged
Tirsvad merged 5 commits from mil-008-smarter-rule into main 2026-10-28 16:43:41 +01:00
15 changed files with 1976 additions and 1854 deletions
Showing only changes of commit 2f1c7ff00a - Show all commits
+1 -1
View File
@@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 009 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 020 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 021 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 |
+61
View File
@@ -0,0 +1,61 @@
# SQA Review Record
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-020 |
| CrossReference | [MIL-008], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Code review record | pending |
---
## Artifact Under Review
- Instance reviewed: [MIL-008]
- Checklist used: [QC-PY-001] (`QC-PY-001`)
- Scope: full review of the source and tests added by MIL-008: the best value rule `choose_best_value`, the `BEST_VALUE` strategy, the production read in the Selenium adapter, the `--best-value` option and 19 new tests; four 300 s container runs produced the comparison
- Language and domain: n/a
- Language reviewer: none
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S02`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | ruff (N rules) passes; names follow PEP 8 casing. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names use the terms of DICT-001 (cookie, upgrade, bot); no single-letter names outside comprehensions. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format` and `ruff check` pass with the rules in `pyproject.toml`; no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | `mypy --strict` passes; every signature is annotated. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare `except`; ruff B and S rules pass. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults; ruff B006 and A rules pass. |
| 7 | Files, locks and connections are managed with context managers | Pass | The browser is closed in a `finally` block in `main`. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions have Doxygen docstrings with `@brief`. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | Diagnostics use `logging`; the required result line `cookies/second: N` is the program's output, not a log; no secrets are read or logged. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No Design Class Diagram exists (ADR-0001 skips use case design); ADR-0002 records the layout instead. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 135 tests pass with fakes; 100% coverage of the logic modules; tests cover the new rule, the production read and its failure paths. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` passes; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `pyproject.toml` lists Selenium as the only runtime dependency; dev extras are test and lint tools. |
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | Constants live only in `constants.py`; rules are defined once. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `domain` imports no Selenium; `application` depends on a port; no import cycles. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Modules have one reason to change; the game is behind a narrow port. |
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Layers `domain`, `application`, `infrastructure` under `src/`, `unit` and `integration` under `tests/`; no `presentation` package by ADR-0002. |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | English names, snake_case files, no spaces. |
## Overall Verdict
Go — the code of MIL-008 meets every Mandatory criterion; `ruff`, `mypy --strict` and `pytest` were run and pass. The review is not independent: the AI wrote and reviewed it as stand-in for S02, and S01 can challenge it in the end-of-run summary.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | - |
---
[MIL-008]: ../../milestones/mil-008-smarter-rule.md
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
+2 -1
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Initial version: baseline artifacts | [cadc5ef] |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Add MIL-008 and RC-019 | [9a6796f] |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Add RC-020, the code review of MIL-008 | pending |
---
@@ -48,6 +48,7 @@ Tracks backward and forward links between artifact instances so that the Busines
| Source and tests of [MIL-004] | Python source code | - | - | [MIL-004], [ADR-0002] | - | RC-016 |
| Source and tests of [MIL-005] | Python source code | - | - | [MIL-005], [ADR-0002] | - | RC-017 |
| Source and tests of [MIL-006] | Python source code | - | - | [MIL-006], [ADR-0002] | - | RC-018 |
| Source and tests of [MIL-008] | Python source code | - | - | [MIL-008], [ADR-0002] | - | RC-020 |
## Concern Coverage
@@ -36,9 +36,29 @@ Det bedste resultat er intervallet på 10 sekunder med køb én. Hvorfor:
- At tjekke oftere er ikke bedre. Hvert tjek læser siden og tager tid fra klikningen, og det dyreste produkt, der er råd til, er som regel kun lidt bedre end det billigere, man kunne have købt et øjeblik før.
- Køb alle gør ingen forskel tidligt i spillet: der er kun råd til Cursor og Grandma, og det køber botten alligevel. Det ville få betydning senere, når antallet af cookies rummer flere produkter på én gang.
## En smartere regel: bedste værdi
`--best-value` ser ikke kun på prisen. Spillet fortæller, hvor mange cookies pr. sekund én enhed af hvert produkt giver, og botten køber det produkt, der er råd til, med den laveste pris pr. cookie pr. sekund (`choose_best_value`). Cursor koster 15 og giver 0,1 (150 cookies for hver cookie pr. sekund), Grandma koster 100 og giver 1 (100 for hver), så Grandma er det bedste køb, selv om den koster mere.
```bash
cookie-clicker-bot --best-value
```
Hver række er en fuld kørsel på 300 sekunder alene i en Docker-container, den ene efter den anden, tjek hvert 5. sekund:
| Kørsel | Strategi | cookies/sekund | Købt |
| --- | --- | --- | --- |
| 1 | dyreste, der er råd til | 18 | 30 Cursor, 15 Grandma |
| 2 | bedste værdi | 17.9 | 29 Cursor, 15 Grandma |
| 3 | dyreste, der er råd til | 18.8 | 28 Cursor, 16 Grandma |
| 4 | bedste værdi | 18.8 | 28 Cursor, 16 Grandma |
Den smartere regel er **ikke bedre**: de to regler adskiller sig mindre end to kørsler af den samme regel (18 og 18,8). Årsagen er den samme som for køb alle. På 5 sekunder klikker botten omkring 125 cookies, så der er kun råd til Cursor og Grandma, og begge regler køber næsten det samme. En regel, der ser på værdi, ville få betydning, når der er råd til flere produkter på én gang, for eksempel med et længere tjekinterval.
## Hvad vi fik ud af dette trin
- **Små, rene regler er nemme at sammenligne.** Fordi valget af opgradering ikke rører Selenium, er en ny strategi en ny funktion plus en test.
- **Mål, gæt ikke.** Det resultat, vi forventede (flere tjek, flere cookies), var det modsatte af, hvad kørslerne viste.
- **En anden rigtig fejl.** Det første hold styrtede ned i én container: spillet omskriver tælleren i hvert billede, og en computer under belastning fik vores ene genforsøg til at fejle. Læsningen forsøger nu fem gange med en kort pause. En test med en falsk driver, der bliver forældet fire gange, viser det.
- **Retfærdige sammenligninger kræver de samme forhold.** Vi kørte rækkerne samtidig og rapporterer to hold, fordi en enkelt kørsel kan være heldig.
- **En bedre idé skal stadig måles.** Reglen om bedste værdi er rigtig på papiret og gjorde ingen forskel i kørslerne, fordi botten aldrig har cookies nok til at vælge mellem mere end to produkter.
@@ -36,9 +36,29 @@ Keputusan terbaik ialah selang 10 saat dengan beli satu. Sebabnya:
- Menyemak lebih kerap tidak lebih baik. Setiap semakan membaca halaman dan mengambil masa daripada mengklik, dan produk termahal yang mampu dibeli biasanya hanya sedikit lebih baik daripada yang lebih murah yang boleh anda beli seketika sebelumnya.
- Beli semua tidak membuat perbezaan pada awal permainan: hanya Cursor dan Grandma yang mampu dibeli, dan bot membelinya juga. Ia akan penting kemudian, apabila bilangan kuki cukup untuk beberapa produk sekali gus.
## Peraturan yang lebih bijak: nilai terbaik
`--best-value` tidak melihat harga sahaja. Permainan memberitahu berapa kuki sesaat yang dihasilkan oleh satu unit setiap produk, dan bot membeli produk yang mampu dibeli dengan harga terendah bagi setiap kuki sesaat (`choose_best_value`). Cursor berharga 15 dan menghasilkan 0.1 (150 kuki bagi setiap kuki sesaat), Grandma berharga 100 dan menghasilkan 1 (100 bagi setiap satu), jadi Grandma ialah belian yang lebih baik walaupun lebih mahal.
```bash
cookie-clicker-bot --best-value
```
Setiap baris ialah larian penuh 300 saat bersendirian dalam bekas Docker, satu demi satu, semak setiap 5 saat:
| Larian | Strategi | kuki/saat | Dibeli |
| --- | --- | --- | --- |
| 1 | termahal yang mampu dibeli | 18 | 30 Cursor, 15 Grandma |
| 2 | nilai terbaik | 17.9 | 29 Cursor, 15 Grandma |
| 3 | termahal yang mampu dibeli | 18.8 | 28 Cursor, 16 Grandma |
| 4 | nilai terbaik | 18.8 | 28 Cursor, 16 Grandma |
Peraturan yang lebih bijak ini **tidak lebih baik**: perbezaan antara kedua-dua peraturan lebih kecil daripada perbezaan antara dua larian peraturan yang sama (18 dan 18.8). Sebabnya sama seperti beli semua. Dalam 5 saat bot mengklik kira-kira 125 kuki, jadi hanya Cursor dan Grandma yang mampu dibeli, dan kedua-dua peraturan membeli hampir perkara yang sama. Peraturan yang melihat nilai akan penting apabila beberapa produk mampu dibeli sekali gus, misalnya dengan selang semakan yang lebih panjang.
## Apa yang kita peroleh dalam langkah ini
- **Peraturan kecil yang tulen mudah dibandingkan.** Oleh kerana pemilihan naik taraf tidak menyentuh Selenium, strategi baharu hanyalah fungsi baharu serta satu ujian.
- **Ukur, jangan meneka.** Keputusan yang kami jangkakan (lebih banyak semakan, lebih banyak kuki) bertentangan dengan apa yang ditunjukkan oleh larian.
- **Pepijat sebenar kedua.** Kelompok pertama ranap dalam satu bekas: permainan menulis semula pembilang pada setiap bingkai, dan komputer yang terbeban menyebabkan satu percubaan semula kami gagal. Bacaan kini mencuba semula lima kali dengan jeda singkat. Ujian dengan pemacu palsu yang menjadi basi empat kali menunjukkannya.
- **Perbandingan yang adil memerlukan keadaan yang sama.** Kami menjalankan baris-baris itu serentak dan melaporkan dua kelompok, kerana satu larian boleh bernasib baik.
- **Idea yang lebih baik masih perlu diukur.** Peraturan nilai terbaik betul di atas kertas dan tidak membuat perbezaan dalam larian, kerana bot tidak pernah mempunyai kuki yang cukup untuk memilih antara lebih daripada dua produk.
+20
View File
@@ -36,9 +36,29 @@ The best result is the 10 second interval with buy one. Why:
- Checking more often is not better. Every check reads the page and takes time away from clicking, and the most expensive affordable product is usually only a little better than the cheaper one you could have bought a moment before.
- Buy all makes no difference early in the game: only Cursor and Grandma are affordable, and the bot buys them anyway. It would matter later, when the cookie count holds several products at once.
## A smarter rule: best value
`--best-value` does not look at the price alone. The game tells how many cookies per second one unit of each product makes, and the bot buys the affordable product with the lowest price per cookie per second (`choose_best_value`). Cursor costs 15 and makes 0.1 (150 cookies for each cookie per second), Grandma costs 100 and makes 1 (100 for each), so Grandma is the better deal even though it costs more.
```bash
cookie-clicker-bot --best-value
```
Each row is a full 300 second run alone in a Docker container, one after the other, check every 5 seconds:
| Run | Strategy | cookies/second | Bought |
| --- | --- | --- | --- |
| 1 | most expensive affordable | 18 | 30 Cursor, 15 Grandma |
| 2 | best value | 17.9 | 29 Cursor, 15 Grandma |
| 3 | most expensive affordable | 18.8 | 28 Cursor, 16 Grandma |
| 4 | best value | 18.8 | 28 Cursor, 16 Grandma |
The smarter rule is **not better**: the two rules differ by less than the difference between two runs of the same rule (18 and 18.8). The reason is the same as for buy all. In 5 seconds the bot clicks about 125 cookies, so only Cursor and Grandma are ever affordable, and both rules buy nearly the same. A rule that looks at value would matter once several products are affordable at once, for example with a longer check interval or after the first minutes of luck.
## What we gained in this step
- **Small, pure rules are easy to compare.** Because the choice of upgrade does not touch Selenium, a new strategy is a new function plus a test.
- **Measure, do not guess.** The result we expected (more checks, more cookies) was the opposite of what the runs showed.
- **A second real bug.** The first batch crashed in one container: the game rewrites the counter every frame, and a computer under load made our single retry fail. The read now retries five times with a short pause. A test with a fake driver that goes stale four times shows it.
- **Fair comparisons need the same conditions.** We ran the rows at the same time and report two batches, because one run can be lucky.
- **A better idea still has to be measured.** The best value rule is correct on paper and made no difference in the runs, because the bot never has enough cookies to choose between more than two products.