ai_coc 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_coc-0.2.1/.gitignore +238 -0
- ai_coc-0.2.1/CLAUDE.md +81 -0
- ai_coc-0.2.1/LICENSE +9 -0
- ai_coc-0.2.1/PKG-INFO +226 -0
- ai_coc-0.2.1/README.md +206 -0
- ai_coc-0.2.1/README.zh-CN.md +206 -0
- ai_coc-0.2.1/README.zh-TW.md +206 -0
- ai_coc-0.2.1/pyproject.toml +548 -0
- ai_coc-0.2.1/src/ai_coc/__init__.py +6 -0
- ai_coc-0.2.1/src/ai_coc/adapters/__init__.py +0 -0
- ai_coc-0.2.1/src/ai_coc/adapters/adb.py +189 -0
- ai_coc-0.2.1/src/ai_coc/adapters/ai.py +185 -0
- ai_coc-0.2.1/src/ai_coc/adapters/database.py +200 -0
- ai_coc-0.2.1/src/ai_coc/adapters/mapping.py +30 -0
- ai_coc-0.2.1/src/ai_coc/adapters/mumu.py +259 -0
- ai_coc-0.2.1/src/ai_coc/adapters/secrets.py +55 -0
- ai_coc-0.2.1/src/ai_coc/battle_scripts/BH10_BABY_DRAGON_01.json +27 -0
- ai_coc-0.2.1/src/ai_coc/battle_scripts/MAIN_VILLAGE_FARM_AI_01.json +32 -0
- ai_coc-0.2.1/src/ai_coc/cli.py +48 -0
- ai_coc-0.2.1/src/ai_coc/constants.py +52 -0
- ai_coc-0.2.1/src/ai_coc/logging_setup.py +43 -0
- ai_coc-0.2.1/src/ai_coc/models.py +436 -0
- ai_coc-0.2.1/src/ai_coc/parsers/__init__.py +0 -0
- ai_coc-0.2.1/src/ai_coc/parsers/battle.py +10 -0
- ai_coc-0.2.1/src/ai_coc/parsers/village.py +34 -0
- ai_coc-0.2.1/src/ai_coc/ui/__init__.py +0 -0
- ai_coc-0.2.1/src/ai_coc/ui/main_window.py +1201 -0
- ai_coc-0.2.1/src/ai_coc/ui/render.py +96 -0
- ai_coc-0.2.1/src/ai_coc/ui/workers.py +77 -0
- ai_coc-0.2.1/tests/__init__.py +0 -0
- ai_coc-0.2.1/tests/test_ai.py +112 -0
- ai_coc-0.2.1/tests/test_core.py +139 -0
- ai_coc-0.2.1/tests/test_render.py +65 -0
- ai_coc-0.2.1/uv.lock +2978 -0
ai_coc-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
#.idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
|
|
204
|
+
# Ruff stuff:
|
|
205
|
+
.ruff_cache/
|
|
206
|
+
|
|
207
|
+
# PyPI configuration file
|
|
208
|
+
.pypirc
|
|
209
|
+
|
|
210
|
+
# Marimo
|
|
211
|
+
marimo/_static/
|
|
212
|
+
marimo/_lsp/
|
|
213
|
+
__marimo__/
|
|
214
|
+
|
|
215
|
+
# Streamlit
|
|
216
|
+
.streamlit/secrets.toml
|
|
217
|
+
|
|
218
|
+
/data/
|
|
219
|
+
/docs/
|
|
220
|
+
TODO.md
|
|
221
|
+
|
|
222
|
+
# Secrets and local configuration
|
|
223
|
+
.env.*
|
|
224
|
+
!.env.example
|
|
225
|
+
*.key
|
|
226
|
+
*.pem
|
|
227
|
+
*.p12
|
|
228
|
+
secrets/
|
|
229
|
+
config/settings.json
|
|
230
|
+
|
|
231
|
+
# Private runtime/account data. COC_LIVE_TEST_SCREENSHOT and COC_AGENT_SCREENSHOT
|
|
232
|
+
# write frames of a live account wherever they are pointed.
|
|
233
|
+
data/accounts/
|
|
234
|
+
data/runtime/
|
|
235
|
+
screenshots/
|
|
236
|
+
captures/
|
|
237
|
+
adbkey
|
|
238
|
+
adbkey.pub
|
ai_coc-0.2.1/CLAUDE.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A Windows-only PyQt5 desktop application that drives Clash of Clans running inside MuMu Player 12. Gemini does the semantic screen reading; the app turns its answers into ADB taps and verifies the outcome on the next screenshot. Live battle tactics are deliberately out of scope: `battle.py` validates army requirements and stops at a reserved `RESERVED_RL` handoff boundary.
|
|
8
|
+
|
|
9
|
+
## Development flow
|
|
10
|
+
|
|
11
|
+
Read `gh-dev-flow` before starting; it owns the path from a task landing to the change being merged. In this repo that path always ends the same way: open the PR as a draft, run `code-review` over the branch and fix what holds up, then merge once CI is green. Merging on green needs no further approval.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync --group test # the suite needs the `test` group
|
|
17
|
+
uv run pytest # full suite (xdist, coverage gate, JUnit/XML into .github/reports)
|
|
18
|
+
uv run pytest tests/test_core.py::CoreTests::test_battle_script_requires_army # single test
|
|
19
|
+
make fmt # pre-commit: ruff, mdformat, codespell, ty, gitleaks, uv-sync/lock
|
|
20
|
+
make gen-docs # rebuild docs/ from the READMEs and the source
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run the app with `uv run ai_coc`. Two CLI hooks exist for smoke tests: `--live-test` captures a frame and asks Gemini to describe it, and `--agent-command=<text>` types a command into the AI tab and executes it. Both save a proof screenshot of the window when `COC_LIVE_TEST_SCREENSHOT` / `COC_AGENT_SCREENSHOT` point at a path.
|
|
24
|
+
|
|
25
|
+
Distributables are built in CI only: pushing a `v*` tag runs `build_release.yml`, which publishes the wheel to PyPI and attaches a PyInstaller Windows build to the release. There is no local build script.
|
|
26
|
+
|
|
27
|
+
That build defaults to `--onedir`, and the dispatch form's `package_mode` switches it to `--onefile`. Measured here, onefile costs 6.6-7.0 s to reach the first log line against onedir's 1.4-3.1 s, because it unpacks the whole bundle into a temp directory on every launch. Onefile is one 76 MB file where onedir is a 174 MB folder, so it stays available, just not the default.
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
`src/ai_coc/cli.py` is the entry point behind both console scripts (`ai_coc` and `cli`) and the PyInstaller build. PyInstaller runs it as `__main__`, so its imports stay absolute even for same-layer modules and it keeps an `if __name__ == "__main__"` block.
|
|
32
|
+
|
|
33
|
+
The three layers are directories, so an import that crosses them is visible in the import line:
|
|
34
|
+
|
|
35
|
+
- **UI and orchestration** — `ui/main_window.py` (`MainWindow`), all seven tabs plus every workflow; `ui/workers.py` (thread-pool workers and the log handler); `ui/render.py` (Markdown and log records to HTML). `cli.py` is only `main()`.
|
|
36
|
+
- **Adapters** — `adapters/mumu.py` (emulator lifecycle), `adapters/adb.py` (every ADB call), `adapters/ai.py` (Gemini), `adapters/secrets.py` (DPAPI), `adapters/database.py` (SQLite).
|
|
37
|
+
- **Pure parsers** — `parsers/village.py`, `parsers/battle.py`.
|
|
38
|
+
- **Data shapes** — `models.py` at the package root holds every Pydantic model in the project; see the Pydantic rule below.
|
|
39
|
+
|
|
40
|
+
Imports across layers are absolute (`from ai_coc.models import …`) and within a layer relative (`from .adb import …`); Ruff's `TID252` enforces it and rewrites the rest.
|
|
41
|
+
|
|
42
|
+
**Threading.** Every blocking call goes through `MainWindow.run_async`, which wraps the callable in a `Worker` (`QRunnable`) on the global `QThreadPool` and delivers the result back to the UI thread via `pyqtSignal`. A call that produces text as it goes uses `MainWindow.run_stream` and a `StreamWorker` instead: it drains a generator and emits each chunk. Never call `MuMuAdapter` or `GeminiClient` directly from a slot.
|
|
43
|
+
|
|
44
|
+
**The AI 助手 transcript is a model, not a text buffer.** `MainWindow.chat` is a `ChatTranscript`; `_say` appends a `ChatMessage` and `_paint_chat` re-renders the whole transcript into the `QTextBrowser` through `ui/render.py`. Bodies are Markdown, rendered by `markdown-it-py` with raw HTML disabled, and styled by `CHAT_STYLESHEET` (Qt honours only a subset of CSS 2.1). A streamed reply grows the last message in place, and repaints are throttled by the `chat_repaint` timer rather than fired per token.
|
|
45
|
+
|
|
46
|
+
**Logging.** `configure_logging()` in `main()` sets up a rotating plain-text file at `~/.ai_coc\logs\controller.log` plus a `rich` stderr console; `MainWindow._attach_log_panel` adds a handler that renders every record through `rich` and mirrors the resulting HTML into the 執行紀錄 panel under the tabs, and the panel's level selector retargets the root logger at runtime (DEBUG adds full prompts and replies). The file handler stays plain text so the log can still be grepped. `Worker.run` logs the traceback before the message box, and `sys.excepthook` catches what Qt would otherwise swallow. New adapter code is expected to log its own decisions; a feature that fails silently is the bug being fixed here.
|
|
47
|
+
|
|
48
|
+
**Agent loop** (`execute_agent_command`): ensure CoC is running, then loop up to `max_steps` (8 normally, 25 when the command mentions 進攻/戰鬥/搜尋資源村). Each step captures a screenshot plus a `uiautomator dump`, sends both to Gemini and reads back an `AgentAction` through structured output, applies it, sleeps 2 s and re-observes. Anything outside `tap|back|swipe_up|swipe_down` ends the task, so a new action verb needs a `Literal` member on `AgentAction`, a branch in `_apply_agent_action` and a mention in the prompt.
|
|
49
|
+
|
|
50
|
+
**Authorization is prompt-level only.** The automation-tab checkboxes are interpolated into the prompt as 自主升級/刷牆/自主進攻 flags, and the prompt forbids gems, cash, deletion and account operations. Nothing below the prompt enforces this. Treat any new capability that spends resources as needing its own guard in code.
|
|
51
|
+
|
|
52
|
+
**Task durability.** Every agent command becomes a row in `tasks`. A task left `PENDING` is picked up again by `resume_pending_tasks` shortly after startup and after each completion, and `automation_cycle` refuses to queue new work while one is running.
|
|
53
|
+
|
|
54
|
+
**Coordinates are hard-coded to 1600x900.** Gemini returns percentages, and `_apply_agent_action` converts them with `x_pct * 16, y_pct * 9`. Any other instance resolution taps the wrong place. Related known quirk: ADB `wm size` reports 900x1600 while the screenshot arrives as 1600x900.
|
|
55
|
+
|
|
56
|
+
**`MuMuAdapter`** uses `mumu-cli.exe` (JSON output) for discovery and lifecycle only; every capture and input goes through `adb.py`. Two rules were paid for in bugs, do not undo them:
|
|
57
|
+
|
|
58
|
+
- `_clean_environment` strips `QT_PLUGIN_PATH` and friends from every subprocess, because the PyInstaller bundle's Qt plugin paths break mumu-cli, which is itself a Qt application.
|
|
59
|
+
- `ensure_coc` escalates launch → poll → restart instance, because MuMu reports Android ready before a `monkey` launch will reliably stick.
|
|
60
|
+
|
|
61
|
+
**`adb.py` owns ADB.** `AdbController` wraps `adbutils`, one instance per serial, and `MuMuAdapter.controller()` caches them — that is how several emulators coexist, since each MuMu instance publishes its own port (`AdbEndpoint`, port 0 meaning not listening yet). `use_adb_executable` points `ADBUTILS_ADB_PATH` at MuMu's own `nx_main\adb.exe` so the adb server matches the emulator. Do not shell out to `adb.exe` from anywhere else.
|
|
62
|
+
|
|
63
|
+
**Every capture and every input names a display.** MuMu runs several Android displays and opens the game on one of its own, leaving display 0 on the emulator's launcher. Unqualified, `screencap -p` prefixes the PNG with a multi-display warning and the decode fails, while `input tap` silently lands on the launcher. `AdbController.display_for(package)` finds the right one and returns a `DisplayTarget`; `screencap -d` takes its physical id and `input -d` its logical id, because MuMu numbers the two schemes apart. Do not drop the `-d` flags.
|
|
64
|
+
|
|
65
|
+
**`GeminiClient`** is the only place the app talks to Gemini, through the `google-genai` SDK's Interactions API. Every request body is a `GeminiRequest`, never a hand-built dict. `generate` returns the whole text; `stream` sets `stream=True` and yields the `step.delta` text events, which is what the chat and the screen analysis use; `generate_structured(prompt, SomeModel, png)` sets `response_format` from the model's JSON schema and returns the validated instance, so no code strips ```` ```json ```` fences by hand. Structured output cannot stream, so the agent loop stays on `generate_structured`. `list_text_models` feeds the Model picker in Settings and only appears after 測試連線 succeeds. The default model is `DEFAULT_GEMINI_MODEL` in `constants.py`.
|
|
66
|
+
|
|
67
|
+
**Storage** lives in `~/.ai_coc\`: `controller.sqlite3`, `frames/`, `account_json/` and the DPAPI-protected `gemini.key.dpapi`. The schema is created idempotently in `Database._initialize` with no migration tooling, so a changed table means bumping `SCHEMA_VERSION` and handling existing databases yourself. The API key never goes into `QSettings`, only into the DPAPI file.
|
|
68
|
+
|
|
69
|
+
**Village and Battle Script parsing is deliberately tolerant.** Unknown sections and fields are preserved verbatim, unknown `data_id`s are queued into `unknown_entities` instead of failing the import. Game updates add IDs; keep it tolerant.
|
|
70
|
+
|
|
71
|
+
## Project rules
|
|
72
|
+
|
|
73
|
+
- **Every structured value is a Pydantic `BaseModel`, no exceptions.** New shapes go in `models.py`: emulator and CLI payloads, database rows, parsed files, AI replies, requests, settings and the chat transcript. The adapters are models too (`Database`, `SecretStore`, `AdbController`, `MuMuAdapter`, `GeminiClient`), so they take keyword arguments and put their non-field state in `PrivateAttr`. Only Qt subclasses and the ctypes `DATA_BLOB` stay plain classes, because their base class rules it out. Do not introduce a `dataclass`, a `TypedDict`, or a bare `dict[str, Any]` that travels between functions, and do not read a value out with `.get("key")` when a model could have declared the field — historical key names belong in `AliasChoices`, not in an `or` chain. Parse at the boundary with `model_validate` / `model_validate_json`, and write JSON out with `model_dump_json()` / `model_dump()` rather than `json.dumps` over a hand-built dict; a bare list on its way into a prompt gets a `RootModel` (`UiElementList`, `AccountRowList`). Ask Gemini for structure through `GeminiClient.generate_structured` and a model, never by parsing the text yourself. Models mirroring an external format that gains fields between releases carry `model_config = TOLERANT` so unknown keys survive.
|
|
74
|
+
- **A model field's type must be importable at runtime.** `[tool.ruff.lint.flake8-type-checking] runtime-evaluated-base-classes` keeps `TC003` from moving those imports into `if TYPE_CHECKING`, which would leave the model unbuildable. If a new model base class appears, add it there.
|
|
75
|
+
- **Never invent master data.** `entity_levels` (costs, times, requirements) stays empty until a value is source-backed, and the UI shows `—` rather than a guess. This is a stated product decision, not an oversight.
|
|
76
|
+
- **`docs/` is generated and gitignored**, rebuilt from the three READMEs and the source by `make gen-docs`. Edit the READMEs and the docstrings, never the generated output.
|
|
77
|
+
- **UI strings, prompts and user-facing messages are Traditional Chinese**; code, comments, commit messages and anything published to GitHub are English.
|
|
78
|
+
- **Deliberate deviations from the repo template**, documented in `pyproject.toml` comments: coverage gate is `--cov-fail-under=12` because almost everything is the untested PyQt shell; `[tool.ty.environment] python-platform = "win32"` is required or `winreg`/`ctypes.windll` fail to resolve on Linux CI runners; ty excludes `cli.py`, `ui/main_window.py` and `ui/workers.py` because PyQt5 ships inaccurate stubs; `allowed-confusables` carries `/` and `?` for the Chinese UI strings; the `build_release.yml` matrix is Windows-only because nothing here runs elsewhere.
|
|
79
|
+
- **The version is never written down.** `constants.py` reads it from the installed package metadata, and CI derives that from the git tag through `dunamai`. The `0.1.0` in `pyproject.toml` is a placeholder CI overwrites; do not hand-edit a version anywhere else.
|
|
80
|
+
- **Package data lives inside the package.** `battle_scripts/` sits at `src/ai_coc/battle_scripts/` and is reached through `BATTLE_SCRIPT_DIR`, so a wheel install resolves it the same way a source checkout does. The PyInstaller step needs `--add-data` for it and `--copy-metadata` for the version lookup; both are in `build_release.yml`.
|
|
81
|
+
- CodeQL and dependency-review jobs are gated on `github.event.repository.visibility == 'public'` because this private repo has no GitHub Advanced Security.
|
ai_coc-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CoC AI Controller contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
ai_coc-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ai_coc
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: AI-driven Clash of Clans automation controller for MuMu Player on Windows
|
|
5
|
+
Project-URL: Homepage, https://mai0313.github.io/ai_coc
|
|
6
|
+
Project-URL: Repository, https://github.com/Mai0313/ai_coc
|
|
7
|
+
Author-email: Wei <mai@mai0313.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: adbutils>=2.10
|
|
12
|
+
Requires-Dist: defusedxml<1,>=0.7
|
|
13
|
+
Requires-Dist: google-genai>=2.3.0
|
|
14
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
15
|
+
Requires-Dist: pydantic>=2.9
|
|
16
|
+
Requires-Dist: pyqt5-qt5==5.15.2; sys_platform == 'win32'
|
|
17
|
+
Requires-Dist: pyqt5<6,>=5.15
|
|
18
|
+
Requires-Dist: rich>=13.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<div align="center" markdown="1">
|
|
22
|
+
|
|
23
|
+
# AI CoC
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/ai_coc/)
|
|
26
|
+
[](https://www.python.org/downloads/source/)
|
|
27
|
+
[](https://docs.astral.sh/uv/)
|
|
28
|
+
[](https://github.com/astral-sh/ruff)
|
|
29
|
+
[](https://github.com/astral-sh/ty)
|
|
30
|
+
[](https://docs.pydantic.dev/latest/contributing/#badges)
|
|
31
|
+
[](https://github.com/Mai0313/ai_coc/actions/workflows/test.yml)
|
|
32
|
+
[](https://github.com/Mai0313/ai_coc/actions/workflows/code-quality-check.yml)
|
|
33
|
+
[](https://deepwiki.com/Mai0313/ai_coc)
|
|
34
|
+
[](https://github.com/Mai0313/ai_coc/tree/main?tab=License-1-ov-file)
|
|
35
|
+
[](https://github.com/Mai0313/ai_coc/pulls)
|
|
36
|
+
[](https://github.com/Mai0313/ai_coc/graphs/contributors)
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
A Windows desktop application that drives Clash of Clans running inside MuMu Player 12. Gemini reads the screen; the app turns its answers into ADB taps and verifies the outcome on the next screenshot.
|
|
41
|
+
|
|
42
|
+
Other Languages: [English](README.md) | [繁體中文](README.zh-TW.md) | [简体中文](README.zh-CN.md)
|
|
43
|
+
|
|
44
|
+
## ✨ What it does
|
|
45
|
+
|
|
46
|
+
- Detects MuMu Player 12 instances, starts the emulator and launches the game
|
|
47
|
+
- Captures the screen over ADB and asks Gemini what is on it, with structured replies validated through Pydantic
|
|
48
|
+
- Runs an agent loop that taps, swipes and goes back, re-observing after every step
|
|
49
|
+
- Imports village JSON exports and battle scripts, keeping unknown fields and unknown `data_id`s instead of failing on them
|
|
50
|
+
- Persists every agent command as a task, so an interrupted run is picked up again on the next start
|
|
51
|
+
- Stores the Gemini API key through Windows DPAPI, never in plain settings
|
|
52
|
+
|
|
53
|
+
Live battle tactics are deliberately out of scope: battle scripts are validated for army requirements and stop at a reserved handoff boundary.
|
|
54
|
+
|
|
55
|
+
## 📋 Requirements
|
|
56
|
+
|
|
57
|
+
- Windows. The app talks to `mumu-cli.exe`, reads the registry through `winreg` and calls DPAPI through `ctypes.windll`, none of which exist elsewhere
|
|
58
|
+
- [MuMu Player 12](https://www.mumuplayer.com/) with Clash of Clans installed, running at 1600x900
|
|
59
|
+
- A Gemini API key, entered in the app's settings tab
|
|
60
|
+
|
|
61
|
+
## 🚀 Install and run
|
|
62
|
+
|
|
63
|
+
From PyPI, without installing anything permanently:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uvx ai_coc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or as a regular install:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv tool install ai_coc
|
|
73
|
+
ai_coc
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Prebuilt Windows executables are attached to every [release](https://github.com/Mai0313/ai_coc/releases).
|
|
77
|
+
|
|
78
|
+
## 🛠️ Local development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/Mai0313/ai_coc.git
|
|
82
|
+
cd ai_coc
|
|
83
|
+
uv sync --group test # install dependencies
|
|
84
|
+
uvx pre-commit install # install git hooks
|
|
85
|
+
uv run ai_coc # start the app
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Two command-line hooks exist for smoke tests. `--live-test` captures a frame and asks Gemini to describe it; `--agent-command=<text>` types a command into the AI tab and runs it. Both save a proof screenshot when `COC_LIVE_TEST_SCREENSHOT` / `COC_AGENT_SCREENSHOT` point at a path.
|
|
89
|
+
|
|
90
|
+
## 🧰 Commands Reference
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Development
|
|
94
|
+
make help # List available make targets
|
|
95
|
+
make clean # Clean caches, artifacts and generated docs
|
|
96
|
+
make fmt # Run all pre-commit hooks
|
|
97
|
+
make test # Run pytest across the repository
|
|
98
|
+
make gen-docs # Generate docs from src/ and scripts/
|
|
99
|
+
|
|
100
|
+
# Dependencies (via uv)
|
|
101
|
+
make uv-install # Install uv on your system
|
|
102
|
+
uv add <pkg> # Add production dependency
|
|
103
|
+
uv add <pkg> --dev # Add development dependency
|
|
104
|
+
# Sync optional groups
|
|
105
|
+
uv sync --group dev # Install dev-only deps (pre-commit, poe, notebook)
|
|
106
|
+
uv sync --group test # Install test-only deps
|
|
107
|
+
uv sync --group docs # Install docs-only deps
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 🧱 Architecture
|
|
111
|
+
|
|
112
|
+
The three layers are directories, so an import that crosses them is visible in the import line:
|
|
113
|
+
|
|
114
|
+
- **UI and orchestration** — `ui/main_window.py` holds the window and every workflow, `ui/workers.py` the thread-pool workers, `ui/render.py` the Markdown and log rendering. `cli.py` is only `main()`
|
|
115
|
+
- **Adapters** — `adapters/mumu.py` (emulator lifecycle), `adapters/adb.py` (every ADB call), `adapters/ai.py` (Gemini), `adapters/secrets.py` (DPAPI), `adapters/database.py` (SQLite)
|
|
116
|
+
- **Pure parsers** — `parsers/village.py`, `parsers/battle.py`
|
|
117
|
+
|
|
118
|
+
Every structured value is a Pydantic model, collected in `models.py`. Blocking calls go through a `QThreadPool` worker and come back to the UI thread as a signal.
|
|
119
|
+
|
|
120
|
+
Application state lives in `~/.ai_coc`: the SQLite database, captured frames, imported account JSON and the DPAPI-protected key file.
|
|
121
|
+
|
|
122
|
+
## 📚 Documentation
|
|
123
|
+
|
|
124
|
+
Documentation is built with [Zensical](https://zensical.org/) and auto-generated from source code via `scripts/gen_docs.py`.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv sync --group docs
|
|
128
|
+
make gen-docs # generate markdown from source
|
|
129
|
+
uv run zensical serve # http://0.0.0.0:9987
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`make gen-docs` recreates `docs/`, copies the three READMEs in, then runs `gen_docs.py` over `./src` and `./scripts`.
|
|
133
|
+
|
|
134
|
+
## 📦 Packaging and Distribution
|
|
135
|
+
|
|
136
|
+
Build artifacts with uv (wheel and sdist go to `dist/`):
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv build
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Publish to PyPI (requires `UV_PUBLISH_TOKEN`):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
UV_PUBLISH_TOKEN=... uv publish
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Pushing a `v*` tag runs `build_release.yml`, which derives the version from git via `dunamai`, builds the wheel and sdist, publishes to PyPI, packages a Windows build with PyInstaller and attaches everything to the GitHub Release.
|
|
149
|
+
|
|
150
|
+
That build is `--onedir` by default: the zip holds the executable next to an `_internal/` folder, which starts several seconds faster than a single-file build that has to unpack itself on every launch. Running the workflow by hand offers a `package_mode` choice if you want the single `.exe` instead.
|
|
151
|
+
|
|
152
|
+
## 🧭 Optional task runner (Poe the Poet)
|
|
153
|
+
|
|
154
|
+
Convenience tasks are defined under `[tool.poe.tasks]` in `pyproject.toml` and available after installing the dev group (`uv sync --group dev`) or via `uvx`:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
uv run poe docs # generate + serve docs (requires dev group)
|
|
158
|
+
uv run poe gen # generate + deploy docs (gh-deploy) (requires dev group)
|
|
159
|
+
uv run poe main # run the app (same as uv run ai_coc)
|
|
160
|
+
|
|
161
|
+
# or ephemeral via uvx (no local install)
|
|
162
|
+
uvx poe docs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 🔁 CI/CD Actions Overview
|
|
166
|
+
|
|
167
|
+
All workflows live in `.github/workflows/`.
|
|
168
|
+
|
|
169
|
+
- Tests (`test.yml`)
|
|
170
|
+
|
|
171
|
+
- Trigger: pushes and pull requests to `main` or `release/*` (ignores md files)
|
|
172
|
+
- Runs pytest on Python 3.12/3.13/3.14 with coverage and comments a summary
|
|
173
|
+
|
|
174
|
+
- Code Quality Check (`code-quality-check.yml`)
|
|
175
|
+
|
|
176
|
+
- Trigger: pull requests
|
|
177
|
+
- Runs ruff and the rest of the pre-commit suite
|
|
178
|
+
|
|
179
|
+
- Docs Deploy (`deploy.yml`)
|
|
180
|
+
|
|
181
|
+
- Trigger: push to `main` and tags `v*`
|
|
182
|
+
- Builds the `zensical` site and publishes to GitHub Pages
|
|
183
|
+
- Setup needed: enable GitHub Pages for the repo (Settings → Pages → Source: GitHub Actions)
|
|
184
|
+
|
|
185
|
+
- Build and Release (`build_release.yml`)
|
|
186
|
+
|
|
187
|
+
- Trigger: tags `v*` push or manual workflow dispatch
|
|
188
|
+
- Builds a Windows x64 executable with PyInstaller, plus the wheel and sdist
|
|
189
|
+
- Publishes to PyPI (requires the `UV_PUBLISH_TOKEN` secret) and uploads every artifact to the GitHub Release
|
|
190
|
+
|
|
191
|
+
- Publish Docker Image (`build_image.yml`)
|
|
192
|
+
|
|
193
|
+
- Trigger: push to `main` and tags `v*`
|
|
194
|
+
- Builds and pushes an image to GHCR: `ghcr.io/<owner>/<repo>`
|
|
195
|
+
|
|
196
|
+
- Release Drafter (`release_drafter.yml`)
|
|
197
|
+
|
|
198
|
+
- Trigger: push to `main` and PR events
|
|
199
|
+
- Maintains a draft release based on Conventional Commits
|
|
200
|
+
|
|
201
|
+
- Code Scanning (`code_scan.yml`)
|
|
202
|
+
|
|
203
|
+
- Trigger: push and PR
|
|
204
|
+
- Runs gitleaks; the CodeQL job needs GitHub Advanced Security and stays skipped while the repo is private
|
|
205
|
+
|
|
206
|
+
- Semantic Pull Request (`semantic-pull-request.yml`)
|
|
207
|
+
|
|
208
|
+
- Trigger: PR open/edit/sync
|
|
209
|
+
- Enforces Conventional Commit style PR titles
|
|
210
|
+
|
|
211
|
+
### CI/CD Configuration Checklist
|
|
212
|
+
|
|
213
|
+
- Conventional commits for PR titles (enforced by the workflow)
|
|
214
|
+
- Set the `UV_PUBLISH_TOKEN` secret to publish to PyPI (Settings → Secrets and variables → Actions)
|
|
215
|
+
- Optional: enable GitHub Pages for docs deployment (Settings → Pages → Source: GitHub Actions)
|
|
216
|
+
- Container Registry permissions are handled automatically via `GITHUB_TOKEN`
|
|
217
|
+
|
|
218
|
+
## 🤝 Contributing
|
|
219
|
+
|
|
220
|
+
- Open issues/PRs
|
|
221
|
+
- Follow the coding style (ruff, type hints)
|
|
222
|
+
- Use Conventional Commit messages and descriptive PR titles
|
|
223
|
+
|
|
224
|
+
## 📄 License
|
|
225
|
+
|
|
226
|
+
MIT — see `LICENSE`.
|