diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a5571f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.venv +.env +__pycache__ +*.pyc +.pytest_cache +docs/doxygen +framework +.claude +.agents diff --git a/.gitignore b/.gitignore index 7dddeb2..65d4a12 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,40 @@ # AI prompt log (the repository is public) /docs/ai-prompts/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Tests and coverage +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +coverage.xml + +# Type checker and linter caches +.mypy_cache/ +.ruff_cache/ + +# Doxygen output +/docs/doxygen/ + +# Browser run artefacts (the lecture screenshots in docs/images are kept) +/output/ +*.log + +# Editors and operating system +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f31eaf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Headless Chromium for real-browser runs and screenshots (ADR-0003). +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + CHROME_BIN=/usr/bin/chromium \ + CHROMEDRIVER_PATH=/usr/bin/chromedriver + +RUN apt-get update \ + && apt-get install -y --no-install-recommends chromium chromium-driver fonts-liberation \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY pyproject.toml README.md LICENSE ./ +COPY src ./src +RUN python -m pip install --upgrade pip && python -m pip install -e ".[dev]" +COPY tests ./tests + +ENTRYPOINT ["python", "-m", "cookie_clicker_bot"] diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..459b2a3 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,16 @@ +# Doxygen configuration for the Cookie Clicker bot. Run: doxygen Doxyfile +PROJECT_NAME = "Cookie Clicker Bot" +PROJECT_BRIEF = "Selenium bot that plays Cookie Clicker (Udemy 100 Days of Code, day 48)" +OUTPUT_DIRECTORY = docs/doxygen +INPUT = src +FILE_PATTERNS = *.py +RECURSIVE = YES +EXTRACT_ALL = YES +OPTIMIZE_OUTPUT_JAVA = YES +PYTHON_DOCSTRING = YES +AUTOLINK_SUPPORT = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +QUIET = YES +WARN_IF_UNDOCUMENTED = YES +WARN_AS_ERROR = NO diff --git a/README.md b/README.md index 6417a2d..177028e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,150 @@ -# 048-the-coockie-factory-new +# ๐Ÿช Cookie Clicker Bot +A Selenium bot that plays [Cookie Clicker][game] 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 the solution to day 48 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*, written for course participants who want to compare solutions and for visitors who browse for ideas. + +## ๐Ÿ“š Table of Contents + +- [Overview](#-overview) +- [Requirements](#-requirements) +- [Setup](#-setup) +- [Run](#-run) +- [Tests](#-tests) +- [License](#-license) +- [Links](#-links) +- [Appendix: Project documents](#-appendix-project-documents) + +## ๐Ÿงญ Overview + +The bot follows the five steps of the lecture: + +1. Get to know the game. +2. Click the cookie as fast as possible, after dismissing the language selection. +3. Every 5 seconds buy the most expensive upgrade you can afford. +4. After 5 minutes stop and print `cookies/second`. +5. Tweak the strategy (check interval, buy one or buy all affordable). + +The repository is organised like this: + +| Folder or file | What it is for | +| --- | --- | +| `src/cookie_clicker_bot/` | The bot. `domain/` holds the rules (parsing, choosing the upgrade) with no Selenium, `application/` runs the game loop, `infrastructure/` drives the browser, `constants.py` holds every constant | +| `tests/` | `pytest` tests; the unit tests need no browser | +| `docs/` | Project documents, wiki pages (English, Danish, Bahasa Malaysia) and the screenshots of the lecture steps | +| `Dockerfile` | A Linux image with headless Chromium for real-browser runs and screenshots | +| `Doxyfile` | Doxygen configuration for the source comments | + +## ๐Ÿ“‹ Requirements + +- Python 3.13 or newer. +- Selenium (the only runtime dependency; it is installed by `pip` and finds the browser driver itself). +- Google Chrome or Chromium to run the bot on your own machine. +- `pytest` and `pytest-cov` for the tests (development extra only). +- Docker, only if you want to run the bot or take screenshots without opening a window on your desktop. +- Doxygen, only to build the source documentation. + +## ๐Ÿ› ๏ธ Setup + +Create a local virtual environment, upgrade `pip` and install the project with the test tools: + +```bash +python -m venv .venv +``` + +Activate it: + +```bash +# Linux and macOS +source .venv/bin/activate +``` + +```powershell +# Windows PowerShell +.venv\Scripts\Activate.ps1 +``` + +```bash +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Leave the environment with `deactivate`. The folder `.venv` is ignored by git. + +## โ–ถ๏ธ Run + +With the environment active: + +```bash +cookie-clicker-bot +``` + +or, without installing the command: + +```bash +python -m cookie_clicker_bot +``` + +To keep your desktop free, run it in the Docker container (headless Chromium): + +```bash +docker build -t cookie-clicker-bot . +docker run --rm cookie-clicker-bot +``` + +## ๐Ÿงช Tests + +```bash +python -m pytest +``` + +Coverage of the logic modules is checked with `python -m pytest --cov`. Tests that need a real browser are marked `browser` and run in the Docker container; all other tests need neither a browser nor the network. + +Build the source documentation with Doxygen: + +```bash +doxygen Doxyfile +``` + +## ๐Ÿ“„ License + +GNU Affero General Public License v3.0, see [LICENSE][license]. + +## ๐Ÿ”— Links + +- [Repository][repository] +- [Documentation][documentation] +- [Issue tracker][issues] + +## ๐Ÿ“Ž Appendix: Project documents + +The documents a newcomer needs, in reading order. + +| Document | What it tells you | +| --- | --- | +| [AGENTS.md][agents] | The rules AI agents follow in this repository | +| [Business Case][BC-001] | Why the project exists, its scope and success criteria | +| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers | +| [Project Plan][PP-001] | The gateways and their schedule | +| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks | +| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain | +| [Domain dictionary][DICT-001] | The PO terms and the IT terms | +| [Traceability matrix][TM-001] | How the artifacts link and when each was last reviewed | +| [Framework README][framework-readme] | The framework this project uses | +| [Framework changelog][framework-changelog] | What changed in each framework release | + +--- + +[game]: https://ozh.github.io/cookieclicker/ +[license]: ./LICENSE +[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new +[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/wiki +[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/issues +[agents]: ./AGENTS.md +[BC-001]: ./docs/business-case.md +[SA-001]: ./docs/stakeholder-analysis.md +[PP-001]: ./docs/project-plan.md +[milestones]: ./docs/milestones/ +[registry]: ./docs/artifact-registry.md +[DICT-001]: ./docs/dictionary.md +[TM-001]: ./docs/sqa/traceability-matrix.md +[framework-readme]: ./framework/README.md +[framework-changelog]: ./framework/CHANGELOG.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 0d7cb57..e91f9ca 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g. | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 008 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 014 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 015 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 | diff --git a/docs/sqa/reviews/rc-014-code-mil-002.md b/docs/sqa/reviews/rc-014-code-mil-002.md new file mode 100644 index 0000000..fa9c879 --- /dev/null +++ b/docs/sqa/reviews/rc-014-code-mil-002.md @@ -0,0 +1,62 @@ +# SQA Review Record + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-014 | +| CrossReference | [MIL-002], [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 | [c4a51c3] | + +--- + +## Artifact Under Review + +- Instance reviewed: [MIL-002] +- Checklist used: [QC-PY-001] (`QC-PY-001`) +- Scope: full review of the source and tests added by MIL-002: package skeleton, constants.py, cli.py, smoke test +- 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 | N-A | No file or connection is opened in this milestone. | +| 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 | `cli.py` prints a placeholder line only; the result line becomes the program output in MIL-005. | +| 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 | Tests are named for behaviour, independent of order, and need no network or browser. | +| 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-002 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-002]: ../../milestones/mil-002-scaffold.md +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[c4a51c3]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/048-the-coockie-factory-new/commit/c4a51c37de39f3997e092caceb710762f70d4314 diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index 390be58..a03bb5e 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -41,6 +41,7 @@ Tracks backward and forward links between artifact instances so that the Busines | [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 | +| Source and tests of [MIL-002] | Python source code | - | - | [MIL-002], [ADR-0002] | - | RC-014 | ## Concern Coverage diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8776efc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["setuptools>=75"] +build-backend = "setuptools.build_meta" + +[project] +name = "cookie-clicker-bot" +version = "0.1.0" +description = "Selenium bot that plays Cookie Clicker (Udemy 100 Days of Code, day 48)." +readme = "README.md" +requires-python = ">=3.13" +license = { file = "LICENSE" } +dependencies = [ + "selenium>=4.25", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3", + "pytest-cov>=6.0", + "ruff>=0.8", + "mypy>=1.13", +] + +[project.scripts] +cookie-clicker-bot = "cookie_clicker_bot.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --strict-markers" +pythonpath = ["src"] +markers = [ + "browser: needs a real browser (run in the Docker container)", +] + +[tool.coverage.run] +source = ["cookie_clicker_bot"] +omit = ["*/infrastructure/*", "*/cli.py", "*/__main__.py"] + +[tool.coverage.report] +fail_under = 80 +show_missing = true + +[tool.ruff] +line-length = 88 +target-version = "py313" +src = ["src", "tests"] +extend-exclude = ["framework", ".claude", ".agents"] + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "N", "D", "ANN", "S", "PT"] +ignore = ["D203", "D213", "D205", "D212", "D400", "D415", "D401"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101", "D"] + +[tool.mypy] +strict = true +python_version = "3.13" +files = ["src", "tests"] diff --git a/src/cookie_clicker_bot/__init__.py b/src/cookie_clicker_bot/__init__.py new file mode 100644 index 0000000..d75e785 --- /dev/null +++ b/src/cookie_clicker_bot/__init__.py @@ -0,0 +1,9 @@ +"""@file __init__.py +@brief Selenium bot that plays Cookie Clicker (Udemy 100 Days of Code, day 48). + +@mainpage Cookie Clicker Bot +The bot opens the game, clicks the big cookie, buys the most expensive affordable +upgrade every few seconds and prints the cookies per second when the run ends. +The rules live in the domain package, the game loop in the application package +and the Selenium adapter in the infrastructure package. +""" diff --git a/src/cookie_clicker_bot/__main__.py b/src/cookie_clicker_bot/__main__.py new file mode 100644 index 0000000..6091b13 --- /dev/null +++ b/src/cookie_clicker_bot/__main__.py @@ -0,0 +1,8 @@ +"""@file __main__.py +@brief Entry point for `python -m cookie_clicker_bot`. +""" + +from cookie_clicker_bot.cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/cookie_clicker_bot/application/__init__.py b/src/cookie_clicker_bot/application/__init__.py new file mode 100644 index 0000000..8d1d6ca --- /dev/null +++ b/src/cookie_clicker_bot/application/__init__.py @@ -0,0 +1,3 @@ +"""@file __init__.py +@brief Application layer: the game loop, which talks to the game through a port. +""" diff --git a/src/cookie_clicker_bot/cli.py b/src/cookie_clicker_bot/cli.py new file mode 100644 index 0000000..148cd8a --- /dev/null +++ b/src/cookie_clicker_bot/cli.py @@ -0,0 +1,12 @@ +"""@file cli.py +@brief Command-line entry point (the composition root of the bot). +""" + + +def main() -> int: + """@brief Start the bot. + + @return Process exit code, 0 when the run finished. + """ + print("cookie-clicker-bot: the game loop arrives with MIL-003 to MIL-005.") + return 0 diff --git a/src/cookie_clicker_bot/constants.py b/src/cookie_clicker_bot/constants.py new file mode 100644 index 0000000..926a493 --- /dev/null +++ b/src/cookie_clicker_bot/constants.py @@ -0,0 +1,22 @@ +"""@file constants.py +@brief Every constant of the bot: the game address, timings and messages. + +Selectors of the game page are added here by the milestone that first uses them. +""" + +from typing import Final + +## Address of the new version of Cookie Clicker on GitHub Pages (ozh/cookieclicker). +GAME_URL: Final[str] = "https://ozh.github.io/cookieclicker/" + +## Total playing time of one run, in seconds (lecture step 4: five minutes). +RUN_DURATION_SECONDS: Final[int] = 300 + +## Seconds between two checks for an affordable upgrade (lecture step 3). +UPGRADE_CHECK_INTERVAL_SECONDS: Final[int] = 5 + +## Longest time to wait for an element of the page, in seconds. +WAIT_TIMEOUT_SECONDS: Final[int] = 20 + +## Folder where screenshots of a container run are written. +SCREENSHOT_DIR: Final[str] = "docs/images" diff --git a/src/cookie_clicker_bot/domain/__init__.py b/src/cookie_clicker_bot/domain/__init__.py new file mode 100644 index 0000000..22e3108 --- /dev/null +++ b/src/cookie_clicker_bot/domain/__init__.py @@ -0,0 +1,3 @@ +"""@file __init__.py +@brief Domain layer: the rules of the bot, free of Selenium. +""" diff --git a/src/cookie_clicker_bot/infrastructure/__init__.py b/src/cookie_clicker_bot/infrastructure/__init__.py new file mode 100644 index 0000000..121f33d --- /dev/null +++ b/src/cookie_clicker_bot/infrastructure/__init__.py @@ -0,0 +1,3 @@ +"""@file __init__.py +@brief Infrastructure layer: the Selenium adapter that drives the browser. +""" diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py new file mode 100644 index 0000000..8aed730 --- /dev/null +++ b/tests/unit/test_constants.py @@ -0,0 +1,17 @@ +"""@file test_constants.py +@brief Smoke test: the assignment's timings are the ones in constants.py. +""" + +from cookie_clicker_bot import constants + + +def test_run_lasts_five_minutes() -> None: + assert constants.RUN_DURATION_SECONDS == 5 * 60 + + +def test_upgrades_are_checked_every_five_seconds() -> None: + assert constants.UPGRADE_CHECK_INTERVAL_SECONDS == 5 + + +def test_game_address_is_https() -> None: + assert constants.GAME_URL.startswith("https://")