Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835cb54d00 | ||
|
|
e7324ca6d6 | ||
|
|
086b9f1582 | ||
|
|
2f1c7ff00a | ||
|
|
ecbe0d2671 | ||
|
|
014d9ad3d9 | ||
|
|
9a6796f775 | ||
|
|
8865608459 | ||
|
|
2b654dbb4d | ||
|
|
edb97de543 | ||
|
|
f2b81f44c5 | ||
|
|
1bfa8f6c4d |
@@ -23,6 +23,18 @@ The bot follows the five steps of the lecture:
|
||||
4. After 5 minutes stop and print `cookies/second`.
|
||||
5. Tweak the strategy (check interval, buy one or buy all affordable).
|
||||
|
||||
### The five steps in pictures
|
||||
|
||||
| Step | Screenshot |
|
||||
| --- | --- |
|
||||
| 1. Get to know the game |  |
|
||||
| 2. Click the cookie |  |
|
||||
| 3. Buy the most expensive affordable upgrade |  |
|
||||
| 4. Stop after 5 minutes, print cookies/second |  |
|
||||
| 5. Tweak the strategy |  |
|
||||
|
||||
What each step taught is explained in the [wiki][documentation], in English, Danish and Bahasa Malaysia (the same pages are in `docs/wiki/`).
|
||||
|
||||
The repository is organised like this:
|
||||
|
||||
| Folder or file | What it is for |
|
||||
@@ -83,11 +95,24 @@ or, without installing the command:
|
||||
python -m cookie_clicker_bot
|
||||
```
|
||||
|
||||
To keep your desktop free, run it in the Docker container (headless Chromium):
|
||||
The bot plays for 5 minutes, then prints one line on standard output, for example `cookies/second: 18.9`. What it did (clicks, purchases, time) is logged on standard error. Options:
|
||||
|
||||
| Option | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| `--duration SECONDS` | How long to play | 300 |
|
||||
| `--interval SECONDS` | Time between upgrade checks (greater than 0) | 5 |
|
||||
| `--buy-all` | Buy every affordable upgrade, most expensive first, instead of one | off |
|
||||
| `--best-value` | Buy the affordable upgrade with the lowest price per cookie per second (not together with `--buy-all`) | off |
|
||||
| `--headless` | Run Chromium without a window | off (on in Docker) |
|
||||
| `--screenshot PATH` | Save a screenshot when the run ends | none |
|
||||
| `--screenshot-start PATH` | Save a screenshot before the first click | none |
|
||||
|
||||
To keep your desktop free, run it in the Docker container (headless Chromium). Options go after the image name:
|
||||
|
||||
```bash
|
||||
docker build -t cookie-clicker-bot .
|
||||
docker run --rm cookie-clicker-bot
|
||||
docker run --rm cookie-clicker-bot --headless --interval 10
|
||||
```
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
@@ -14,9 +14,9 @@ document of a type. `Primary File` may contain a glob (e.g.
|
||||
| BC | Business Case | docs/business-case.md | 002 |
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 008 |
|
||||
| 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 | 019 |
|
||||
| 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 |
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# MIL-008 Smarter purchase rule
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-008 |
|
||||
| CrossReference | [BC-001] |
|
||||
| Language | en |
|
||||
| Domain | it |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Initial version | [9a6796f] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether a purchase rule that looks at what an upgrade earns, not only at its price, beats the assignment's rule ("most expensive affordable", 18.9 cookies/second alone on the computer) in a measured container run.
|
||||
|
||||
## Deliverable
|
||||
|
||||
A "best value" strategy next to buy-one and buy-all (the lowest price per cookie per second among the affordable upgrades), selectable from the command line, unit tested, with a recorded comparison and a wiki note for each language. The defaults stay the assignment's behaviour.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The new strategy is selectable from the command line and the defaults are unchanged (5 seconds, buy one) | Defaults unchanged | Defaults changed |
|
||||
| 2 | The rule is a pure function in `domain/` with no Selenium import, and unit tests cover it | 0 failures, coverage of the logic modules at 80% or more | A test fails or coverage drops |
|
||||
| 3 | Baseline and new strategy are each run at least twice in the same container conditions and the table is recorded | Table with 4 or more rows | Fewer rows |
|
||||
| 4 | The result is stated as measured, also when the new rule is not better | Result named | Claim without a measurement |
|
||||
| 5 | The wiki records the result in English, Danish and Bahasa Malaysia | 3 languages | A language missing |
|
||||
| 6 | The code review against `qc-programming-python.md` is `Go` | `RC-*` is `Go` | `No-Go` |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends on | Reason |
|
||||
| --- | --- |
|
||||
| MIL-006 | Extends the strategy options and the comparison method |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| O4 | [BC-001] |
|
||||
| CN-003 | [SA-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S02 (AI as stand-in, autonomous mode, not independent) |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-11-06 — after the planned window of [PP-001]; the Business Case sets no deadline.
|
||||
|
||||
## Tasks
|
||||
|
||||
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Read the production of each upgrade | Add to the game port a read of how many cookies per second one unit of each visible upgrade makes (from the game's own data), and add it to `Upgrade`. Unit tested with a fake game. | No | O4 |
|
||||
| 2 | Add the best value rule | Add a pure function that picks the affordable upgrade with the lowest price per cookie per second, and a `BEST_VALUE` strategy. Unit tested, including ties and unknown production. | No | O4 |
|
||||
| 3 | Select the strategy from the command line | Add the option, keep buy-one as the default, and update the README option table. | No | O4 |
|
||||
| 4 | Compare with container runs | Run baseline and best value at least twice each in Docker under the same conditions, record cookies per second and name the better rule. | No | O4 |
|
||||
| 5 | Record the result in the wiki | Add the result and what was gained to Step 5 in English, Danish and Bahasa Malaysia. | No | O4 |
|
||||
|
||||
---
|
||||
|
||||
[BC-001]: ../business-case.md
|
||||
[PP-001]: ../project-plan.md
|
||||
[SA-001]: ../stakeholder-analysis.md
|
||||
[9a6796f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/9a6796f775680effe37a54f2609c3915140c8c0e
|
||||
+21
-11
@@ -12,12 +12,13 @@
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version: seven gateways; accepted by S01 in chat | [cadc5ef] |
|
||||
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | S01 | Add MIL-008 (smarter purchase rule); accepted by S01 in chat | [9a6796f] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This plan schedules seven gateways (`MIL-001` to `MIL-007`) that take the Cookie Clicker bot from an accepted baseline to a documented, tested and published repository. The Business Case sets no deadline, so the window 2026-10-28 to 2026-11-04 is a planning assumption of this plan. Each gateway ends in a decision and a deliverable and is delivered by one pull request on its own branch.
|
||||
This plan schedules eight gateways (`MIL-001` to `MIL-008`) that take the Cookie Clicker bot from an accepted baseline to a documented, tested and published repository. The Business Case sets no deadline, so the window 2026-10-28 to 2026-11-04 is a planning assumption of this plan. Each gateway ends in a decision and a deliverable and is delivered by one pull request on its own branch.
|
||||
|
||||
## Planning Assumptions
|
||||
|
||||
@@ -31,13 +32,14 @@ This plan schedules seven gateways (`MIL-001` to `MIL-007`) that take the Cookie
|
||||
|
||||
| Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Baseline and repository set-up | [MIL-001] | 2026-10-28 | 2026-10-28 | S01 | none (documents) | Accepted baseline, ADRs, repository description and topics | |
|
||||
| Project scaffold and tooling | [MIL-002] | 2026-10-28 to 2026-10-29 | 2026-10-29 | S01 | none (technical tasks) | `pyproject.toml`, layout, `.gitignore`, Doxyfile, README, Docker image | |
|
||||
| Lecture steps 1 and 2 | [MIL-003] | 2026-10-29 to 2026-10-30 | 2026-10-30 | S01 | none (technical tasks) | Open game, dismiss language, click cookie, screenshots 1 and 2 | |
|
||||
| Lecture step 3 | [MIL-004] | 2026-10-30 to 2026-10-31 | 2026-10-31 | S01 | none (technical tasks) | Parsing, selection rule, purchase every 5 s, screenshot 3 | |
|
||||
| Lecture step 4 | [MIL-005] | 2026-10-31 to 2026-11-01 | 2026-11-01 | S01 | none (technical tasks) | 5 minute run, cookies per second, screenshot 4 | |
|
||||
| Lecture step 5 | [MIL-006] | 2026-11-01 to 2026-11-02 | 2026-11-02 | S01 | none (technical tasks) | Strategy options, comparison, screenshot 5 | |
|
||||
| Documentation, wiki and release | [MIL-007] | 2026-11-02 to 2026-11-04 | 2026-11-04 | S01 | none (documentation) | README, Doxygen, wiki in en, da and ms, final checks | |
|
||||
| Baseline and repository set-up | [MIL-001] | 2026-10-28 | 2026-10-28 | S01 | none (documents) | Accepted baseline, ADRs, repository description and topics | [Milestone 1] |
|
||||
| Project scaffold and tooling | [MIL-002] | 2026-10-28 to 2026-10-29 | 2026-10-29 | S01 | none (technical tasks) | `pyproject.toml`, layout, `.gitignore`, Doxyfile, README, Docker image | [Milestone 2] |
|
||||
| Lecture steps 1 and 2 | [MIL-003] | 2026-10-29 to 2026-10-30 | 2026-10-30 | S01 | none (technical tasks) | Open game, dismiss language, click cookie, screenshots 1 and 2 | [Milestone 3] |
|
||||
| Lecture step 3 | [MIL-004] | 2026-10-30 to 2026-10-31 | 2026-10-31 | S01 | none (technical tasks) | Parsing, selection rule, purchase every 5 s, screenshot 3 | [Milestone 4] |
|
||||
| Lecture step 4 | [MIL-005] | 2026-10-31 to 2026-11-01 | 2026-11-01 | S01 | none (technical tasks) | 5 minute run, cookies per second, screenshot 4 | [Milestone 5] |
|
||||
| Lecture step 5 | [MIL-006] | 2026-11-01 to 2026-11-02 | 2026-11-02 | S01 | none (technical tasks) | Strategy options, comparison, screenshot 5 | [Milestone 6] |
|
||||
| Documentation, wiki and release | [MIL-007] | 2026-11-02 to 2026-11-04 | 2026-11-04 | S01 | none (documentation) | README, Doxygen, wiki in en, da and ms, final checks | [Milestone 7] |
|
||||
| Smarter purchase rule | [MIL-008] | 2026-11-05 to 2026-11-06 | 2026-11-06 | S01 | none (technical tasks) | Best value strategy, measured comparison, wiki note in en, da and ms | [Milestone 8] |
|
||||
|
||||
```plantuml
|
||||
@startgantt
|
||||
@@ -90,9 +92,7 @@ A No-Go on a gateway stops all later gateways until it is fixed and re-reviewed.
|
||||
|
||||
## Open Issues
|
||||
|
||||
- Target repository: the user names `https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code`, but `origin` is `Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new`. The plan uses `origin` until S01 states otherwise in chat.
|
||||
- The Business Case sets no deadline; the window 2026-10-28 to 2026-11-04 is this plan's own assumption.
|
||||
- No QC checklist exists for the Project Plan acceptance beyond `QC-PP-001`; S01's acceptance of the plan in chat accepts the row.
|
||||
- Resolved: S01 accepted the plan in chat on 2026-10-28; the target repository is `origin`, as S01 confirmed.
|
||||
|
||||
---
|
||||
|
||||
@@ -105,4 +105,14 @@ A No-Go on a gateway stops all later gateways until it is fixed and re-reviewed.
|
||||
[MIL-005]: ./milestones/mil-005-timed-run.md
|
||||
[MIL-006]: ./milestones/mil-006-strategy.md
|
||||
[MIL-007]: ./milestones/mil-007-documentation.md
|
||||
[MIL-008]: ./milestones/mil-008-smarter-rule.md
|
||||
[cadc5ef]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/cadc5eff21d9afc09799d4bc2e3f6ac17ab2612a
|
||||
[Milestone 1]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/290
|
||||
[Milestone 2]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/291
|
||||
[Milestone 3]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/292
|
||||
[Milestone 4]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/293
|
||||
[Milestone 5]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/294
|
||||
[Milestone 6]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/295
|
||||
[Milestone 7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/296
|
||||
[Milestone 8]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/milestone/308
|
||||
[9a6796f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/9a6796f775680effe37a54f2609c3915140c8c0e
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# SQA Review Record
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-019 |
|
||||
| CrossReference | [MIL-008], [QC-MIL-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Review record, full review | [9a6796f] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [MIL-008]
|
||||
- Checklist used: [QC-MIL-001] (`QC-MIL-001`)
|
||||
- Scope: full review
|
||||
- Language and domain: en / it
|
||||
- Language reviewer: none
|
||||
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S02`)
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | A concrete deliverable is defined for every gate | Pass | Deliverable names the strategy, the comparison and the wiki note. |
|
||||
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Go/No-Go table has objectively checkable criteria with Go and No-Go columns. |
|
||||
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the preceding gateway. |
|
||||
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability maps to Business Case objectives and success criteria. |
|
||||
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership: owner S01, approving reviewer S02 (AI stand-in). |
|
||||
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies after the plan window in PP-001 (the plan is extended in this change); the Business Case sets no deadline. |
|
||||
|
||||
## 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 BCP 47; `it` is in the registry domain list. |
|
||||
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. |
|
||||
| 4 | The register matches the one the registry gives for the artifact type | Pass | Register of the type is IT Executive English (registry, Languages). |
|
||||
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | Terms follow DICT-001: bot, cookie, cookies per second, lecture step, upgrade. |
|
||||
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Keys, headings, IDs and statuses are English. |
|
||||
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists in `docs/`. |
|
||||
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | N-A | First version; no language change. |
|
||||
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The stand-in reviewer reads English and checked the IT terms in context. |
|
||||
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations (ADR, QC, ISO/IEC, S-IDs) are spelled out or defined where first used, or are the framework's own IDs. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — MIL-008 meets every Mandatory criterion. No blocking finding. The review is not independent: the AI authored 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-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||
[9a6796f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/9a6796f775680effe37a54f2609c3915140c8c0e
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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 | [2f1c7ff] |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
[2f1c7ff]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/2f1c7ff00a80bdcfa4ed4ff087227a3e4d4d591c
|
||||
@@ -9,7 +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 | [2f1c7ff] |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +30,7 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| [BC-001] | Business Case | en | it | - | [SA-001], [PP-001] | RC-001 |
|
||||
| [SA-001] | Stakeholder Analysis | en | it | [BC-001] | [PP-001] | RC-002 |
|
||||
| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001] to [MIL-007] | - (accepted by S01 in chat; no checklist) |
|
||||
| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001] to [MIL-008] | - (accepted by S01 in chat; no checklist) |
|
||||
| [DICT-001] | Domain Dictionary | en | it | [BC-001], [SA-001] | - | RC-003 |
|
||||
| [ADR-0001] | ADR | - | - | [BC-001] | [PP-001] | RC-004 |
|
||||
| [ADR-0002] | ADR | - | - | [BC-001], [SA-001] | code in MIL-002 onward | RC-005 |
|
||||
@@ -40,26 +41,28 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
| [MIL-004] | Milestone | en | it | [BC-001], [PP-001] | [MIL-005] | RC-010 |
|
||||
| [MIL-005] | Milestone | en | it | [BC-001], [PP-001] | [MIL-006] | RC-011 |
|
||||
| [MIL-006] | Milestone | en | it | [BC-001], [PP-001] | [MIL-007] | RC-012 |
|
||||
| [MIL-007] | Milestone | en | it | [BC-001], [PP-001] | - | RC-013 |
|
||||
| [MIL-007] | Milestone | en | it | [BC-001], [PP-001] | [MIL-008] | RC-013 |
|
||||
| [MIL-008] | Milestone | en | it | [BC-001], [PP-001] | - | RC-019 |
|
||||
| Source and tests of [MIL-002] | Python source code | - | - | [MIL-002], [ADR-0002] | - | RC-014 |
|
||||
| Source and tests of [MIL-003] | Python source code | - | - | [MIL-003], [ADR-0002] | - | RC-015 |
|
||||
| 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
|
||||
|
||||
| Concern (CN-ID) | Views that answer it | Decisions (ADR) | Quality check (TP) | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CN-001 | [BC-001], [MIL-003], [MIL-004], [MIL-005] | [ADR-0002] | Unit tests per gateway | Planned |
|
||||
| CN-001 | [BC-001], [MIL-003], [MIL-004], [MIL-005] | [ADR-0002] | Unit tests per gateway | Covered |
|
||||
| CN-002 | [PP-001], [MIL-001] | [ADR-0001] | Review records | Covered |
|
||||
| CN-003 | [MIL-006] | - | Unit tests per gateway | Planned |
|
||||
| CN-004 | [MIL-002], [MIL-003] | [ADR-0002] | Code review | Planned |
|
||||
| CN-005 | [MIL-002], [MIL-007] | - | Clean clone walk-through | Planned |
|
||||
| CN-006 | [MIL-007] | - | Wiki check | Planned |
|
||||
| CN-007 | [MIL-001], [MIL-007] | - | Repository API check | Planned |
|
||||
| CN-008 | [MIL-002] | [ADR-0002] | pyproject check | Planned |
|
||||
| CN-009 | [MIL-001] | - | Review of .gitignore and history | Planned |
|
||||
| CN-003 | [MIL-006], [MIL-008] | - | Unit tests per gateway | Covered |
|
||||
| CN-004 | [MIL-002], [MIL-003] | [ADR-0002] | Code review | Covered |
|
||||
| CN-005 | [MIL-002], [MIL-007] | - | Clean clone walk-through | Covered |
|
||||
| CN-006 | [MIL-007] | - | Wiki check | Covered |
|
||||
| CN-007 | [MIL-001], [MIL-007] | - | Repository API check | Covered |
|
||||
| CN-008 | [MIL-002] | [ADR-0002] | pyproject check | Covered |
|
||||
| CN-009 | [MIL-001] | - | Review of .gitignore and history | Covered |
|
||||
|
||||
## Open Inconsistencies
|
||||
|
||||
@@ -71,7 +74,7 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
|
||||
- No test plan (`TP`), architecture description (`AD`) or design artifacts exist: ADR-0001 skips stories and use cases, and the code is small. Quality checks in Concern Coverage name the check used instead.
|
||||
- `-` in Upstream means foundational, in Downstream that nothing is built on it yet, and in Last Reviewed that no `RC-*` exists.
|
||||
- Status `Planned` means a milestone will answer the concern; it is not a gap.
|
||||
- Every concern is covered once MIL-007 is merged; the quality check column names the check that was run.
|
||||
|
||||
---
|
||||
|
||||
@@ -89,4 +92,7 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
[MIL-005]: ../milestones/mil-005-timed-run.md
|
||||
[MIL-006]: ../milestones/mil-006-strategy.md
|
||||
[MIL-007]: ../milestones/mil-007-documentation.md
|
||||
[MIL-008]: ../milestones/mil-008-smarter-rule.md
|
||||
[cadc5ef]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/cadc5eff21d9afc09799d4bc2e3f6ac17ab2612a
|
||||
[9a6796f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/9a6796f775680effe37a54f2609c3915140c8c0e
|
||||
[2f1c7ff]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/2f1c7ff00a80bdcfa4ed4ff087227a3e4d4d591c
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
[English](Home) · **Dansk** · [Bahasa Malaysia](Home-ms)
|
||||
|
||||
# Cookie Clicker Bot
|
||||
|
||||
En Selenium-bot, der spiller [Cookie Clicker](https://ozh.github.io/cookieclicker/) for dig: den klikker på den store cookie, køber den dyreste opgradering, den har råd til, hvert 5. sekund og udskriver cookies pr. sekund efter 5 minutter. Det er dag 48 i Udemys *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||
|
||||
## De fem trin i lektionen
|
||||
|
||||
| Trin | Hvad botten lærer | Side |
|
||||
| --- | --- | --- |
|
||||
| 1 | Lær spillet og dets side at kende | [Step 1](Step-1-Get-to-Know-the-Game-da) |
|
||||
| 2 | Luk sprogvalget og klik på cookien | [Step 2](Step-2-Click-the-Cookie-da) |
|
||||
| 3 | Køb den dyreste opgradering, der er råd til, hvert 5. sekund | [Step 3](Step-3-Buy-Upgrades-da) |
|
||||
| 4 | Stop efter 5 minutter og udskriv cookies pr. sekund | [Step 4](Step-4-Five-Minute-Run-da) |
|
||||
| 5 | Justér strategien og sammenlign kørsler | [Step 5](Step-5-Improve-the-Strategy-da) |
|
||||
|
||||
Hver side viser et skærmbillede af spillet på det pågældende trin og forklarer, hvad vi fik ud af det.
|
||||
|
||||
## Kør den
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
cookie-clicker-bot
|
||||
```
|
||||
|
||||
For at holde dit skrivebord frit kan du køre den i Docker (headless Chromium): `docker build -t cookie-clicker-bot .` og `docker run --rm cookie-clicker-bot`. Repositoriets README har den fulde vejledning til Windows, Linux og macOS samt til testene.
|
||||
|
||||
## Resultater
|
||||
|
||||
| Kørsel | cookies/second |
|
||||
| --- | --- |
|
||||
| Lektionens trin 4: tjek hvert 5. sekund, køb én (alene på computeren) | 18.9 |
|
||||
| Lektionens trin 5: tjek hvert 10. sekund, køb én (bedste i sammenligningen) | 18.2 |
|
||||
|
||||
Rækkerne stammer fra forskellige forhold; [Step 5](Step-5-Improve-the-Strategy-da) forklarer, hvordan de blev målt.
|
||||
|
||||
## Sådan er koden organiseret
|
||||
|
||||
- `domain/`: reglerne (fortolkning af sidens tekst, valg af opgradering) uden Selenium.
|
||||
- `application/`: spillets løkke, som taler med spillet gennem en port.
|
||||
- `infrastructure/`: Selenium-adapteren og Chromium-fabrikken.
|
||||
- `constants.py`: alle konstanter.
|
||||
|
||||
Testene bruger en falsk driver, så de kører på et sekund og kræver ingen browser.
|
||||
@@ -0,0 +1,46 @@
|
||||
[English](Home) · [Dansk](Home-da) · **Bahasa Malaysia**
|
||||
|
||||
# Cookie Clicker Bot
|
||||
|
||||
Bot Selenium yang bermain [Cookie Clicker](https://ozh.github.io/cookieclicker/) untuk anda: ia mengklik kuki besar, membeli naik taraf termahal yang mampu dibeli setiap 5 saat dan mencetak kuki sesaat selepas 5 minit. Ini ialah hari ke-48 kursus Udemy *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||
|
||||
## Lima langkah kuliah
|
||||
|
||||
| Langkah | Apa yang dipelajari bot | Halaman |
|
||||
| --- | --- | --- |
|
||||
| 1 | Mengenali permainan dan halamannya | [Langkah 1](Step-1-Get-to-Know-the-Game-ms) |
|
||||
| 2 | Menutup pemilihan bahasa dan mengklik kuki | [Langkah 2](Step-2-Click-the-Cookie-ms) |
|
||||
| 3 | Setiap 5 saat, beli naik taraf termahal yang mampu dibeli | [Langkah 3](Step-3-Buy-Upgrades-ms) |
|
||||
| 4 | Berhenti selepas 5 minit dan cetak kuki sesaat | [Langkah 4](Step-4-Five-Minute-Run-ms) |
|
||||
| 5 | Ubah suai strategi dan bandingkan larian | [Langkah 5](Step-5-Improve-the-Strategy-ms) |
|
||||
|
||||
Setiap halaman menunjukkan tangkapan skrin permainan pada langkah tersebut dan menerangkan apa yang kita peroleh di dalamnya.
|
||||
|
||||
## Jalankannya
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
cookie-clicker-bot
|
||||
```
|
||||
|
||||
Untuk memastikan desktop anda bebas, jalankannya dalam Docker (Chromium tanpa kepala): `docker build -t cookie-clicker-bot .` dan `docker run --rm cookie-clicker-bot`. README repositori mengandungi arahan lengkap untuk Windows, Linux dan macOS, serta untuk ujian.
|
||||
|
||||
## Keputusan
|
||||
|
||||
| Larian | cookies/second |
|
||||
| --- | --- |
|
||||
| Langkah kuliah 4: semak setiap 5 saat, beli satu (bersendirian pada komputer) | 18.9 |
|
||||
| Langkah kuliah 5: semak setiap 10 saat, beli satu (terbaik dalam perbandingan) | 18.2 |
|
||||
|
||||
Baris-baris ini datang daripada keadaan yang berbeza; [Langkah 5](Step-5-Improve-the-Strategy-ms) menerangkan cara ia diukur.
|
||||
|
||||
## Cara kod disusun
|
||||
|
||||
- `domain/`: peraturan (menghurai teks halaman, memilih naik taraf), tanpa Selenium.
|
||||
- `application/`: gelung permainan, yang berhubung dengan permainan melalui port.
|
||||
- `infrastructure/`: penyesuai Selenium dan kilang Chromium.
|
||||
- `constants.py`: semua pemalar.
|
||||
|
||||
Ujian menggunakan pemacu palsu, jadi ia berjalan dalam sesaat dan tidak memerlukan pelayar.
|
||||
@@ -0,0 +1,46 @@
|
||||
**English** · [Dansk](Home-da) · [Bahasa Malaysia](Home-ms)
|
||||
|
||||
# Cookie Clicker Bot
|
||||
|
||||
A Selenium bot that plays [Cookie Clicker](https://ozh.github.io/cookieclicker/) for you: it clicks the big cookie, buys the most expensive upgrade it can afford every 5 seconds and prints the cookies per second after 5 minutes. It is day 48 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||
|
||||
## The five lecture steps
|
||||
|
||||
| Step | What the bot learns | Page |
|
||||
| --- | --- | --- |
|
||||
| 1 | Get to know the game and its page | [Step 1](Step-1-Get-to-Know-the-Game) |
|
||||
| 2 | Dismiss the language selection and click the cookie | [Step 2](Step-2-Click-the-Cookie) |
|
||||
| 3 | Every 5 seconds buy the most expensive affordable upgrade | [Step 3](Step-3-Buy-Upgrades) |
|
||||
| 4 | Stop after 5 minutes and print the cookies per second | [Step 4](Step-4-Five-Minute-Run) |
|
||||
| 5 | Tweak the strategy and compare runs | [Step 5](Step-5-Improve-the-Strategy) |
|
||||
|
||||
Each page shows a screenshot of the game at that step and explains what we gained in it.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
cookie-clicker-bot
|
||||
```
|
||||
|
||||
To keep your desktop free, run it in Docker (headless Chromium): `docker build -t cookie-clicker-bot .` and `docker run --rm cookie-clicker-bot`. The README of the repository has the full instructions for Windows, Linux and macOS, and for the tests.
|
||||
|
||||
## Results
|
||||
|
||||
| Run | cookies/second |
|
||||
| --- | --- |
|
||||
| Lecture step 4: check every 5 seconds, buy one (alone on the computer) | 18.9 |
|
||||
| Lecture step 5: check every 10 seconds, buy one (best of the comparison) | 18.2 |
|
||||
|
||||
The rows come from different conditions; [Step 5](Step-5-Improve-the-Strategy) explains how they were measured.
|
||||
|
||||
## How the code is organised
|
||||
|
||||
- `domain/`: the rules (parsing the page text, choosing the upgrade), with no Selenium.
|
||||
- `application/`: the game loop, which talks to the game through a port.
|
||||
- `infrastructure/`: the Selenium adapter and the Chromium factory.
|
||||
- `constants.py`: every constant.
|
||||
|
||||
The tests use a fake driver, so they run in a second and need no browser.
|
||||
@@ -0,0 +1,36 @@
|
||||
[English](Step-1-Get-to-Know-the-Game) · **Dansk** · [Bahasa Malaysia](Step-1-Get-to-Know-the-Game-ms)
|
||||
|
||||
# Lektionens trin 1: lær spillet at kende
|
||||
|
||||

|
||||
|
||||
## Hvad opgaven går ud på
|
||||
|
||||
Åbn den nye version af Cookie Clicker (ozh.github.io/cookieclicker) og lær, hvordan den fungerer, før du skriver kode.
|
||||
|
||||
## Hvad vi fandt
|
||||
|
||||
Vi åbnede siden i en headless Chromium i en Docker-container og kiggede på dens HTML. Det, botten har brug for:
|
||||
|
||||
| Hvad | Hvor på siden |
|
||||
| --- | --- |
|
||||
| Engelsk i sprogvalget | `#langSelect-EN` |
|
||||
| Den store cookie | `#bigCookie` |
|
||||
| Antal cookies og cookies pr. sekund | `#cookies` (og `#cookiesPerSecond` inde i den) |
|
||||
| Butikken | `#products`, med `#product0` (Cursor), `#product1` (Grandma), `#product2` (Farm) osv. |
|
||||
| Et produkts navn og pris | `.productName` og `.price` inde i produktet |
|
||||
| Har jeg råd? | produktet har klassen `unlocked enabled` (et produkt, du endnu ikke har råd til, er `unlocked disabled`; et skjult er `locked disabled`) |
|
||||
|
||||
Godt at vide:
|
||||
|
||||
- Spillet beder om et sprog, første gang siden indlæses. Botten skal vælge et af sig selv.
|
||||
- Antallet er skrevet som tekst: `3 cookies`, `5,000 cookies`, og for store tal på to linjer, `1.235` og `million cookies`.
|
||||
- Et cookie-banner ("Got it!") sidder nederst på siden. Botten klikker aldrig på det, fordi det er et samtykkebanner, der tilhører den, der spiller; botten arbejder uden om det (se [Step 4](Step-4-Five-Minute-Run-da)).
|
||||
|
||||
Alle disse id'er og selektorer står ét sted, i `constants.py`.
|
||||
|
||||
## Hvad vi fik ud af dette trin
|
||||
|
||||
- **Læs siden, før du koder.** Et kort kig på den rigtige HTML sparede timers gætværk; hver eneste selektor i botten stammer fra dette trin.
|
||||
- **Hav selektorerne ét sted.** Hvis spillet ændrer et id, er rettelsen én linje i `constants.py`.
|
||||
- **Kør browseren i en container.** Det samme Docker-image, der kører botten, tager skærmbillederne, så der åbner intet på dit eget skrivebord, og hver kørsel starter fra den samme rene browser.
|
||||
@@ -0,0 +1,36 @@
|
||||
[English](Step-1-Get-to-Know-the-Game) · [Dansk](Step-1-Get-to-Know-the-Game-da) · **Bahasa Malaysia**
|
||||
|
||||
# Langkah kuliah 1: mengenali permainan
|
||||
|
||||

|
||||
|
||||
## Apa yang diminta oleh tugasan
|
||||
|
||||
Buka versi baharu Cookie Clicker (ozh.github.io/cookieclicker) dan fahami cara ia berfungsi sebelum menulis sebarang kod.
|
||||
|
||||
## Apa yang kami temui
|
||||
|
||||
Kami membuka halaman itu dalam Chromium tanpa kepala di dalam bekas Docker dan melihat HTML-nya. Bahagian yang diperlukan oleh bot:
|
||||
|
||||
| Apa | Di mana pada halaman |
|
||||
| --- | --- |
|
||||
| Bahasa Inggeris pada pemilihan bahasa | `#langSelect-EN` |
|
||||
| Kuki besar | `#bigCookie` |
|
||||
| Bilangan kuki dan kuki sesaat | `#cookies` (dan `#cookiesPerSecond` di dalamnya) |
|
||||
| Kedai | `#products`, dengan `#product0` (Cursor), `#product1` (Grandma), `#product2` (Farm) dan seterusnya |
|
||||
| Nama dan harga produk | `.productName` dan `.price` di dalam produk |
|
||||
| Mampukah saya membelinya? | produk itu mempunyai kelas `unlocked enabled` (produk yang belum mampu dibeli ialah `unlocked disabled`; yang tersembunyi ialah `locked disabled`) |
|
||||
|
||||
Juga baik untuk diketahui:
|
||||
|
||||
- Permainan meminta bahasa apabila halaman dimuatkan buat kali pertama. Bot mesti memilih satu sendiri.
|
||||
- Bilangan ditulis sebagai teks: `3 cookies`, `5,000 cookies`, dan untuk nombor besar pada dua baris, `1.235` dan `million cookies`.
|
||||
- Sepanduk kuki ("Got it!") berada di bahagian bawah halaman. Bot tidak pernah mengkliknya, kerana ia ialah sepanduk persetujuan yang menjadi hak orang yang bermain; bot mengatasinya dengan cara lain (lihat [Langkah 4](Step-4-Five-Minute-Run-ms)).
|
||||
|
||||
Semua id dan pemilih ini ditulis sekali sahaja, dalam `constants.py`.
|
||||
|
||||
## Apa yang kita peroleh dalam langkah ini
|
||||
|
||||
- **Baca halaman sebelum menulis kod.** Meninjau sebentar HTML sebenar menjimatkan berjam-jam meneka; setiap pemilih dalam bot datang daripada langkah ini.
|
||||
- **Simpan pemilih di satu tempat.** Jika permainan menukar sesuatu id, satu baris dalam `constants.py` ialah pembetulannya.
|
||||
- **Jalankan pelayar dalam bekas.** Imej Docker yang sama yang menjalankan bot mengambil tangkapan skrin, jadi tiada apa yang terbuka pada desktop anda sendiri dan setiap larian bermula daripada pelayar bersih yang sama.
|
||||
@@ -0,0 +1,36 @@
|
||||
**English** · [Dansk](Step-1-Get-to-Know-the-Game-da) · [Bahasa Malaysia](Step-1-Get-to-Know-the-Game-ms)
|
||||
|
||||
# Lecture step 1: get to know the game
|
||||
|
||||

|
||||
|
||||
## What the assignment asks
|
||||
|
||||
Open the new version of Cookie Clicker (ozh.github.io/cookieclicker) and learn how it works before writing any code.
|
||||
|
||||
## What we found
|
||||
|
||||
We opened the page in a headless Chromium inside a Docker container and looked at its HTML. The parts the bot needs:
|
||||
|
||||
| What | Where on the page |
|
||||
| --- | --- |
|
||||
| English on the language selection | `#langSelect-EN` |
|
||||
| The big cookie | `#bigCookie` |
|
||||
| The cookie count and the cookies per second | `#cookies` (and `#cookiesPerSecond` inside it) |
|
||||
| The store | `#products`, with `#product0` (Cursor), `#product1` (Grandma), `#product2` (Farm) and so on |
|
||||
| A product's name and price | `.productName` and `.price` inside the product |
|
||||
| Can I afford it? | the product has the class `unlocked enabled` (a product you cannot afford yet is `unlocked disabled`; a hidden one is `locked disabled`) |
|
||||
|
||||
Also good to know:
|
||||
|
||||
- The game asks for a language the first time the page loads. The bot must pick one by itself.
|
||||
- The count is written as text: `3 cookies`, `5,000 cookies`, and for large numbers on two lines, `1.235` and `million cookies`.
|
||||
- A cookie banner ("Got it!") sits at the bottom of the page. The bot never clicks it, because it is a consent banner that belongs to the person playing; the bot works around it (see [Step 4](Step-4-Five-Minute-Run)).
|
||||
|
||||
All of these ids and selectors are written once, in `constants.py`.
|
||||
|
||||
## What we gained in this step
|
||||
|
||||
- **Read the page before coding.** A short look at the real HTML saved hours of guessing; every selector in the bot came from this step.
|
||||
- **Keep selectors in one place.** If the game changes an id, one line in `constants.py` is the fix.
|
||||
- **Run the browser in a container.** The same Docker image that runs the bot takes the screenshots, so nothing opens on your own desktop and every run starts from the same clean browser.
|
||||
@@ -0,0 +1,26 @@
|
||||
[English](Step-2-Click-the-Cookie) · **Dansk** · [Bahasa Malaysia](Step-2-Click-the-Cookie-ms)
|
||||
|
||||
# Lektionens trin 2: klik på cookien så hurtigt som muligt
|
||||
|
||||

|
||||
|
||||
## Hvad opgaven går ud på
|
||||
|
||||
Lav en bot med Selenium og Python, der klikker på cookien så hurtigt som muligt. Den skal selv lukke sprogvalget, før den går i gang.
|
||||
|
||||
## Hvad vi byggede
|
||||
|
||||
- `open_game` indlæser siden.
|
||||
- `dismiss_language` venter, til den engelske knap er synlig, klikker på den og venter derefter, til den store cookie er synlig. Spillet skal bruge nogle sekunder på at starte, efter sproget er valgt, så botten poller (hvert 0,2 sekund, højst 20 sekunder) i stedet for at sove i fast tid.
|
||||
- `click_cookie` klikker én gang på `#bigCookie`. Hvis et banner eller en gentegning blokerer klikket et øjeblik, prøver botten igen i næste runde.
|
||||
- En løkke kalder `click_cookie`, indtil tiden er gået. I en kørsel på 15 sekunder i containeren klikkede botten 344 gange, og skærmbilledet ovenfor viser 342 cookies.
|
||||
|
||||
```bash
|
||||
cookie-clicker-bot --headless --duration 15 --screenshot step-2.png
|
||||
```
|
||||
|
||||
## Hvad vi fik ud af dette trin
|
||||
|
||||
- **Vent på en betingelse, ikke på et tidsrum.** En fast `sleep` er enten for kort (botten fejler) eller for lang (botten spilder tid). At polle efter "cookien er synlig" er både hurtigere og sikrere.
|
||||
- **Hold browseren bag en port.** Løkken kender kun `GamePort` (`open_game`, `dismiss_language`, `click_cookie`, ...). Selenium-koden bor i én adapter, så løkken testes med et falsk spil på få millisekunder.
|
||||
- **Tidsstyr en løkke med et ur, du kan udskifte.** `click_and_buy` tager uret som parameter; testene giver den et falsk ur og kontrollerer antallet af klik helt præcist.
|
||||
@@ -0,0 +1,26 @@
|
||||
[English](Step-2-Click-the-Cookie) · [Dansk](Step-2-Click-the-Cookie-da) · **Bahasa Malaysia**
|
||||
|
||||
# Langkah kuliah 2: klik kuki secepat mungkin
|
||||
|
||||

|
||||
|
||||
## Apa yang diminta oleh tugasan
|
||||
|
||||
Cipta bot dengan Selenium dan Python yang mengklik kuki secepat mungkin. Ia mesti menutup pemilihan bahasa sendiri sebelum bermula.
|
||||
|
||||
## Apa yang kami bina
|
||||
|
||||
- `open_game` memuatkan halaman.
|
||||
- `dismiss_language` menunggu sehingga butang bahasa Inggeris kelihatan, mengkliknya, kemudian menunggu sehingga kuki besar kelihatan. Permainan memerlukan beberapa saat untuk bermula selepas bahasa dipilih, jadi bot membuat pengundian (setiap 0.2 saat, paling lama 20 saat) dan bukannya tidur untuk masa yang tetap.
|
||||
- `click_cookie` mengklik `#bigCookie` sekali. Jika sepanduk atau pemaparan semula menghalang klik seketika, bot mencuba lagi pada pusingan seterusnya.
|
||||
- Satu gelung memanggil `click_cookie` sehingga masa tamat. Dalam larian 15 saat di dalam bekas, bot mengklik sebanyak 344 kali, dan tangkapan skrin di atas menunjukkan 342 kuki.
|
||||
|
||||
```bash
|
||||
cookie-clicker-bot --headless --duration 15 --screenshot step-2.png
|
||||
```
|
||||
|
||||
## Apa yang kita peroleh dalam langkah ini
|
||||
|
||||
- **Tunggu sesuatu keadaan, bukan sesuatu masa.** `sleep` yang tetap sama ada terlalu pendek (bot gagal) atau terlalu lama (bot membazir masa). Mengundi untuk "kuki kelihatan" adalah lebih pantas dan lebih selamat.
|
||||
- **Letakkan pelayar di belakang port.** Gelung hanya mengenali `GamePort` (`open_game`, `dismiss_language`, `click_cookie`, ...). Kod Selenium berada dalam satu penyesuai, jadi gelung diuji dengan permainan palsu dalam beberapa milisaat.
|
||||
- **Catat masa gelung dengan jam yang boleh digantikan.** `click_and_buy` menerima jam sebagai parameter; ujian memberikannya jam palsu dan menyemak bilangan klik dengan tepat.
|
||||
@@ -0,0 +1,26 @@
|
||||
**English** · [Dansk](Step-2-Click-the-Cookie-da) · [Bahasa Malaysia](Step-2-Click-the-Cookie-ms)
|
||||
|
||||
# Lecture step 2: click the cookie as fast as possible
|
||||
|
||||

|
||||
|
||||
## What the assignment asks
|
||||
|
||||
Create a bot with Selenium and Python that clicks the cookie as fast as possible. It must dismiss the language selection by itself before it starts.
|
||||
|
||||
## What we built
|
||||
|
||||
- `open_game` loads the page.
|
||||
- `dismiss_language` waits until the English button is visible, clicks it, and then waits until the big cookie is visible. The game needs a few seconds to start after the language is chosen, so the bot polls (every 0.2 seconds, at most 20 seconds) instead of sleeping for a fixed time.
|
||||
- `click_cookie` clicks `#bigCookie` once. If a banner or a re-render blocks the click for a moment, the bot tries again on the next round.
|
||||
- A loop calls `click_cookie` until the time is up. In a 15 second run in the container the bot clicked 344 times, and the screenshot above shows 342 cookies.
|
||||
|
||||
```bash
|
||||
cookie-clicker-bot --headless --duration 15 --screenshot step-2.png
|
||||
```
|
||||
|
||||
## What we gained in this step
|
||||
|
||||
- **Wait for a condition, not for a time.** A fixed `sleep` is either too short (the bot fails) or too long (the bot wastes time). Polling for "the cookie is visible" is both faster and safer.
|
||||
- **Keep the browser behind a port.** The loop knows only `GamePort` (`open_game`, `dismiss_language`, `click_cookie`, ...). The Selenium code lives in one adapter, so the loop is tested with a fake game in a few milliseconds.
|
||||
- **Time a loop with a clock you can replace.** `click_and_buy` takes the clock as a parameter; the tests give it a fake clock and check the number of clicks exactly.
|
||||
@@ -0,0 +1,23 @@
|
||||
[English](Step-3-Buy-Upgrades) · **Dansk** · [Bahasa Malaysia](Step-3-Buy-Upgrades-ms)
|
||||
|
||||
# Lektionens trin 3: køb den dyreste opgradering, der er råd til
|
||||
|
||||

|
||||
|
||||
## Hvad opgaven går ud på
|
||||
|
||||
Hvert 5. sekund skal botten se på ruden til højre, finde de opgraderinger, der er råd til, og købe den dyreste. Med 105 cookies er der for eksempel råd til både Cursor (15) og Grandma (100), så botten køber Grandma. Hintene peger på `split`, `strip` og `replace` til at omdanne sidens tekst til tal.
|
||||
|
||||
## Hvad vi byggede
|
||||
|
||||
- `parse_amount` og `parse_cookie_count` omdanner sidens tekst til tal. De fjerner ordet `cookies`, dropper alt efter `per second`, fjerner tusindtalskommaerne med `replace` og forstår ord som `1.4 million`. Eksempler: `5,000 cookies` giver 5000; `1.235` og `million cookies` på to linjer giver 1.235.000.
|
||||
- `SeleniumGame.upgrades` læser hvert synligt produkt: dets id, navn og pris.
|
||||
- `choose_most_expensive_affordable(upgrades, cookies)` er en ren funktion: blandt de opgraderinger, hvis pris ikke overstiger antallet af cookies, returnerer den den dyreste eller ingenting. Den er testet med eksemplet ovenfor (105 cookies giver Grandma).
|
||||
- `click_and_buy` bliver ved med at klikke, og når `check_interval_seconds` (som standard 5) er gået, læser den antallet, vælger og køber.
|
||||
- I en kørsel på 45 sekunder i containeren købte botten 8 opgraderinger, og skærmbilledet viser de Cursors og Grandmas, den ejer.
|
||||
|
||||
## Hvad vi fik ud af dette trin
|
||||
|
||||
- **Det svære er teksten, ikke klikket.** Det samme tal optræder som `120`, `5,000`, `1.4 million` og på to linjer. Fortolkningen er en lille ren funktion med mange testcases, så den er sikker at ændre.
|
||||
- **Hold reglen væk fra browseren.** "Køb den dyreste opgradering, der er råd til" er én funktion, der intet ved om Selenium. Derfor kunne trin 5 tilføje en anden regel uden at røre browserkoden.
|
||||
- **Tæl ikke et køb, du ikke har foretaget.** Den første version loggede et køb, hver gang den klikkede. En rigtig kørsel viste, at klikket ikke altid nåede produktet, så `buy` melder nu, om det gjorde, og kun rigtige køb tælles (se [Step 4](Step-4-Five-Minute-Run-da)).
|
||||
@@ -0,0 +1,23 @@
|
||||
[English](Step-3-Buy-Upgrades) · [Dansk](Step-3-Buy-Upgrades-da) · **Bahasa Malaysia**
|
||||
|
||||
# Langkah kuliah 3: beli naik taraf termahal yang mampu dibeli
|
||||
|
||||

|
||||
|
||||
## Apa yang diminta oleh tugasan
|
||||
|
||||
Setiap 5 saat, lihat anak tetingkap sebelah kanan, cari naik taraf yang mampu dibeli, dan beli yang paling mahal. Contohnya, dengan 105 kuki, Cursor (15) dan Grandma (100) kedua-duanya mampu dibeli, jadi bot membeli Grandma. Petunjuk menyebut `split`, `strip` dan `replace` untuk menukar teks halaman kepada nombor.
|
||||
|
||||
## Apa yang kami bina
|
||||
|
||||
- `parse_amount` dan `parse_cookie_count` menukar teks halaman kepada nombor. Ia membuang perkataan `cookies`, menggugurkan segala-galanya selepas `per second`, membuang koma ribuan dengan `replace`, dan memahami perkataan seperti `1.4 million`. Contoh: `5,000 cookies` menghasilkan 5000; `1.235` dan `million cookies` pada dua baris menghasilkan 1,235,000.
|
||||
- `SeleniumGame.upgrades` membaca setiap produk yang kelihatan: id, nama dan harganya.
|
||||
- `choose_most_expensive_affordable(upgrades, cookies)` ialah fungsi tulen: antara naik taraf yang harganya tidak melebihi bilangan kuki, pulangkan yang paling mahal, atau tiada apa-apa. Ia diuji dengan contoh di atas (105 kuki menghasilkan Grandma).
|
||||
- `click_and_buy` terus mengklik, dan apabila `check_interval_seconds` (5 secara lalai) telah berlalu, ia membaca bilangan kuki, memilih, dan membeli.
|
||||
- Dalam larian bekas selama 45 saat, bot membeli 8 naik taraf, dan tangkapan skrin menunjukkan Cursor dan Grandma yang dimilikinya.
|
||||
|
||||
## Apa yang kita peroleh dalam langkah ini
|
||||
|
||||
- **Bahagian yang sukar ialah teks, bukan kliknya.** Nombor yang sama muncul sebagai `120`, `5,000`, `1.4 million` dan pada dua baris. Penghuraian ialah fungsi tulen kecil dengan banyak kes ujian, jadi selamat untuk diubah.
|
||||
- **Jauhkan peraturan daripada pelayar.** "Beli naik taraf termahal yang mampu dibeli" ialah satu fungsi yang tidak tahu apa-apa tentang Selenium. Itulah sebabnya langkah 5 boleh menambah peraturan kedua tanpa menyentuh kod pelayar.
|
||||
- **Jangan kira pembelian yang tidak anda buat.** Versi pertama mencatat pembelian setiap kali ia mengklik. Larian sebenar menunjukkan klik itu tidak selalu sampai ke produk, jadi `buy` kini memberitahu sama ada ia berjaya, dan hanya pembelian sebenar dikira (lihat [Langkah 4](Step-4-Five-Minute-Run-ms)).
|
||||
@@ -0,0 +1,23 @@
|
||||
**English** · [Dansk](Step-3-Buy-Upgrades-da) · [Bahasa Malaysia](Step-3-Buy-Upgrades-ms)
|
||||
|
||||
# Lecture step 3: buy the most expensive affordable upgrade
|
||||
|
||||

|
||||
|
||||
## What the assignment asks
|
||||
|
||||
Every 5 seconds, look at the right-hand pane, find the upgrades you can afford, and buy the most expensive one. For example, with 105 cookies both Cursor (15) and Grandma (100) are affordable, so the bot buys Grandma. The hints point to `split`, `strip` and `replace` for turning the page text into numbers.
|
||||
|
||||
## What we built
|
||||
|
||||
- `parse_amount` and `parse_cookie_count` turn page text into numbers. They remove the word `cookies`, drop everything after `per second`, take away the thousands commas with `replace`, and understand words such as `1.4 million`. Examples: `5,000 cookies` gives 5000; `1.235` and `million cookies` on two lines gives 1,235,000.
|
||||
- `SeleniumGame.upgrades` reads every visible product: its id, its name and its price.
|
||||
- `choose_most_expensive_affordable(upgrades, cookies)` is a pure function: of the upgrades whose price is not above the cookies, return the most expensive one, or nothing. It is tested with the example above (105 cookies gives Grandma).
|
||||
- `click_and_buy` keeps clicking, and when `check_interval_seconds` (5 by default) have passed it reads the count, chooses, and buys.
|
||||
- In a 45 second container run the bot bought 8 upgrades, and the screenshot shows the Cursors and Grandmas it owns.
|
||||
|
||||
## What we gained in this step
|
||||
|
||||
- **The hard part is the text, not the click.** The same number appears as `120`, `5,000`, `1.4 million` and on two lines. Parsing is a small pure function with many test cases, so it is safe to change.
|
||||
- **Keep the rule away from the browser.** "Buy the most expensive affordable upgrade" is one function that knows nothing about Selenium. That is why step 5 could add a second rule without touching the browser code.
|
||||
- **Do not count a purchase you did not make.** The first version logged a purchase whenever it clicked. A real run showed the click did not always reach the product, so `buy` now says whether it did, and only real purchases are counted (see [Step 4](Step-4-Five-Minute-Run)).
|
||||
@@ -0,0 +1,33 @@
|
||||
[English](Step-4-Five-Minute-Run) · **Dansk** · [Bahasa Malaysia](Step-4-Five-Minute-Run-ms)
|
||||
|
||||
# Lektionens trin 4: stop efter fem minutter og udskriv cookies pr. sekund
|
||||
|
||||

|
||||
|
||||
## Hvad opgaven går ud på
|
||||
|
||||
Efter 5 minutter stopper botten og udskriver `cookies/second`.
|
||||
|
||||
## Hvad vi byggede
|
||||
|
||||
- `click_and_buy` kører til en deadline målt med et monotont ur (`time.monotonic`), så kørslen varer præcis `RUN_DURATION_SECONDS` (300) og driver ikke, hvis et klik er langsomt.
|
||||
- `SeleniumGame.cookies_per_second` læser spillets eget tal fra `#cookiesPerSecond` (for eksempel `per second: 18.9`), og `parse_cookies_per_second` omdanner det til et tal, også værdier som `1.2 million`.
|
||||
- `format_result` bygger linjen `cookies/second: 18.9`. Programmet udskriver præcis denne ene linje på standard output; loggen (klik, køb, forløbet tid) går til standard error, så `cookie-clicker-bot > result.txt` kun beholder resultatet.
|
||||
|
||||
## Vores første resultat
|
||||
|
||||
En fuld kørsel i Docker-containeren med opgavens regler (tjek hvert 5. sekund, køb det dyreste produkt, der er råd til):
|
||||
|
||||
| Mål | Værdi |
|
||||
| --- | --- |
|
||||
| Varighed | 300,0 sekunder |
|
||||
| Klik | 7.666 |
|
||||
| Købte produkter | 45 (29 Cursor, 16 Grandma) |
|
||||
| Resultat | `cookies/second: 18.9` |
|
||||
|
||||
## Hvad vi fik ud af dette trin
|
||||
|
||||
- **En tidsstyret løkke, der ikke driver.** Et monotont ur og en deadline er mere pålidelige end at tælle sleeps.
|
||||
- **Adskilt output til mennesker og til programmer.** Logning går til standard error og resultatet til standard output.
|
||||
- **To rigtige fejl fundet ved en rigtig kørsel.** Spillet gentegner siden, efter sproget er valgt, så et element kan blive forældet mellem det øjeblik, man finder det, og det øjeblik, man bruger det; løsningen er at slå det op igen. Et cookie-banner dækker en del af butikken, så et klik på et produkt kan blive opfanget; botten klikker så med JavaScript og tæller kun et køb, når klikket gik igennem. Enhedstests med en falsk driver afslørede ingen af problemerne, og derfor har hvert trin en containerkørsel og et skærmbillede.
|
||||
- **Et udgangspunkt.** 18,9 cookies pr. sekund er det tal, trin 5 forsøger at slå.
|
||||
@@ -0,0 +1,33 @@
|
||||
[English](Step-4-Five-Minute-Run) · [Dansk](Step-4-Five-Minute-Run-da) · **Bahasa Malaysia**
|
||||
|
||||
# Langkah kuliah 4: berhenti selepas lima minit dan cetak kuki sesaat
|
||||
|
||||

|
||||
|
||||
## Apa yang diminta oleh tugasan
|
||||
|
||||
Selepas 5 minit bot berhenti dan mencetak `cookies/second`.
|
||||
|
||||
## Apa yang kami bina
|
||||
|
||||
- `click_and_buy` berjalan sehingga tarikh akhir yang diukur dengan jam monotonik (`time.monotonic`), jadi larian berlangsung tepat `RUN_DURATION_SECONDS` (300) dan tidak tersasar jika sesuatu klik perlahan.
|
||||
- `SeleniumGame.cookies_per_second` membaca angka permainan itu sendiri daripada `#cookiesPerSecond` (contohnya `per second: 18.9`), dan `parse_cookies_per_second` menukarnya kepada nombor, termasuk nilai seperti `1.2 million`.
|
||||
- `format_result` membina baris `cookies/second: 18.9`. Program mencetak tepat satu baris ini pada output standard; log (klik, pembelian, masa berlalu) pergi ke ralat standard, jadi `cookie-clicker-bot > result.txt` hanya menyimpan keputusan.
|
||||
|
||||
## Keputusan pertama kami
|
||||
|
||||
Larian penuh dalam bekas Docker dengan peraturan tugasan (semak setiap 5 saat, beli produk termahal yang mampu dibeli):
|
||||
|
||||
| Ukuran | Nilai |
|
||||
| --- | --- |
|
||||
| Tempoh | 300.0 saat |
|
||||
| Klik | 7,666 |
|
||||
| Produk dibeli | 45 (29 Cursor, 16 Grandma) |
|
||||
| Keputusan | `cookies/second: 18.9` |
|
||||
|
||||
## Apa yang kita peroleh dalam langkah ini
|
||||
|
||||
- **Gelung bermasa yang tidak tersasar.** Jam monotonik dan tarikh akhir lebih boleh dipercayai daripada mengira tidur.
|
||||
- **Output berasingan untuk manusia dan untuk program.** Pencatatan log pergi ke ralat standard dan keputusan ke output standard.
|
||||
- **Dua pepijat sebenar ditemui oleh larian sebenar.** Permainan memaparkan semula selepas bahasa dipilih, jadi sesuatu elemen boleh menjadi basi antara ketika ia ditemui dan ketika ia digunakan; pembetulannya ialah mencari semula. Sepanduk kuki menutup sebahagian kedai, jadi klik pada produk boleh dipintas; bot kemudian mengklik dengan JavaScript dan hanya mengira pembelian apabila klik itu berjaya. Ujian unit dengan pemacu palsu tidak menunjukkan sebarang masalah ini, itulah sebabnya setiap langkah mempunyai larian bekas dan tangkapan skrin.
|
||||
- **Garis dasar.** 18.9 kuki sesaat ialah nombor yang cuba dikalahkan oleh langkah 5.
|
||||
@@ -0,0 +1,64 @@
|
||||
[English](Step-5-Improve-the-Strategy) · **Dansk** · [Bahasa Malaysia](Step-5-Improve-the-Strategy-ms)
|
||||
|
||||
# Lektionens trin 5: justér algoritmen
|
||||
|
||||

|
||||
|
||||
## Hvad opgaven går ud på
|
||||
|
||||
Prøv et andet tjekinterval (hvert sekund, hvert 10. sekund) eller køb alle opgraderinger, der er råd til, og se, om cookies pr. sekund stiger.
|
||||
|
||||
## Hvad vi byggede
|
||||
|
||||
- `--interval SECONDS` sætter tiden mellem tjek af opgraderinger. Den skal være større end 0; standarden forbliver 5 sekunder.
|
||||
- `--buy-all` køber alle opgraderinger, der stadig kan være inden for de cookies, du har, den dyreste først (`choose_affordable_upgrades`). Uden den køber botten én opgradering, den dyreste, der er råd til (`choose_most_expensive_affordable`).
|
||||
- Begge regler er rene funktioner i domænepakken, så de enhedstestes uden en browser. Strategien er en værdi (`BuyStrategy`), ikke et flag spredt rundt i koden.
|
||||
|
||||
```bash
|
||||
cookie-clicker-bot --interval 10
|
||||
cookie-clicker-bot --interval 1 --buy-all
|
||||
```
|
||||
|
||||
## Hvad vi målte
|
||||
|
||||
Hver række er en fuld kørsel på 300 sekunder i en Docker-container. Fem containere kørte samtidig på én computer, i to hold, så tallene er gode til at sammenligne rækkerne med hinanden, men en kørsel alene er hurtigere (vores solo-udgangspunkt fra lektionens trin 4 var 18,9).
|
||||
|
||||
| Tjek hvert | Strategi | Hold 1 | Hold 2 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 sekund | køb én | 13.5 | 13.5 |
|
||||
| 1 sekund | køb alle | 13.5 | 13.5 |
|
||||
| 5 sekunder (opgaven) | køb én | 15.5 | 15.5 |
|
||||
| 5 sekunder | køb alle | styrtede ned, se nedenfor | 14.5 |
|
||||
| 10 sekunder | køb én | **18.2** | **18.2** |
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,64 @@
|
||||
[English](Step-5-Improve-the-Strategy) · [Dansk](Step-5-Improve-the-Strategy-da) · **Bahasa Malaysia**
|
||||
|
||||
# Langkah kuliah 5: ubah suai algoritma
|
||||
|
||||

|
||||
|
||||
## Apa yang diminta oleh tugasan
|
||||
|
||||
Cuba selang semakan lain (setiap saat, setiap 10 saat) atau beli semua naik taraf yang mampu dibeli, dan lihat sama ada kuki sesaat meningkat.
|
||||
|
||||
## Apa yang kami bina
|
||||
|
||||
- `--interval SECONDS` menetapkan masa antara semakan naik taraf. Ia mesti lebih besar daripada 0; nilai lalai kekal 5 saat.
|
||||
- `--buy-all` membeli setiap naik taraf yang masih muat dalam kuki yang anda ada, yang paling mahal dahulu (`choose_affordable_upgrades`). Tanpanya bot membeli satu naik taraf, iaitu yang termahal yang mampu dibeli (`choose_most_expensive_affordable`).
|
||||
- Kedua-dua peraturan ialah fungsi tulen dalam pakej domain, jadi ia diuji unit tanpa pelayar. Strategi ialah satu nilai (`BuyStrategy`), bukan bendera yang tersebar di seluruh kod.
|
||||
|
||||
```bash
|
||||
cookie-clicker-bot --interval 10
|
||||
cookie-clicker-bot --interval 1 --buy-all
|
||||
```
|
||||
|
||||
## Apa yang kami ukur
|
||||
|
||||
Setiap baris ialah larian penuh 300 saat dalam bekas Docker. Lima bekas berjalan serentak pada satu komputer, dalam dua kelompok, jadi nombor ini sesuai untuk membandingkan baris antara satu sama lain tetapi larian bersendirian adalah lebih pantas (garis dasar solo kami pada langkah kuliah 4 ialah 18.9).
|
||||
|
||||
| Semak setiap | Strategi | Kelompok 1 | Kelompok 2 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 saat | beli satu | 13.5 | 13.5 |
|
||||
| 1 saat | beli semua | 13.5 | 13.5 |
|
||||
| 5 saat (tugasan) | beli satu | 15.5 | 15.5 |
|
||||
| 5 saat | beli semua | ranap, lihat di bawah | 14.5 |
|
||||
| 10 saat | beli satu | **18.2** | **18.2** |
|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Cookie Clicker Bot, a solution to day 48 of Udemy 100 Days of Code. [English](Home) · [Dansk](Home-da) · [Bahasa Malaysia](Home-ms)
|
||||
@@ -0,0 +1,14 @@
|
||||
**Cookie Clicker Bot**
|
||||
|
||||
- [Home](Home)
|
||||
- [Step 1: Get to know the game](Step-1-Get-to-Know-the-Game)
|
||||
- [Step 2: Click the cookie](Step-2-Click-the-Cookie)
|
||||
- [Step 3: Buy upgrades](Step-3-Buy-Upgrades)
|
||||
- [Step 4: Five minute run](Step-4-Five-Minute-Run)
|
||||
- [Step 5: Improve the strategy](Step-5-Improve-the-Strategy)
|
||||
|
||||
**Language**
|
||||
|
||||
- [English](Home)
|
||||
- [Dansk](Home-da)
|
||||
- [Bahasa Malaysia](Home-ms)
|
||||
+1
-1
Submodule framework updated: 7c2791939c...3197d4b75e
@@ -11,6 +11,7 @@ from cookie_clicker_bot.domain.buy_strategy import BuyStrategy
|
||||
from cookie_clicker_bot.domain.upgrades import (
|
||||
Upgrade,
|
||||
choose_affordable_upgrades,
|
||||
choose_best_value,
|
||||
choose_most_expensive_affordable,
|
||||
)
|
||||
|
||||
@@ -48,16 +49,35 @@ def buy_most_expensive_upgrade(game: GamePort) -> Upgrade | None:
|
||||
return None
|
||||
|
||||
|
||||
def buy_best_value_upgrade(game: GamePort) -> Upgrade | None:
|
||||
"""@brief Buy the affordable upgrade with the lowest price per cookie per second.
|
||||
|
||||
@param game The game to play.
|
||||
@return The upgrade that was bought, or None when nothing was affordable or
|
||||
the click did not reach the product.
|
||||
"""
|
||||
chosen = choose_best_value(game.upgrades(), game.cookie_count())
|
||||
if chosen is not None and game.buy(chosen):
|
||||
return chosen
|
||||
return None
|
||||
|
||||
|
||||
def buy_upgrades(game: GamePort, strategy: BuyStrategy) -> list[Upgrade]:
|
||||
"""@brief Do one purchase round with the given strategy (lecture step 5).
|
||||
|
||||
@param game The game to play.
|
||||
@param strategy BUY_ONE buys the most expensive affordable upgrade, BUY_ALL buys
|
||||
every upgrade the cookies pay for.
|
||||
every upgrade the cookies pay for, BEST_VALUE buys the one with the lowest
|
||||
price per cookie per second.
|
||||
@return The upgrades really bought, in order; a click that missed is left out.
|
||||
"""
|
||||
if strategy is BuyStrategy.BUY_ONE:
|
||||
bought = buy_most_expensive_upgrade(game)
|
||||
if strategy is not BuyStrategy.BUY_ALL:
|
||||
buy_one = (
|
||||
buy_best_value_upgrade
|
||||
if strategy is BuyStrategy.BEST_VALUE
|
||||
else buy_most_expensive_upgrade
|
||||
)
|
||||
bought = buy_one(game)
|
||||
return [] if bought is None else [bought]
|
||||
plan = choose_affordable_upgrades(game.upgrades(), game.cookie_count())
|
||||
return [upgrade for upgrade in plan if game.buy(upgrade)]
|
||||
|
||||
@@ -69,7 +69,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
default=constants.UPGRADE_CHECK_INTERVAL_SECONDS,
|
||||
help="seconds between upgrade checks, greater than 0 (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
choice = parser.add_mutually_exclusive_group()
|
||||
choice.add_argument(
|
||||
"--buy-all",
|
||||
action="store_true",
|
||||
help=(
|
||||
@@ -77,6 +78,14 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
"(default: buy only the most expensive affordable one)"
|
||||
),
|
||||
)
|
||||
choice.add_argument(
|
||||
"--best-value",
|
||||
action="store_true",
|
||||
help=(
|
||||
"at each check buy the affordable upgrade with the lowest price per "
|
||||
"cookie per second"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--headless",
|
||||
action="store_true",
|
||||
@@ -95,6 +104,19 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def choose_strategy(args: argparse.Namespace) -> BuyStrategy:
|
||||
"""@brief Turn the purchase options into a strategy.
|
||||
|
||||
@param args The parsed options.
|
||||
@return BUY_ALL, BEST_VALUE or, by default, BUY_ONE.
|
||||
"""
|
||||
if args.buy_all:
|
||||
return BuyStrategy.BUY_ALL
|
||||
if args.best_value:
|
||||
return BuyStrategy.BEST_VALUE
|
||||
return BuyStrategy.BUY_ONE
|
||||
|
||||
|
||||
def main(
|
||||
argv: Sequence[str] | None = None,
|
||||
game_factory: Callable[[bool], GamePort] = create_game,
|
||||
@@ -113,7 +135,7 @@ def main(
|
||||
game.dismiss_language()
|
||||
if args.screenshot_start is not None:
|
||||
game.save_screenshot(args.screenshot_start)
|
||||
strategy = BuyStrategy.BUY_ALL if args.buy_all else BuyStrategy.BUY_ONE
|
||||
strategy = choose_strategy(args)
|
||||
logger.info(
|
||||
"checking upgrades every %s seconds, strategy %s",
|
||||
args.interval,
|
||||
|
||||
@@ -90,6 +90,12 @@ WINDOW_SIZE: Final[str] = "1366,900"
|
||||
## Script that clicks an element from inside the page, past anything that covers it.
|
||||
JS_CLICK_SCRIPT: Final[str] = "arguments[0].click();"
|
||||
|
||||
## Script that reads the cookies per second of one unit of each product, by index.
|
||||
JS_PRODUCTION_SCRIPT: Final[str] = "return Game.ObjectsById.map(o => o.storedCps);"
|
||||
|
||||
## Prefix of a product element id; the rest is the index into the script's list.
|
||||
PRODUCT_ID_PREFIX: Final[str] = "product"
|
||||
|
||||
## Chromium arguments needed to run headless inside a container.
|
||||
HEADLESS_BROWSER_ARGUMENTS: Final[tuple[str, ...]] = (
|
||||
"--headless=new",
|
||||
|
||||
@@ -12,3 +12,5 @@ class BuyStrategy(Enum):
|
||||
BUY_ONE = "buy-one"
|
||||
## Buy every upgrade the cookies pay for, most expensive first.
|
||||
BUY_ALL = "buy-all"
|
||||
## Buy the affordable upgrade with the lowest price per cookie per second.
|
||||
BEST_VALUE = "best-value"
|
||||
|
||||
@@ -16,6 +16,8 @@ class Upgrade:
|
||||
name: str
|
||||
## Price in cookies.
|
||||
price: int
|
||||
## Cookies per second one unit makes; None when the page did not tell.
|
||||
cookies_per_second: float | None = None
|
||||
|
||||
|
||||
def choose_affordable_upgrades(
|
||||
@@ -39,6 +41,33 @@ def choose_affordable_upgrades(
|
||||
return plan
|
||||
|
||||
|
||||
def choose_best_value(upgrades: Sequence[Upgrade], cookies: int) -> Upgrade | None:
|
||||
"""@brief Pick the affordable upgrade that pays for itself fastest.
|
||||
|
||||
The value of an upgrade is its price divided by the cookies per second it makes,
|
||||
so the lowest ratio wins. Upgrades with unknown or zero production are skipped;
|
||||
when none of the affordable ones has a known production the rule falls back to
|
||||
the most expensive affordable upgrade.
|
||||
|
||||
@param upgrades The visible upgrades, in store order.
|
||||
@param cookies The cookies the player has.
|
||||
@return The chosen upgrade (the first on a tie), or None when none is affordable.
|
||||
"""
|
||||
producing = [
|
||||
upgrade
|
||||
for upgrade in upgrades
|
||||
if upgrade.price <= cookies
|
||||
and upgrade.cookies_per_second is not None
|
||||
and upgrade.cookies_per_second > 0
|
||||
]
|
||||
if not producing:
|
||||
return choose_most_expensive_affordable(upgrades, cookies)
|
||||
return min(
|
||||
producing,
|
||||
key=lambda upgrade: upgrade.price / (upgrade.cookies_per_second or 1.0),
|
||||
)
|
||||
|
||||
|
||||
def choose_most_expensive_affordable(
|
||||
upgrades: Sequence[Upgrade], cookies: int
|
||||
) -> Upgrade | None:
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, Protocol, TypeVar
|
||||
|
||||
from selenium.common.exceptions import (
|
||||
ElementClickInterceptedException,
|
||||
JavascriptException,
|
||||
NoSuchElementException,
|
||||
StaleElementReferenceException,
|
||||
)
|
||||
@@ -149,7 +150,19 @@ class SeleniumGame:
|
||||
products = self._driver.find_elements(
|
||||
By.CSS_SELECTOR, constants.VISIBLE_PRODUCT_SELECTOR
|
||||
)
|
||||
return [self._read_upgrade(product) for product in products]
|
||||
production = self._read_production()
|
||||
return [self._read_upgrade(product, production) for product in products]
|
||||
|
||||
def _read_production(self) -> list[float]:
|
||||
"""@brief Read the cookies per second of one unit of every product.
|
||||
|
||||
@return The numbers by product index; empty when the page does not tell.
|
||||
"""
|
||||
try:
|
||||
production = self._driver.execute_script(constants.JS_PRODUCTION_SCRIPT)
|
||||
except JavascriptException:
|
||||
return []
|
||||
return production if isinstance(production, list) else []
|
||||
|
||||
def _read_again_if_stale(self, read: Callable[[], T]) -> T:
|
||||
"""@brief Run a read; repeat it while the page re-renders under it.
|
||||
@@ -195,12 +208,26 @@ class SeleniumGame:
|
||||
self._driver.quit()
|
||||
|
||||
@staticmethod
|
||||
def _read_upgrade(product: ElementLike) -> Upgrade:
|
||||
"""@brief Read the id, name and price of one product element."""
|
||||
def _read_upgrade(product: ElementLike, production: Sequence[float]) -> Upgrade:
|
||||
"""@brief Read the id, name, price and production of one product element."""
|
||||
name = product.find_element(By.CSS_SELECTOR, constants.PRODUCT_NAME_SELECTOR)
|
||||
price = product.find_element(By.CSS_SELECTOR, constants.PRODUCT_PRICE_SELECTOR)
|
||||
key = product.get_attribute(constants.ID_ATTRIBUTE) or ""
|
||||
return Upgrade(key=key, name=name.text.strip(), price=parse_amount(price.text))
|
||||
return Upgrade(
|
||||
key=key,
|
||||
name=name.text.strip(),
|
||||
price=parse_amount(price.text),
|
||||
cookies_per_second=SeleniumGame._production_of(key, production),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _production_of(key: str, production: Sequence[float]) -> float | None:
|
||||
"""@brief Look up the production of the product with this element id."""
|
||||
index = key.removeprefix(constants.PRODUCT_ID_PREFIX)
|
||||
if not index.isdigit() or int(index) >= len(production):
|
||||
return None
|
||||
value = production[int(index)]
|
||||
return float(value) if isinstance(value, int | float) else None
|
||||
|
||||
def _visible(self, element_id: str) -> ElementLike | None:
|
||||
"""@brief Return the element with this id when it exists and is displayed."""
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
import pytest
|
||||
from selenium.common.exceptions import (
|
||||
ElementClickInterceptedException,
|
||||
JavascriptException,
|
||||
NoSuchElementException,
|
||||
StaleElementReferenceException,
|
||||
)
|
||||
@@ -63,8 +64,13 @@ class FakeDriver:
|
||||
self.screenshots: list[str] = []
|
||||
self.quit_called = False
|
||||
self.scripts: list[tuple[str, FakeElement]] = []
|
||||
self.production: Any = []
|
||||
|
||||
def execute_script(self, script: str, *args: Any) -> None: # noqa: ANN401
|
||||
def execute_script(self, script: str, *args: Any) -> Any: # noqa: ANN401
|
||||
if script == constants.JS_PRODUCTION_SCRIPT:
|
||||
if isinstance(self.production, Exception):
|
||||
raise self.production
|
||||
return self.production
|
||||
self.scripts.append((script, args[0]))
|
||||
|
||||
def get(self, url: str) -> None:
|
||||
@@ -427,3 +433,58 @@ def test_close_quits_the_driver() -> None:
|
||||
make_game(driver).close()
|
||||
|
||||
assert driver.quit_called
|
||||
|
||||
|
||||
def _driver_with_two_products() -> FakeDriver:
|
||||
driver = FakeDriver()
|
||||
driver.groups[constants.VISIBLE_PRODUCT_SELECTOR] = [
|
||||
make_product("product0", "Cursor", "15"),
|
||||
make_product("product1", "Grandma", "100"),
|
||||
]
|
||||
return driver
|
||||
|
||||
|
||||
def test_upgrades_reads_the_production_of_each_product() -> None:
|
||||
driver = _driver_with_two_products()
|
||||
driver.production = [0.1, 1]
|
||||
|
||||
assert make_game(driver).upgrades() == [
|
||||
Upgrade("product0", "Cursor", 15, 0.1),
|
||||
Upgrade("product1", "Grandma", 100, 1.0),
|
||||
]
|
||||
|
||||
|
||||
def test_upgrades_leaves_the_production_unknown_when_the_script_fails() -> None:
|
||||
driver = _driver_with_two_products()
|
||||
driver.production = JavascriptException("Game is not defined")
|
||||
|
||||
upgrades = make_game(driver).upgrades()
|
||||
|
||||
assert [upgrade.cookies_per_second for upgrade in upgrades] == [None, None]
|
||||
|
||||
|
||||
def test_upgrades_leaves_the_production_unknown_for_a_product_the_list_lacks() -> None:
|
||||
driver = _driver_with_two_products()
|
||||
driver.production = [0.1]
|
||||
|
||||
upgrades = make_game(driver).upgrades()
|
||||
|
||||
assert [upgrade.cookies_per_second for upgrade in upgrades] == [0.1, None]
|
||||
|
||||
|
||||
def test_upgrades_leaves_a_production_that_is_not_a_number_unknown() -> None:
|
||||
driver = _driver_with_two_products()
|
||||
driver.production = [{}, 1]
|
||||
|
||||
upgrades = make_game(driver).upgrades()
|
||||
|
||||
assert [upgrade.cookies_per_second for upgrade in upgrades] == [None, 1.0]
|
||||
|
||||
|
||||
def test_upgrades_leaves_the_production_unknown_for_an_unexpected_answer() -> None:
|
||||
driver = _driver_with_two_products()
|
||||
driver.production = None
|
||||
|
||||
upgrades = make_game(driver).upgrades()
|
||||
|
||||
assert [upgrade.cookies_per_second for upgrade in upgrades] == [None, None]
|
||||
|
||||
@@ -179,6 +179,31 @@ def test_parse_args_buys_one_upgrade_unless_buy_all_is_given() -> None:
|
||||
assert parse_args(["--buy-all"]).buy_all is True
|
||||
|
||||
|
||||
def test_parse_args_best_value_is_off_by_default() -> None:
|
||||
assert parse_args([]).best_value is False
|
||||
assert parse_args(["--best-value"]).best_value is True
|
||||
|
||||
|
||||
def test_parse_args_rejects_buy_all_together_with_best_value(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
parse_args(["--buy-all", "--best-value"])
|
||||
|
||||
assert exit_info.value.code == 2
|
||||
assert "not allowed with" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_main_logs_the_best_value_strategy(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level("INFO")
|
||||
|
||||
main(["--duration", "0", "--best-value"], lambda _h: RecordingGame())
|
||||
|
||||
assert "strategy best-value" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["0", "-1", "abc", "nan"])
|
||||
def test_parse_args_rejects_an_interval_that_is_not_positive(
|
||||
value: str, capsys: pytest.CaptureFixture[str]
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
from cookie_clicker_bot.application.game_loop import (
|
||||
PlayResult,
|
||||
buy_best_value_upgrade,
|
||||
buy_most_expensive_upgrade,
|
||||
buy_upgrades,
|
||||
click_and_buy,
|
||||
@@ -14,6 +15,9 @@ from cookie_clicker_bot.application.game_loop import (
|
||||
from cookie_clicker_bot.domain.buy_strategy import BuyStrategy
|
||||
from cookie_clicker_bot.domain.upgrades import Upgrade
|
||||
|
||||
VALUE_CURSOR = Upgrade("product0", "Cursor", 15, 0.1)
|
||||
VALUE_GRANDMA = Upgrade("product1", "Grandma", 100, 1.0)
|
||||
|
||||
CURSOR = Upgrade("product0", "Cursor", 15)
|
||||
GRANDMA = Upgrade("product1", "Grandma", 100)
|
||||
|
||||
@@ -248,3 +252,31 @@ def test_click_and_buy_checks_every_ten_seconds_with_a_ten_second_interval() ->
|
||||
|
||||
assert game.buy_times == [11.0, 21.0]
|
||||
assert result.purchases == ("Grandma", "Grandma")
|
||||
|
||||
|
||||
def test_buy_best_value_upgrade_buys_the_cheapest_per_cookie_per_second() -> None:
|
||||
game = FakeGame(cookies=500, upgrades=[VALUE_CURSOR, VALUE_GRANDMA])
|
||||
|
||||
bought = buy_best_value_upgrade(game)
|
||||
|
||||
assert bought == VALUE_GRANDMA
|
||||
assert game.bought == [VALUE_GRANDMA]
|
||||
|
||||
|
||||
def test_buy_best_value_upgrade_buys_nothing_when_too_poor() -> None:
|
||||
game = FakeGame(cookies=10, upgrades=[VALUE_CURSOR, VALUE_GRANDMA])
|
||||
|
||||
assert buy_best_value_upgrade(game) is None
|
||||
assert game.bought == []
|
||||
|
||||
|
||||
def test_buy_best_value_upgrade_leaves_out_a_click_that_missed() -> None:
|
||||
game = FakeGame(cookies=500, upgrades=[VALUE_GRANDMA], buy_succeeds=False)
|
||||
|
||||
assert buy_best_value_upgrade(game) is None
|
||||
|
||||
|
||||
def test_buy_upgrades_with_best_value_buys_one_upgrade() -> None:
|
||||
game = FakeGame(cookies=500, upgrades=[VALUE_CURSOR, VALUE_GRANDMA])
|
||||
|
||||
assert buy_upgrades(game, BuyStrategy.BEST_VALUE) == [VALUE_GRANDMA]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from cookie_clicker_bot.domain.upgrades import (
|
||||
Upgrade,
|
||||
choose_affordable_upgrades,
|
||||
choose_best_value,
|
||||
choose_most_expensive_affordable,
|
||||
)
|
||||
|
||||
@@ -68,3 +69,46 @@ def test_plan_does_not_change_the_given_list() -> None:
|
||||
choose_affordable_upgrades(upgrades, 1000)
|
||||
|
||||
assert upgrades == [CURSOR, GRANDMA]
|
||||
|
||||
|
||||
VALUE_CURSOR = Upgrade("product0", "Cursor", 15, 0.1)
|
||||
VALUE_GRANDMA = Upgrade("product1", "Grandma", 100, 1.0)
|
||||
VALUE_FARM = Upgrade("product2", "Farm", 1100, 8.0)
|
||||
|
||||
|
||||
def test_best_value_picks_the_lowest_price_per_cookie_per_second() -> None:
|
||||
upgrades = [VALUE_CURSOR, VALUE_GRANDMA, VALUE_FARM]
|
||||
|
||||
assert choose_best_value(upgrades, 2000) == VALUE_GRANDMA
|
||||
|
||||
|
||||
def test_best_value_ignores_what_the_cookies_cannot_pay() -> None:
|
||||
upgrades = [VALUE_CURSOR, VALUE_GRANDMA, VALUE_FARM]
|
||||
|
||||
assert choose_best_value(upgrades, 50) == VALUE_CURSOR
|
||||
|
||||
|
||||
def test_best_value_returns_none_when_nothing_is_affordable() -> None:
|
||||
assert choose_best_value([VALUE_CURSOR, VALUE_GRANDMA], 10) is None
|
||||
|
||||
|
||||
def test_best_value_returns_none_without_upgrades() -> None:
|
||||
assert choose_best_value([], 1000) is None
|
||||
|
||||
|
||||
def test_best_value_takes_the_first_on_a_tie() -> None:
|
||||
twin = Upgrade("product9", "Twin", 100, 1.0)
|
||||
|
||||
assert choose_best_value([VALUE_GRANDMA, twin], 500) == VALUE_GRANDMA
|
||||
|
||||
|
||||
def test_best_value_skips_an_upgrade_with_zero_production() -> None:
|
||||
idle = Upgrade("product9", "Idle", 10, 0.0)
|
||||
|
||||
assert choose_best_value([idle, VALUE_GRANDMA], 500) == VALUE_GRANDMA
|
||||
|
||||
|
||||
def test_best_value_falls_back_to_the_most_expensive_when_production_is_unknown() -> (
|
||||
None
|
||||
):
|
||||
assert choose_best_value([CURSOR, GRANDMA, FARM], 1200) == FARM
|
||||
|
||||
Reference in New Issue
Block a user