解放双手!麦当劳自动兑换code脚本

想必大家兑换码已经攒了不少了(不知道的往下看放在最后的引用),用AI写了个自动粘贴的脚本,解放双手!(虽然可能有点晚了,原谅一下拖延症晚期患者…

#!/usr/bin/env python
"""Automates entering McDonald's codes via UI automation."""
from __future__ import annotations

import argparse
import random
import sys
import time
from pathlib import Path
from typing import Dict, Iterable, List, Tuple

import pyautogui
import pyperclip

# Update these coordinates to match the buttons/fields in the "absurd interface" UI.
# Use `python -m pyautogui` in a separate terminal to read the live mouse position.
COORDINATES: Dict[str, Tuple[int, int]] = {
    "open_entry": (0, 0),      # TODO: replace with coordinates
    "add_code_manually": (0, 0),      # TODO: replace with coordinates
}

# Sequence of steps for each code. Edit this list to match the button flow you need.
STEPS: List[Dict[str, object]] = [
    {"action": "click", "target": "open_entry", "pause": 1.},
    {"action": "click", "target": "add_code_manually", "pause": 1.},
    {"action": "paste_code", "pause": 1.},
    # After pasting the code, press Enter to submit the input instead of
    # clicking submit/confirm buttons. Adjust pause as needed.
    {"action": "keypress", "key": "enter", "pause": 0.5},
]


def load_codes(path: Path, repeat: int) -> List[str]:
    """Read codes from file, trimming blanks, and ensure enough entries exist."""
    if not path.exists():
        raise FileNotFoundError(f"Codes file not found: {path}")

    codes = [line.strip() for line in path.read_text().splitlines() if line.strip()]
    if repeat > len(codes):
        raise ValueError(
            f"Requested {repeat} iterations but only {len(codes)} codes found in {path}"
        )
    return codes[:repeat]


def ensure_coordinates_are_set(keys: Iterable[str]) -> None:
    """Guard against running automation with placeholder coordinates."""
    unset = [key for key in keys if COORDINATES.get(key) in {(0, 0), None}]
    if unset:
        raise ValueError(
            "Update COORDINATES before running automation. Unset keys: " + ", ".join(unset)
        )


def adjusted_pause(base: float, jitter: float) -> float:
    """Return a randomized pause length around the base value."""
    if base <= 0 or jitter <= 0:
        return max(0.0, base)

    lower = max(0.0, base * (1.0 - jitter))
    upper = base * (1.0 + jitter)
    return random.uniform(lower, upper)


def click(target: str, pause: float, pixel_jitter: int = 0, dry_run: bool = False) -> None:
    """
    Click the target coordinate, optionally applying a pixel jitter.

    If dry_run is True the function will not move the mouse but will print the
    intended click location.
    """
    base_x, base_y = COORDINATES[target]
    if pixel_jitter and pixel_jitter > 0:
        jitter_x = random.randint(-pixel_jitter, pixel_jitter)
        jitter_y = random.randint(-pixel_jitter, pixel_jitter)
    else:
        jitter_x = jitter_y = 0

    x = base_x + jitter_x
    y = base_y + jitter_y

    if dry_run:
        print(f"[dry-run] click -> {target} @ ({base_x},{base_y}) + jitter({jitter_x},{jitter_y}) -> ({x},{y})")
    else:
        pyautogui.click(x=x, y=y)

    time.sleep(pause)


def paste_code(code: str, pause: float) -> None:
    pyperclip.copy(code)
    pyautogui.hotkey("command", "v")
    time.sleep(pause)


def run_step(
    step: Dict[str, object], code: str, dry_run: bool, pause_jitter: float, click_jitter: int
) -> None:
    action = step["action"]
    base_pause = float(step.get("pause", 0.2))
    pause = adjusted_pause(base_pause, pause_jitter)

    if action == "click":
        target = step["target"]
        if dry_run:
            # Show base coordinate and jitter that will be applied
            print(
                f"[dry-run] click -> {target} @ {COORDINATES[target]} (pause {pause:.2f}s, pixel_jitter={click_jitter})"
            )
        else:
            click(target, pause, pixel_jitter=click_jitter, dry_run=False)
    elif action == "paste_code":
        if dry_run:
            print(f"[dry-run] paste -> {code} (pause {pause:.2f}s)")
        else:
            paste_code(code, pause)
    elif action == "wait":
        duration = float(step["duration"])
        if dry_run:
            print(f"[dry-run] wait -> {duration}s")
        else:
            time.sleep(duration)
    elif action == "keypress":
        key = step["key"]
        if dry_run:
            print(f"[dry-run] keypress -> {key} (pause {pause:.2f}s)")
        else:
            pyautogui.press(key)
            time.sleep(pause)
    else:
        raise ValueError(f"Unsupported action: {action}")


def main() -> int:
    parser = argparse.ArgumentParser(description="Automate McDonald's code entry.")
    parser.add_argument(
        "-f",
        "--codes-file",
        type=Path,
        default=Path("codes.txt"),
        help="Path to text file containing one code per line.",
    )
    parser.add_argument(
        "-n",
        "--repeat",
        type=int,
        default=10,
        help="Number of codes to process.",
    )
    parser.add_argument(
        "--lead-time",
        type=float,
        default=5.0,
        help="Seconds to wait before starting so you can focus the target window.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print planned steps without moving the mouse or typing.",
    )
    parser.add_argument(
        "--pause-jitter",
        type=float,
        default=0.15,
        help="Fractional jitter applied to pauses (e.g. 0.15 => ±15%).",
    )
    parser.add_argument(
        "--click-jitter",
        type=int,
        default=10,
        help="Pixel jitter to apply to click coordinates (±N pixels).",
    )
    parser.add_argument(
        "--activate-target",
        type=str,
        default="open_entry",
        help="Coordinate key to click once before starting the loop to activate the window.",
    )
    parser.add_argument(
        "--no-activate",
        action="store_true",
        help="Do not perform the one-time activation click before the loop.",
    )
    parser.add_argument(
        "--post-wait",
        type=float,
        default=1.,
        help="Seconds to wait after processing each code (before next code).",
    )
    args = parser.parse_args()

    pyautogui.FAILSAFE = True  # Moving the mouse to a corner aborts the script.
    pyautogui.PAUSE = 0.05

    codes = load_codes(args.codes_file, args.repeat)
    ensure_coordinates_are_set(
        step["target"]
        for step in STEPS
        if step["action"] == "click"
    )

    # Ensure the activation target is also present (if activation is requested)
    if not args.no_activate:
        if args.activate_target not in COORDINATES:
            raise ValueError(
                f"Activation target '{args.activate_target}' is not present in COORDINATES"
            )

    if not args.dry_run:
        print(f"Starting in {args.lead_time:.1f} seconds. Move the mouse to a corner to abort.")
        time.sleep(args.lead_time)

    # One-time activation click (useful to focus the target window). This
    # happens only once before processing the codes. It respects click jitter.
    if not args.no_activate:
        if args.dry_run:
            print(f"[dry-run] one-time activate -> {args.activate_target} (pixel_jitter={args.click_jitter})")
        else:
            click(
                args.activate_target,
                adjusted_pause(0.2, args.pause_jitter),
                pixel_jitter=args.click_jitter,
            )

    pause_jitter = max(0.0, args.pause_jitter)

    click_jitter = max(0, args.click_jitter)

    for idx, code in enumerate(codes, start=1):
        print(f"Processing code {idx}/{len(codes)}: {code}")
        for step in STEPS:
            run_step(step, code, args.dry_run, pause_jitter, click_jitter)
        # Wait after each code to allow the UI to settle. Apply jitter.
        post_wait = adjusted_pause(max(0.0, args.post_wait), pause_jitter)
        if args.dry_run:
            print(f"[dry-run] post-code wait -> {post_wait:.2f}s")
        else:
            time.sleep(post_wait)

    print("Done.")
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:  # Catch-all to surface friendly errors.
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)

