Compare commits

..
Author SHA1 Message Date
Tirsvad c054bfc3a7 Resolve pending commit links 2026-10-28 17:30:35 +08:00
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
Tirsvad 2a293be0d8 Merge pull request 'Plan MIL-006: purchase strategy improvement' (#34) from mil-006-plan into main
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 7s
2026-10-28 10:12:20 +01:00
32 changed files with 672 additions and 131 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ The bot follows the five steps of the lecture. Every step is one function with t
![Step 5: the bakery after five minutes](docs/images/step-5-result.png)
In the run that made these screenshots the bot reached **16.6 cookies per second** after 300 seconds. Your number will differ: it depends on the speed of your machine and the network.
In the run that made these screenshots the bot reached **29 cookies per second** after 300 seconds (the first release reached 16.6). Your number will differ: it depends on the speed of your machine and the network.
### Parts of the repository
+1 -1
View File
@@ -17,7 +17,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| MIL | Milestone / Gateway | docs/milestones/mil-*.md | 007 |
| ADR | Architecture Decision Record | docs/adr/adr-*.md | 0002 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 016 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 018 |
## Languages
+221 -91
View File
@@ -1,123 +1,253 @@
INFO:__main__:Read 15 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 18 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:6 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 100 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:11 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 115 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:16 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 133 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:23 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:2 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 20 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:3 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:5 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 23 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:6 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 27 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:8 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 31 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:28 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:9 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:11 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 35 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:12 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:14 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 40 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:33 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:15 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:17 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 46 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:18 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:20 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:21 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 53 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:39 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:23 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:25 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:26 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 61 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:45 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 153 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:51 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:28 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:30 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:31 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 70 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:56 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 175 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:62 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:33 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:34 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:36 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 81 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:67 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:37 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:38 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:40 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:41 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:43 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.store:Bought upgrade 0
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:44 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:46 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 93 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:72 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:47 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:49 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:50 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 107 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:78 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:51 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:52 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 100 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:54 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:55 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:56 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 123 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:83 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:58 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:59 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 115 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:60 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:62 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:63 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 141 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:89 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:64 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:65 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:67 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 133 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:68 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:69 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:71 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 153 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:72 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:74 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:75 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:77 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 175 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:78 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:79 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:81 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 162 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:95 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:101 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:82 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:83 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:85 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:86 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 202 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:107 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 186 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:112 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:117 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:87 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:88 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:90 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:91 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 232 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:123 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:92 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:93 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:95 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:96 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 186 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:97 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:99 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:100 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:101 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 214 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:129 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:134 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:103 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:104 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:105 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:106 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:108 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 267 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:140 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:146 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 306 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:151 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:157 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:109 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:111 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:112 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:113 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 246 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:162 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:167 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 352 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:172 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:178 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:114 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:116 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:118 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:120 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:122 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 306 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:123 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:124 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:126 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:127 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:128 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 283 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:184 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:190 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:129 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:131 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:132 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:133 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:135 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:136 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 352 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:137 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:139 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:140 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:141 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:143 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:144 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 325 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:195 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:201 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:146 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:148 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:150 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:152 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:153 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:155 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 405 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:207 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:213 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:157 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:158 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:160 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:161 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:163 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:165 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 374 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:218 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:223 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:229 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 466 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:234 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:240 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:167 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:168 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:170 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:172 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:174 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:175 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 430 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 0 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:246 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:251 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:256 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:177 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:179 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:180 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:182 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:183 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:184 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:186 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 466 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:187 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:188 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:190 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:191 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:192 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:193 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:195 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:196 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.store:Bought upgrade 1
INFO:cookie_clicker_bot.application.use_cases.run_bot:197 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:199 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:201 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:202 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:204 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 536 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:262 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:268 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:273 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:205 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:207 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:208 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:209 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:210 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 494 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:278 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:283 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:289 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:212 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:213 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:214 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:216 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:217 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 616 cookies
WARNING:cookie_clicker_bot.application.use_cases.run_bot:Purchase of product 1 did not raise the owned amount
INFO:cookie_clicker_bot.application.use_cases.run_bot:219 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:220 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:222 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:223 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:224 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 568 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:226 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:229 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:231 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:233 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:234 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 654 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:236 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:238 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:239 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:241 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:242 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:243 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 751 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:245 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:247 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:248 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:250 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:251 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:253 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 708 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:254 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:257 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:259 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:261 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:263 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:265 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 814 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:267 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:269 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:271 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:273 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:275 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:277 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:279 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 1 for 936 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:281 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:282 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:284 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:285 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:286 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:288 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:289 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:291 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Bought product 0 for 864 cookies
INFO:cookie_clicker_bot.application.use_cases.run_bot:292 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:294 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:299 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Run finished at 16.6 cookies per second
Result : 16.6
exit 0
INFO:cookie_clicker_bot.application.use_cases.run_bot:295 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:297 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:298 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:300 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:302 of 300 seconds played
INFO:cookie_clicker_bot.application.use_cases.run_bot:Run finished at 29 cookies per second
Result : 29
Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

+5 -3
View File
@@ -11,7 +11,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version (reviewed in RC-014) | [638730d] |
| 2026-10-28 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version (reviewed in RC-014) | [638730d] |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Task 1 changed after the first measurement run: one product per store visit instead of waiting for a price change (reviewed in RC-016) | [375cc0b] |
---
@@ -21,7 +22,7 @@ Decide whether the bot reaches a higher cookies-per-second (CPS) rate than the 1
## Deliverable
The functions `buy_upgrade`, `get_store_items` and `run_bot` changed so that the bot waits for the store to redraw after a purchase, visits the store every second and also buys affordable upgrades; a Docker run of 300 seconds with its CPS, log and refreshed screenshot of step 5; the README and the wiki pages in English, Danish and Bahasa Malaysia updated with the new result and what it taught.
The functions `buy_upgrade`, `get_store_items` and `run_bot` changed so that the bot buys at most one product per store visit and therefore always reads the store after the game has redrawn it, visits the store every second and also buys affordable upgrades; a Docker run of 300 seconds with its CPS, log and refreshed screenshot of step 5; the README and the wiki pages in English, Danish and Bahasa Malaysia updated with the new result and what it taught.
## Go / No-Go Criteria
@@ -60,7 +61,7 @@ The functions `buy_upgrade`, `get_store_items` and `run_bot` changed so that the
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Wait for the store redraw after a purchase | After a successful purchase the store flags and prices are stale until the game redraws, so the next choice clicks a product that is no longer affordable. Wait until the price text of the bought product changes before the store is read again; the log of MIL-004 shows the failures follow a purchase. | No | |
| 1 | Wait for the store redraw after a purchase | After a successful purchase the store flags and prices are stale until the game redraws, so the next choice clicks a product that is no longer affordable. The game updates the price and owned text at once but the affordable flag only at its next redraw, so the price text is not a redraw signal (found in the first measurement run). Buy at most one product per store visit, so the next read comes a second later, after the redraw; the log of MIL-004 shows the failures follow a purchase. | No | |
| 2 | Visit the store every second | The store is visited every 5 seconds and the loop stops buying at the first failure, so cookies wait idle. Shorten `STORE_CHECK_INTERVAL_SECONDS` and keep buying while something is affordable; measure that clicks per second do not drop. | No | |
| 3 | Buy affordable upgrades | Upgrades such as the cursor and grandma boosts raise the CPS cheaply. Read the enabled upgrade crates in the `upgrades` area and click the cheapest first, behind the existing port with a fake in the tests. | No | |
| 4 | Measure the new strategy in Docker | Run the 300 second headless Docker run again, compare the CPS and the failure rate with the first release (16.6, about 40 percent) and refresh the screenshot of step 5. Environment: Docker container. | No | |
@@ -70,3 +71,4 @@ The functions `buy_upgrade`, `get_store_items` and `run_bot` changed so that the
[BC-001]: ../business-case.md
[638730d]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/638730d95ba24ba77c6448730d7999aa3e90d863
[375cc0b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/375cc0b5f3919b00daeb2f8f5fcc81de25acfc4c
@@ -15,7 +15,7 @@
## Artifact Under Review
- Instance reviewed: MIL-006 ([MIL-006])
- Instance reviewed: [MIL-006]
- Checklist used: [QC-MIL-001]
- Scope: full review
- Language and domain: en / it (the artifact's Metadata rows)
@@ -0,0 +1,66 @@
# SQA Review Record
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-016 |
| CrossReference | [MIL-006], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [375cc0b] |
---
## Artifact Under Review
- Instance reviewed: [MIL-006] version 2 (task 1 changed after the first measurement)
- Checklist used: [QC-MIL-001]
- Scope: full review
- Language and domain: en / it (the artifact's Metadata rows)
- Language reviewer: S01 (confirmed criteria 3, 5 and 9 of QC-LANG-001 in chat on 2026-10-28)
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Deliverable names the changed functions, the one-product-per-visit behaviour, a Docker run with CPS and log, and the updated README and wiki. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Five checkable Go criteria; measured in the run: 1 failed purchase of 49 (about 2 percent, below 5), CPS 29 (above 16.6). |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Rechecked after the change of task 1; unchanged from RC-014. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Rechecked after the change of task 1; unchanged from RC-014. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Rechecked after the change of task 1; unchanged from RC-014. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Rechecked after the change of task 1; unchanged from RC-014. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata has Language en and Domain it. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | en is a BCP 47 code and it is in the registry domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Confirmed by S01 in chat on 2026-10-28 (named human reviewer). Content is English. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | Register: IT Executive English. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | Confirmed by S01 in chat on 2026-10-28 (named human reviewer). Ordinary IT terms; no dictionary exists or is needed. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Headings, keys, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists. |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | N-A | No change of language or domain. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Confirmed by S01 in chat on 2026-10-28 (named human reviewer). S01 is competent in English and in IT. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out at first use. |
## Overall Verdict
Go - All Mandatory criteria pass; the language criteria for the amended text were confirmed by S01 in chat. Stand-in review, not independent.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | - |
---
[MIL-006]: ../../milestones/mil-006-purchase-strategy.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[375cc0b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/375cc0b5f3919b00daeb2f8f5fcc81de25acfc4c
+62
View File
@@ -0,0 +1,62 @@
# SQA Review Record
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-017 |
| CrossReference | [MIL-006-SRC], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [375cc0b] |
---
## Artifact Under Review
- Instance reviewed: Source code of MIL-006 (store, run_bot, selenium_game, constants, tests) ([MIL-006-SRC])
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 7 | Files, locks and connections are managed with context managers | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 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 | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 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 | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 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 | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
| 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 | Checked in the diff of branch mil-006-purchase-strategy; 111 tests, ruff check, ruff format and mypy --strict pass; Selenium stays the only runtime dependency. |
## Overall Verdict
Go - All Mandatory criteria pass. Stand-in review, not independent.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | - |
---
[MIL-006-SRC]: ../../../src/cookie_clicker_bot
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[375cc0b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/375cc0b5f3919b00daeb2f8f5fcc81de25acfc4c
+5 -2
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added RC-013 (MIL-004 source review) | [9a9ea53] |
| 2026-10-28 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-006, RC-014 and RC-015 | [638730d] |
| 2026-10-28 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added RC-016 and RC-017 (MIL-006 version 2 and its source review) | [375cc0b] |
---
@@ -32,7 +32,7 @@ updated whenever an artifact instance is created or reviewed.
| [MIL-003] | MIL | en | it | [MIL-002] | [MIL-004] | [RC-006], [RC-012] |
| [MIL-004] | MIL | en | it | [MIL-003] | [MIL-005] | [RC-007], [RC-013] |
| [MIL-005] | MIL | en | it | [MIL-004] | [MIL-006] | [RC-008] |
| [MIL-006] | MIL | en | it | [MIL-005] | - | [RC-014] |
| [MIL-006] | MIL | en | it | [MIL-005] | - | [RC-014], [RC-016], [RC-017] |
| [ADR-0001] | ADR | - | - | [BC-001], [SA-001], [MIL-001] | - | [RC-009] |
## Concern Coverage
@@ -82,3 +82,6 @@ updated whenever an artifact instance is created or reviewed.
[RC-014]: ./reviews/rc-014-mil-006-purchase-strategy.md
[RC-015]: ./reviews/rc-015-pp-001-version-2.md
[638730d]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/638730d95ba24ba77c6448730d7999aa3e90d863
[RC-016]: ./reviews/rc-016-mil-006-version-2.md
[RC-017]: ./reviews/rc-017-mil-006-source.md
[375cc0b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory/commit/375cc0b5f3919b00daeb2f8f5fcc81de25acfc4c
+1 -1
View File
@@ -17,4 +17,4 @@ Denne wiki gennemgår de fem trin fra forelæsningen. Hver side har et skærmbil
| [Trin 5: kørslen på fem minutter](Step-5-Five-Minute-Run-da) | Se løkken, resultatet og Docker-kørslen |
| [Appendiks](Appendix-Documentation-da) | Finde projektets dokumenter |
Resultatet af kørslen på skærmbillederne: **16,6 cookies i sekundet** efter 300 sekunder.
Resultatet af kørslen på skærmbillederne: **29 cookies i sekundet** efter 300 sekunder.
+1 -1
View File
@@ -17,4 +17,4 @@ Wiki ini menerangkan lima langkah kuliah. Setiap halaman mempunyai tangkapan skr
| [Langkah 5: larian lima minit](Step-5-Five-Minute-Run-ms) | Melihat gelung, hasil dan larian Docker |
| [Lampiran](Appendix-Documentation-ms) | Mencari dokumen projek |
Hasil larian dalam tangkapan skrin: **16.6 kuki sesaat** selepas 300 saat.
Hasil larian dalam tangkapan skrin: **29 kuki sesaat** selepas 300 saat.
+1 -1
View File
@@ -17,4 +17,4 @@ This wiki walks through the five lecture steps. Each page has a screenshot taken
| [Step 5: the five-minute run](Step-5-Five-Minute-Run) | See the loop, the result and the Docker run |
| [Appendix](Appendix-Documentation) | Find the project documents |
The result of the run shown in the screenshots: **16.6 cookies per second** after 300 seconds.
The result of the run shown in the screenshots: **29 cookies per second** after 300 seconds.
+1 -1
View File
@@ -22,7 +22,7 @@ def choose_upgrade(items: Sequence[StoreItem]) -> StoreItem | None:
- **Hold beslutningen ren.** `choose_upgrade` tager almindelige `StoreItem`-værdier og returnerer én, så den testes uden noget spil (lige priser, intet overkommeligt, tom liste).
- **Bekræft resultatet, antag det ikke.** `buy_upgrade` tjekker, at det ejede antal (`productOwned<N>`) steg. At tjekke antallet af cookies først var forkert: antallet stiger, mens botten klikker.
- **Siden halter bagefter.** Den rigtige kørsel viste, at cirka 40 procent af købsforsøgene mislykkes, fordi butikkens markeringer læses lige før spillet tegner om. Botten logger det og prøver igen ved næste butiksbesøg.
- **Siden halter bagefter.** Den første version tabte cirka 40 procent af sine køb: spillet opdaterer pris og ejet antal med det samme, men markeringen af, om der er råd, først ved næste gentegning, så et andet køb i samme besøg brugte en forældet markering. Højst ét produkt pr. butiksbesøg gav kun 1 fejl ud af 49.
- **Det rigtige spil finder det, falske ikke kan.** Genforsøget ved forældede elementer og tjekket af ejet antal kom begge fra kørsler i Docker.
Næste: [Trin 5: kørslen på fem minutter](Step-5-Five-Minute-Run-da)
+1 -1
View File
@@ -22,7 +22,7 @@ def choose_upgrade(items: Sequence[StoreItem]) -> StoreItem | None:
- **Pastikan keputusan bersih.** `choose_upgrade` menerima nilai `StoreItem` biasa dan memulangkan satu, jadi ia diuji tanpa permainan langsung (harga sama, tiada yang mampu dibeli, senarai kosong).
- **Sahkan hasilnya, jangan andaikan.** `buy_upgrade` menyemak bahawa jumlah dimiliki (`productOwned<N>`) bertambah. Menyemak bilangan kuki dahulu adalah salah: bilangan terus meningkat semasa bot mengklik.
- **Halaman ketinggalan.** Larian sebenar menunjukkan kira-kira 40 peratus percubaan pembelian gagal kerana bendera kedai dibaca sebelum permainan melukis semula. Bot mencatatkannya dan mencuba lagi pada lawatan kedai seterusnya.
- **Halaman ketinggalan.** Keluaran pertama kehilangan kira-kira 40 peratus pembeliannya: permainan mengemas kini teks harga dan jumlah dimiliki serta-merta tetapi bendera mampu beli hanya pada lukisan semula seterusnya, jadi pembelian kedua dalam lawatan yang sama menggunakan bendera usang. Membeli paling banyak satu produk setiap lawatan mengurangkan kegagalan kepada 1 daripada 49.
- **Permainan sebenar menemui apa yang tidak dapat ditemui oleh yang palsu.** Percubaan semula elemen usang dan semakan jumlah dimiliki kedua-duanya datang daripada larian dalam Docker.
Seterusnya: [Langkah 5: larian lima minit](Step-5-Five-Minute-Run-ms)
+1 -1
View File
@@ -22,7 +22,7 @@ def choose_upgrade(items: Sequence[StoreItem]) -> StoreItem | None:
- **Keep the decision pure.** `choose_upgrade` takes plain `StoreItem` values and returns one, so it is tested with no game at all (ties, none affordable, empty list).
- **Confirm the result, do not assume it.** `buy_upgrade` checks that the owned amount (`productOwned<N>`) went up. Checking the cookie count first was wrong: the count keeps rising while the bot clicks.
- **The page lags behind.** The live run showed that about 40 percent of the purchase attempts fail because the store flags are read just before the game redraws. The bot logs it and tries again at the next store check.
- **The page lags behind.** The first release lost about 40 percent of its purchases: 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 store visit cut the failures to 1 of 49.
- **The real game finds what fakes cannot.** The stale-element retry and the owned-amount check both came from running in Docker.
Next: [Step 5: the five-minute run](Step-5-Five-Minute-Run)
+2 -2
View File
@@ -2,7 +2,7 @@
🌐 [English](Step-5-Five-Minute-Run) · **Dansk** · [Bahasa Melayu](Step-5-Five-Minute-Run-ms)
`run_bot` spiller i 300 sekunder i én løkke: en byge på 20 klik, og hvert 5. sekund et besøg i butikken. Til sidst læser `get_cps` cookies i sekundet. Kørslen på skærmbilledet nåede **16,6 cookies i sekundet** med 26 markører og 14 bedstemødre.
`run_bot` spiller i 300 sekunder i én løkke: en byge på 20 klik, og hvert sekund ét besøg i butikken: først opgraderingerne, så højst ét produkt. Til sidst læser `get_cps` cookies i sekundet. Kørslen på skærmbilledet nåede **29 cookies i sekundet** med 29 markører, 17 bedstemødre og 2 opgraderinger (den første version nåede 16,6).
**Skærmbillede**
@@ -24,4 +24,4 @@ while now - start < duration_seconds:
- **Én løkke, ingen tråde.** Den tidligere løsning brugte to tråde og en ekstra pakke. Én løkke er lettere at læse, kræver ingen ekstra afhængighed og er let at teste.
- **Injicér uret.** `run_bot` får et `Clock`, så et falsk ur tester en kørsel på 300 sekunder på millisekunder.
- **Kør GUI'en i Docker.** Headless Chromium i en container tager aldrig din mus eller dit tastatur, og skærmbillederne og loggen kommer ud som filer.
- **Videre derfra.** Køb efter pris pr. cookie i sekundet, køb opgraderingerne og klik på den gyldne cookie: hver er en ny funktion bag de samme porte.
- **Videre derfra.** Køb efter pris pr. cookie i sekundet og klik på den gyldne cookie: hver er en ny funktion bag de samme porte.
+2 -2
View File
@@ -2,7 +2,7 @@
🌐 [English](Step-5-Five-Minute-Run) · [Dansk](Step-5-Five-Minute-Run-da) · **Bahasa Melayu**
`run_bot` bermain selama 300 saat dalam satu gelung: satu rentetan 20 klik, kemudian setiap 5 saat satu lawatan ke kedai. Pada akhirnya `get_cps` membaca kuki sesaat. Larian dalam tangkapan skrin mencapai **16.6 kuki sesaat** dengan 26 kursor dan 14 nenek.
`run_bot` bermain selama 300 saat dalam satu gelung: satu rentetan 20 klik, kemudian setiap saat satu lawatan ke kedai: peti peningkatan dahulu, kemudian paling banyak satu produk. Pada akhirnya `get_cps` membaca kuki sesaat. Larian dalam tangkapan skrin mencapai **29 kuki sesaat** dengan 29 kursor, 17 nenek dan 2 peningkatan (keluaran pertama mencapai 16.6).
**Tangkapan skrin**
@@ -24,4 +24,4 @@ while now - start < duration_seconds:
- **Satu gelung, tiada thread.** Penyelesaian terdahulu menggunakan dua thread dan satu pakej tambahan. Satu gelung lebih mudah dibaca, tidak memerlukan kebergantungan tambahan dan mudah diuji.
- **Suntik jam.** `run_bot` menerima `Clock`, jadi jam palsu menguji larian 300 saat dalam milisaat.
- **Jalankan GUI dalam Docker.** Chromium tanpa kepala dalam bekas tidak pernah mengambil tetikus atau papan kekunci anda, dan tangkapan skrin serta log keluar sebagai fail.
- **Langkah seterusnya.** Beli mengikut kos setiap kuki sesaat, beli peningkatan dan klik kuki emas: setiap satu ialah fungsi baharu di belakang port yang sama.
- **Langkah seterusnya.** Beli mengikut kos setiap kuki sesaat dan klik kuki emas: setiap satu ialah fungsi baharu di belakang port yang sama.
+2 -2
View File
@@ -2,7 +2,7 @@
🌐 **English** · [Dansk](Step-5-Five-Minute-Run-da) · [Bahasa Melayu](Step-5-Five-Minute-Run-ms)
`run_bot` plays for 300 seconds in one loop: a burst of 20 clicks, then every 5 seconds a visit to the store. At the end `get_cps` reads the cookies per second. The run in the screenshot reached **16.6 cookies per second** with 26 cursors and 14 grandmas.
`run_bot` plays for 300 seconds in one loop: a burst of 20 clicks, then every second one visit to the store: upgrade crates first, then at most one product. At the end `get_cps` reads the cookies per second. The run in the screenshot reached **29 cookies per second** with 29 cursors, 17 grandmas and 2 upgrades (the first release reached 16.6).
**Screenshot**
@@ -24,4 +24,4 @@ while now - start < duration_seconds:
- **One loop, no threads.** The earlier solution used two threads and an extra package. One loop is easier to read, needs no extra dependency and is easy to test.
- **Inject the clock.** `run_bot` receives a `Clock`, so a fake clock tests a 300 second run in milliseconds.
- **Run the GUI in Docker.** Headless Chromium in a container never takes your mouse or keyboard, and the screenshots and the log come out as files.
- **Where to go next.** Buy by cost per cookie per second, buy the upgrade crates, and click the golden cookie: each is a new function behind the same ports.
- **Where to go next.** Buy by cost per cookie per second and click the golden cookie: each is a new function behind the same ports.
@@ -20,6 +20,13 @@ class GamePort(Protocol):
"""
...
def has_element(self, element_id: str) -> bool:
"""@brief Tell whether the element is on the page now.
@param element_id The HTML id of the element.
@return True when the page holds an element with the id.
"""
...
def click_element(self, element_id: str) -> None:
"""@brief Click the element the way a user does.
@param element_id The HTML id of the element.
@@ -9,6 +9,7 @@ from cookie_clicker_bot.application.ports.clock import Clock
from cookie_clicker_bot.application.ports.game_port import GamePort
from cookie_clicker_bot.application.use_cases.bot_core import click_cookie
from cookie_clicker_bot.application.use_cases.store import (
buy_available_upgrades,
buy_upgrade,
get_cps,
get_store_items,
@@ -23,18 +24,22 @@ from cookie_clicker_bot.domain.store_item import choose_upgrade
_logger = logging.getLogger(__name__)
def _buy_while_affordable(game: GamePort) -> int:
"""@brief Buy the best affordable product until none is left or a purchase fails."""
bought = 0
while (item := choose_upgrade(get_store_items(game))) is not None:
if not buy_upgrade(game, item):
_logger.warning(
"Purchase of product %d did not raise the owned amount", item.index
)
break
bought += 1
_logger.info("Bought product %d for %d cookies", item.index, item.price)
return bought
def _buy_best_product(game: GamePort) -> bool:
"""@brief Buy the best affordable product, at most one, from a fresh read.
The game updates the owned amount at once but the affordable flags only at
its next draw, so a second purchase in the same visit would read stale flags.
"""
item = choose_upgrade(get_store_items(game))
if item is None:
return False
if not buy_upgrade(game, item):
_logger.warning(
"Purchase of product %d did not raise the owned amount", item.index
)
return False
_logger.info("Bought product %d for %d cookies", item.index, item.price)
return True
def run_bot(
@@ -45,7 +50,8 @@ def run_bot(
"""@brief Click the cookie and buy upgrades until the time is up.
One loop: a burst of clicks, then, every store check interval, a visit to
the store that buys the most expensive affordable product until none is.
the store that buys the affordable upgrades, cheapest first, and then at
most one product, the most expensive affordable one.
@param game The game page to operate.
@param clock The clock that measures the run.
@@ -62,7 +68,8 @@ def run_bot(
click_cookie(game, CLICKS_PER_BURST)
now = clock.monotonic()
if now >= next_store_check:
_buy_while_affordable(game)
buy_available_upgrades(game)
_buy_best_product(game)
next_store_check = now + STORE_CHECK_INTERVAL_SECONDS
_logger.info("%.0f of %s seconds played", now - start, duration_seconds)
cps = get_cps(game)
@@ -2,6 +2,7 @@
@brief Lecture step 4: read the store, buy a product and read the cookies per second.
"""
import logging
from decimal import Decimal
from cookie_clicker_bot.application.ports.game_port import GamePort
@@ -10,12 +11,16 @@ from cookie_clicker_bot.constants import (
CPS_ID,
CPS_SEPARATOR,
ENABLED_CLASS,
FIRST_UPGRADE_ID,
OWNED_CHECK_ATTEMPTS,
PRODUCT_COUNT,
PRODUCT_ID_PREFIX,
PRODUCT_OWNED_ID_PREFIX,
PRODUCT_PRICE_ID_PREFIX,
REDRAW_CHECK_ATTEMPTS,
UNLOCKED_CLASS,
UPGRADE_BUY_LIMIT,
UPGRADE_IDENTITY_ATTRIBUTE,
)
from cookie_clicker_bot.domain.number_parsing import (
parse_game_decimal,
@@ -23,6 +28,8 @@ from cookie_clicker_bot.domain.number_parsing import (
)
from cookie_clicker_bot.domain.store_item import StoreItem
_logger = logging.getLogger(__name__)
def get_store_items(game: GamePort) -> list[StoreItem]:
"""@brief Read the products that are visible in the store.
@@ -66,6 +73,48 @@ def buy_upgrade(game: GamePort, item: StoreItem) -> bool:
)
def _first_upgrade_is_affordable(game: GamePort) -> bool:
if not game.has_element(FIRST_UPGRADE_ID):
return False
return (
ENABLED_CLASS in game.read_attribute(FIRST_UPGRADE_ID, CLASS_ATTRIBUTE).split()
)
def _upgrade_was_replaced(game: GamePort, old_identity: str) -> bool:
"""@brief Wait until the first crate shows another upgrade or is gone."""
for _ in range(REDRAW_CHECK_ATTEMPTS):
if not game.has_element(FIRST_UPGRADE_ID):
return True
identity = game.read_attribute(FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)
if identity != old_identity:
return True
return False
def buy_available_upgrades(game: GamePort) -> int:
"""@brief Buy the upgrade crates the player can afford, cheapest first.
The crates are numbered by price and numbered again after each purchase, so
the cheapest one is always the first. The loop stops at the first crate that
is missing or unaffordable, at UPGRADE_BUY_LIMIT, or when a click leaves the
crate unchanged.
@param game The game page to operate.
@return How many upgrades were bought.
"""
bought = 0
while bought < UPGRADE_BUY_LIMIT and _first_upgrade_is_affordable(game):
identity = game.read_attribute(FIRST_UPGRADE_ID, UPGRADE_IDENTITY_ATTRIBUTE)
game.click_element(FIRST_UPGRADE_ID)
if not _upgrade_was_replaced(game, identity):
_logger.warning("Upgrade %s stayed in the store after the click", identity)
break
bought += 1
_logger.info("Bought upgrade %s", identity)
return bought
def get_cps(game: GamePort) -> Decimal:
"""@brief Read the cookies per second the bakery makes now.
@param game The game page to read.
+14 -1
View File
@@ -6,7 +6,7 @@
RUN_DURATION_SECONDS: int = 300
## Seconds between two looks at the store while the bot keeps clicking.
STORE_CHECK_INTERVAL_SECONDS: int = 5
STORE_CHECK_INTERVAL_SECONDS: int = 1
## Seconds to wait for an element before the bot gives up.
ELEMENT_TIMEOUT_SECONDS: int = 20
@@ -44,6 +44,19 @@ PRODUCT_OWNED_ID_PREFIX: str = "productOwned"
## Reads of the owned amount after a purchase click, as the game redraws late.
OWNED_CHECK_ATTEMPTS: int = 20
## Reads of a text after a purchase while the bot waits for the store to redraw.
REDRAW_CHECK_ATTEMPTS: int = 20
## Element id of the cheapest upgrade crate; the game numbers the crates by price
## and numbers them again after each purchase.
FIRST_UPGRADE_ID: str = "upgrade0"
## Attribute of a crate that holds the id of the upgrade it shows.
UPGRADE_IDENTITY_ATTRIBUTE: str = "data-id"
## Most upgrades the bot buys in one visit to the store.
UPGRADE_BUY_LIMIT: int = 50
## Times an action on the page is repeated after a stale or blocked element.
CLICK_RETRIES: int = 3
@@ -94,6 +94,15 @@ class SeleniumGame:
"""
_retry_on_page_change(lambda: self._click_once(element_id))
def has_element(self, element_id: str) -> bool:
"""@brief Tell whether the element is on the page now.
@param element_id The HTML id of the element.
@return True when the page holds an element with the id.
"""
return _retry_on_page_change(
lambda: bool(self._driver.find_elements(By.ID, element_id))
)
def _click_once(self, element_id: str) -> None:
element = WebDriverWait(self._driver, ELEMENT_TIMEOUT_SECONDS).until(
expected_conditions.element_to_be_clickable((By.ID, element_id))
+20
View File
@@ -76,6 +76,12 @@ class FakeDriver:
self.screenshots.append(filename)
return True
def find_elements(self, by: str, value: str) -> list[FakeElement]:
self.lookups.append((by, value))
if self.lookup_failures:
raise self.lookup_failures.pop(0)
return [self.elements[value]] if value in self.elements else []
def quit(self) -> None:
self.quit_calls += 1
@@ -186,6 +192,20 @@ def test_read_attribute_raises_after_the_retry_limit() -> None:
make_game(driver).read_attribute("product0", "class")
def test_has_element_tells_whether_the_id_is_on_the_page() -> None:
game = make_game(FakeDriver({"upgrade0": FakeElement()}))
assert game.has_element("upgrade0") is True
assert game.has_element("upgrade1") is False
def test_has_element_retries_after_a_stale_element() -> None:
driver = FakeDriver({"upgrade0": FakeElement()})
driver.lookup_failures = [StaleElementReferenceException()] * CLICK_RETRIES
assert make_game(driver).has_element("upgrade0") is True
def test_save_screenshot_creates_the_folder_and_writes_the_file(
tmp_path: Path,
) -> None:
+18 -1
View File
@@ -14,13 +14,19 @@ class FakeGame:
attributes: dict[tuple[str, str], str] | None = None,
on_click: Callable[[str], None] | None = None,
text_sequences: dict[str, list[str]] | None = None,
attribute_sequences: dict[tuple[str, str], list[str]] | None = None,
) -> None:
self.texts: dict[str, str] = texts or {}
self.attributes: dict[tuple[str, str], str] = attributes or {}
self.on_click = on_click
## Texts returned one per read, before the plain text applies.
self.text_sequences: dict[str, list[str]] = text_sequences or {}
## Attribute values returned one per read, before the plain value applies.
self.attribute_sequences: dict[tuple[str, str], list[str]] = (
attribute_sequences or {}
)
self.reads: dict[str, int] = {}
self.attribute_reads: dict[tuple[str, str], int] = {}
self.opened: list[str] = []
self.waited: list[str] = []
self.clicked: list[str] = []
@@ -32,6 +38,12 @@ class FakeGame:
def wait_for_element(self, element_id: str) -> None:
self.waited.append(element_id)
def has_element(self, element_id: str) -> bool:
"""An element exists when a text or an attribute is defined for it."""
return element_id in self.texts or any(
known == element_id for known, _ in self.attributes
)
def click_element(self, element_id: str) -> None:
self.clicked.append(element_id)
if self.on_click is not None:
@@ -45,7 +57,12 @@ class FakeGame:
return self.texts[element_id]
def read_attribute(self, element_id: str, attribute: str) -> str:
return self.attributes.get((element_id, attribute), "")
key = (element_id, attribute)
self.attribute_reads[key] = self.attribute_reads.get(key, 0) + 1
queued = self.attribute_sequences.get(key)
if queued:
return queued.pop(0)
return self.attributes.get(key, "")
def close(self) -> None:
self.close_calls += 1
+4
View File
@@ -9,5 +9,9 @@ def test_run_duration_is_five_minutes() -> None:
assert constants.RUN_DURATION_SECONDS == 300
def test_store_is_visited_every_second() -> None:
assert constants.STORE_CHECK_INTERVAL_SECONDS == 1
def test_store_check_happens_inside_the_run() -> None:
assert 0 < constants.STORE_CHECK_INTERVAL_SECONDS < constants.RUN_DURATION_SECONDS
+58 -3
View File
@@ -13,11 +13,13 @@ from cookie_clicker_bot.constants import (
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
@@ -66,7 +68,7 @@ def test_run_bot_stops_after_the_duration() -> None:
def test_run_bot_does_not_visit_the_store_before_the_interval() -> None:
game = make_game()
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS - 1)
run_bot(game, FakeClock(step=0.25), STORE_CHECK_INTERVAL_SECONDS * 0.75)
assert PRODUCT not in game.clicked
@@ -76,7 +78,60 @@ def test_run_bot_buys_at_the_interval() -> None:
assert game.clicked.count(PRODUCT) == 1
def test_run_bot_stops_buying_when_nothing_is_affordable() -> None:
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
@@ -85,7 +140,7 @@ def test_run_bot_stops_buying_when_nothing_is_affordable() -> None:
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 + 1)
run_bot(game, FakeClock(step=1.0), STORE_CHECK_INTERVAL_SECONDS)
assert game.clicked.count(PRODUCT) == 1
+98 -1
View File
@@ -7,6 +7,7 @@ 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,
@@ -15,10 +16,14 @@ 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
@@ -126,7 +131,10 @@ def test_buy_upgrade_rejects_a_change_one_attempt_too_late() -> None:
def test_buy_upgrade_ignores_the_cookie_count() -> None:
game = FakeGame(
texts={f"{PRODUCT_OWNED_ID_PREFIX}3": "", COOKIE_COUNTER_ID: "100 cookies"}
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
@@ -138,6 +146,95 @@ def test_buy_upgrade_propagates_an_unreadable_owned_amount() -> None:
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"),
[