本帖仅在Mac测试过。Windows+安卓用到的工具完全相同,需要自行去配置下载。Windows+iOS感觉需要第三方工具不是很容易(可以用个安卓模拟器?)。

准备工作 (Prerequisites)

  1. 安装 Python:

    • 如果没装,可以通过 Homebrew (见下面)安装:brew install python
    • 然后安装脚本依赖:pip install pyperclip pyautogui
  2. 手机连接 Mac:

    • 确保手机屏幕能投屏到 Mac 上并可以操作。
    • 安卓: 需要 android-platform-toolsstrcpy。Mac可以通过brew安装,如果是Windows需要去链接的官网下载
      # 安装 Homebrew (如果还没有)
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
      # 安装工具
      brew install android-platform-tools strcpy
      
    • iPhone: 直接用系统自带的“屏幕镜像”功能。
  3. 开启辅助功能权限:

    • 打开 系统设置隐私与安全性辅助功能
    • 把你运行脚本的 App (比如 终端VS Code) 添加进去并打勾。

使用步骤

  1. 保存代码:

    • 将脚本代码保存为 main.py
    • main.py 同目录下,创建一个 codes.txt 文件,把你所有的兑换码放进去,每行一个。
  2. 获取屏幕坐标:

    • 在终端里运行 python -m pyautogui,这个工具会实时显示你鼠标的坐标。
    • 脚本需要点击两个位置,请依次把鼠标悬停在手机投屏画面的对应按钮上,记下 X, Y 坐标:1)“输入兑换码” 按钮 2)“手动输入兑换码” 按钮
    • 打开 main.py,修改 COORDINATES 字典里 open_entryadd_code_manually 的坐标值。(更好的方法是用OCR但我懒得搞了)
  3. 运行脚本:

    • 搞定以上步骤后,就可以运行了。比如,要自动用掉10个兑换码:
      python main.py -n 10
      
    • 脚本开始前有5秒倒计时。要中途停下,把鼠标移到屏幕角落就行。
      PS:如果有1password之类的密码填充工具,可能需要暂时关掉自动填充。

可选参数

脚本提供了一些微调选项,可以根据需要使用:

  • --lead-time: 开始前的等待时间 (默认5秒)。
  • --pause-jitter: 每步操作之间的随机延迟 (默认0.15)。
  • --post-wait: 每个码用完后的等待时间 (默认1秒)。
  • --click-jitter: 点击位置的随机偏移量 (默认10像素),模拟手抖。
  • --dry-run: 可以看到要进行的一系列操作

获取兑换码请参考:

13 个赞

等一个windows版

1 个赞

没windows啊,你可以测试一下

等一个自动四十台手机的汉堡送到家脚本

有没有自动帮吃脚本,咀嚼消化排泄一条龙

等一个自动售卖的脚本,抽奖到了就自动卖掉

等一个自动撸泥潭羊毛agent

1 个赞

我看了下windows安卓肯定可以,ios感觉需要一些第三方工具…可以用个安卓模拟器?

既然用电脑了为什么不直接adb

@echo off
setlocal enabledelayedexpansion

REM ===== 配置 =====
set "ADB_PATH=C:\portable apps\Android\platform-tools\adb.exe"
set "FILE=codes.txt"
set "SLEEP_SEC=2"
set "BATCH_SIZE=10"

:batch_loop
if not exist "%FILE%" (
  echo [ERROR] %FILE% not found.
  goto end
)

REM 读取前 BATCH_SIZE 行到数组 LINE[1..idx]
set "idx=0"
for /f "usebackq delims=" %%A in ("%FILE%") do (
  set /a idx+=1
  set "LINE[!idx!]=%%A"
  if !idx! geq %BATCH_SIZE% goto got_batch
)
:got_batch

if !idx! EQU 0 (
  echo [INFO] No more codes. Done.
  goto end
)

echo ================== NEW BATCH (size=!idx!) ==================
for /l %%i in (1,1,!idx!) do (
  set "CODE=!LINE[%%i]!"
  echo [%%i/!idx!] Executing code: !CODE!
  "%ADB_PATH%" shell am start -W -a android.intent.action.VIEW -d "mcdmobileapp://external_link?url=https://iwin-us-mcd-monopoly-prd25-api.tmsiwinapi.com/play/!CODE!" com.mcdonalds.app
  timeout /t %SLEEP_SEC% /nobreak >nul
)

echo ------------------------------------------------------------
echo 已执行本批 !idx! 条。是否刪除這 !idx! 行並繼續下一批?
choice /c YN /m "按 Y 刪除並繼續,按 N 取消並退出"
if errorlevel 2 (
  echo 已取消,未刪除。退出。
  goto end
)

REM 删除文件前 idx 行
more +!idx! "%FILE%" > "%FILE%.tmp"
move /y "%FILE%.tmp" "%FILE%" >nul
echo 已刪除本批 !idx! 行
echo.

:end
echo 完成。
exit /b



1 个赞

你这是抓包搞到的链接吧,这个脚本只是模拟点击(同时适用于iOS):joy:

感谢喂饭,唯一可惜还得手动进去一次刷新session

是的,第一次之前得从首页点一下进去

Mac的iPhone mirroring会弹一个相机不可用的错误,在open_entry之后加一个enter keypress就可以了。

good catch 对我其实后来更新了脚本,但没更新帖子里面的