# LDPlayer macro loop: bucles, parada y programación

> Monta un LDPlayer macro loop o uno en BlueStacks con condiciones de parada, guarda el progreso entre ejecuciones, sal con un código y prográmalo a diario.

Source: https://automationmacro.com/es/docs/guides/loops-and-scheduling (Guías, updated 2026-09-05)

Una macro que se ejecuta sin vigilancia necesita tres cosas: un bucle que sepa cuándo parar, una forma de recordar hasta dónde llegó y un horario que la arranque sin ti. Esta guía cubre las tres para un LDPlayer macro loop, o uno en BlueStacks, en Macro Automation Studio (MAS). Tienes los patrones de bucle, las condiciones de parada, el almacenamiento entre ejecuciones, los códigos de salida que los webhooks pueden notificar y el Programador. Está pensada para quien lleva una macro de "hago clic en Ejecutar" a "se ejecuta todas las mañanas".

## Antes de empezar

- MAS está instalado y con sesión iniciada. Es una [descarga gratuita](/download) para la prueba.
- Un proyecto basado en código se ejecuta en un emulador desde el Code Editor. Consulta [Primeros pasos](/docs/getting-started).
- El emulador está añadido como dispositivo con un puerto que puedes ver en su tarjeta. Consulta [Dispositivos](/docs/devices).
- Tus plantillas y regiones están capturadas. Consulta la [guía de reconocimiento de imagen](/docs/guides/image-recognition-macros).

## Patrones de LDPlayer macro loop y de BlueStacks

Tres formas cubren casi todas las macros.

**While, con una condición de parada.** El bucle se ejecuta hasta que algo en pantalla o en tus contadores dice que pare.

```python
import mas

images = mas.images({"collect": 401, "done": 402})

collected = 0
while collected < 50:
    if mas.find_object(images.done):
        break
    button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
    if button is None:
        continue
    mas.click(button.x, button.y, delay_ms=1000)
    collected += 1
```

**For con un número fijo.** Sabes cuántas veces hay que hacerlo.

```python
for round_no in range(1, 11):
    mas.log(f"Round {round_no} of 10")
    button = mas.find_object_retry(images.collect)
    if button is None:
        mas.log("Nothing to collect, stopping early", level="warning")
        break
    mas.click(button.x, button.y)
```

**Hasta que aparezca una plantilla.** Una espera con plazo, usada en pantallas de carga y temporizadores largos.

```python
import time

def wait_for(image, timeout_s=120, every_s=3.0):
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        match = mas.find_object(image)
        if match:
            return match
        time.sleep(every_s)
    return None
```

Todos los bucles de arriba tienen una salida. `find_object` devuelve `None` cuando falla y nunca lanza una excepción, así que una plantilla ausente por sí sola no puede terminar un `while True`. Ponle un límite a cada bucle.

## Condiciones de parada de un macro loop

Combina al menos dos. Una te protege del juego y la otra de tu propio error.

- **Un contador.** `while collected < 50`. Barato y predecible.
- **Un presupuesto de tiempo.** `time.monotonic()` cuenta segundos y nunca salta cuando cambia el reloj. Úsalo en el bucle exterior para que una ejecución programada termine siempre antes de que empiece la siguiente.
- **Un estado de pantalla.** Una plantilla como "sin energía", "inventario lleno" o un mensaje de límite diario. Captúrala en Asset Lab como cualquier otra plantilla y compruébala al principio de cada pasada.
- **Fallos consecutivos.** Cuenta cuántas pasadas seguidas no encontraron nada y ríndete a las cinco. Una pantalla que no habías previsto se ve exactamente así.

```python
started = time.monotonic()
misses = 0
while time.monotonic() - started < 20 * 60:
    if mas.find_object(images.out_of_energy):
        break
    button = mas.find_object_retry(images.collect)
    if button is None:
        misses += 1
        if misses >= 5:
            break
        continue
    misses = 0
    mas.click(button.x, button.y)
```

## Pausas y tiempos humanizados

`click` ya espera `delay_ms=1000` tras el toque y `swipe` dura `duration_ms=1000` por defecto. Entre pasadas, añade una pausa que varíe.

```python
import random

def pause(low=0.8, high=2.5):
    time.sleep(random.uniform(low, high))
```

Los intervalos fijos parecen mecánicos y además compiten con el juego. Un aviso que aparece 900 ms después de un toque se cuela por delante de un retardo fijo de 1000 ms en un día lento. Una pausa aleatoria suaviza ambas cosas. Mantén `delay_ms` en `click` para el tiempo de reacción de la app y usa `pause()` para el ritmo humano.

## Guardar el progreso con el almacenamiento

Una ejecución programada que se detiene a medias debería retomar donde lo dejó la próxima vez. Las funciones de almacenamiento guardan un pequeño documento JSON por tarea, indexado por este ordenador, el puerto del dispositivo y un nombre de tarea que eliges tú.

```python
TASK = "daily_collect"

state = mas.retrieve(TASK)          # {} on the first run
collected = state.get("collected", 0)
last_day = state.get("day")

mas.save(TASK, {"collected": collected, "day": today})   # after each pass of the loop

if collected >= 50:
    mas.clear(TASK)                 # start fresh next time
```

`save(task_name, data)` escribe o sustituye la entrada; `retrieve(task_name)` devuelve el diccionario o uno vacío; `clear(task_name)` la borra. `data` debe ser serializable a JSON. Como el puerto forma parte de la clave, dos instancias de emulador que ejecutan la misma macro nunca comparten un contador. `retrieve_all(task_name)` devuelve la entrada de cada instancia cuando quieres un total. Consulta [Almacenamiento](/docs/sdk/storage).

> [!TIP]
> Guarda la fecha junto al contador. Una macro diaria puede así distinguir una ejecución reanudada de un día nuevo y reiniciarse.

## Salir con un código para que los webhooks informen del estado

MAS lee el código de salida del proceso cuando tu script termina. Cero significa que la ejecución se completó; cualquier otro valor la marca como fallida. Los suscriptores de webhooks reciben `macro.completed` o `macro.failed` según corresponda, con el `exit_code` en el payload.

```python
import sys

if energy is None:
    mas.log("Could not read energy", level="error")
    sys.exit(2)      # macro.failed, exit_code 2

mas.log("All done")
sys.exit(0)          # macro.completed
```

Una excepción no controlada también sale con un valor distinto de cero, así que un fallo se notifica como error sin código adicional. Para eventos que solo tu script conoce, `mas.webhook("level.reached", {"level": 40})` envía un evento `custom.level.reached` a los endpoints suscritos a eventos personalizados. Configura los endpoints en la página **Webhooks**; consulta [Webhooks](/docs/webhooks).

## Un ejemplo completo

La macro recoge hasta cincuenta recompensas al día, recuerda su recuento, se detiene con "sin energía" o tras veinte minutos, e informa mediante su código de salida. Sustituye los ID por los tuyos.

```python
import datetime
import random
import sys
import time

import mas

images = mas.images({
    "collect": 401,
    "close_popup": 402,
    "out_of_energy": 403,
})

TASK = "daily_collect"
DAILY_LIMIT = 50
TIME_BUDGET_S = 20 * 60


def pause(low=0.8, high=2.5):
    time.sleep(random.uniform(low, high))


def main():
    today = datetime.date.today().isoformat()
    state = mas.retrieve(TASK)
    collected = state.get("collected", 0) if state.get("day") == today else 0
    mas.log(f"{today}: starting at {collected} of {DAILY_LIMIT}")

    started = time.monotonic()
    misses = 0
    while collected < DAILY_LIMIT:
        if time.monotonic() - started > TIME_BUDGET_S:
            mas.log("Time budget reached", level="warning")
            break
        popup = mas.find_object(images.close_popup)
        if popup:
            mas.click(popup.x, popup.y, delay_ms=800)
            continue
        if mas.find_object(images.out_of_energy):
            mas.log("Out of energy")
            break
        button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
        if button is None:
            misses += 1
            if misses >= 5:
                mas.log("Five misses in a row, giving up", level="error")
                mas.save(TASK, {"collected": collected, "day": today})
                sys.exit(2)
            continue
        misses = 0
        mas.click(button.x, button.y, delay_ms=1000)
        collected += 1
        mas.save(TASK, {"collected": collected, "day": today})
        pause()

    mas.log(f"Finished at {collected} of {DAILY_LIMIT}")
    sys.exit(0)


if __name__ == "__main__":
    main()
```

Ejecútala una vez desde el Code Editor con **Ejecutar** (<kbd>F5</kbd>) y confirma que termina sola antes de programarla.

## Programar la macro en el Programador de MAS en BlueStacks o LDPlayer

El Programador vive dentro de la app de escritorio y arranca macros en tus emuladores a las horas que fijes.

1. Abre **Programador** y haz clic en **Crear nueva programación**.
2. Escribe un **Nombre** (de 3 a 100 caracteres) y elige la **Macro**.
3. En **Puerto del emulador**, haz clic en **Escanear** y elige el puerto de la instancia, o escríbelo.
4. Fija la **Fecha** y la **Hora**. La hora es la hora local de tu ordenador.
5. Fija la **Recurrencia**: **Ninguna** para una sola ejecución, **Diaria**, **Semanal** con los **Días de la semana** marcados, o **Mensual**.
6. Deja el **Número de repeticiones** en 1 salvo que quieras que la macro se ejecute varias veces seguidas en cada disparo.
7. Deja **Activa** encendida y haz clic en **Crear programación**.

Lo que debes saber antes de confiar en él:

- La app debe seguir abierta y con sesión iniciada. El programador no es un servicio del sistema y no hay sintaxis cron.
- Las tareas se comprueban cada 30 segundos, así que una ejecución puede empezar hasta medio minuto después de su hora.
- Una programación apunta a un solo puerto. Si el puerto está ocupado con otra ejecución cuando llega la hora, el programador salta ese ciclo y lo intenta en el siguiente.
- Dos programaciones no pueden compartir una franja horaria en el mismo puerto; la app lo rechaza con "Time slot is occupied on this port".
- Pueden ejecutarse hasta 20 tareas programadas a la vez.
- Con un **Número de repeticiones** mayor que 1, una ejecución fallida (salida distinta de cero) detiene las repeticiones restantes y marca la tarea como Fallida.
- Las ejecuciones programadas usan los argumentos guardados en la tarjeta del dispositivo, o el perfil de ajustes que sigue el dispositivo. Consulta [Perfiles de ajustes](/docs/settings-profiles).
- **Ver historial** en una tarea muestra cada ejecución con su estado y sus logs.

La página [Programador](/docs/scheduler) cubre cómo editar, saltar y cancelar tareas.

## Ejecutarla en un grupo

Para ejecutar la misma macro en varias instancias a mano, ponlas en un grupo de dispositivos, asigna la macro a cada dispositivo y haz clic en **Iniciar todos**. Los dispositivos arrancan uno tras otro, con medio segundo de separación, y cada uno tiene su propia ejecución, sus logs y su código de salida. Para programar el grupo, crea una tarea programada por puerto con la misma macro y la misma hora. La [guía de multi instancia](/docs/guides/multi-instance) cubre los argumentos, los proxies y el almacenamiento por dispositivo.

## Qué puede salir mal

### El bucle nunca termina

Un `while True` sin límite, o una condición de parada que depende de una plantilla que nunca aparece. Añade un presupuesto de tiempo con `time.monotonic()` y un contador de fallos consecutivos; esos dos terminan cualquier bucle.

### La programación no se disparó

La app estaba cerrada o suspendida a esa hora, el puerto estaba ocupado o la tarea no está **Activa**. Revisa **Ver historial** en busca de una entrada saltada o fallida y mantén el ordenador despierto durante la ventana programada.

### Time slot is occupied on this port

Otra tarea en el mismo puerto ya tiene esa hora. Mueve una tarea unos minutos, o mete la segunda macro dentro de la primera con el **Número de repeticiones**.

### La ejecución aparece como Fallida pero la macro terminó

El script terminó con un código de salida distinto de cero o con una excepción no controlada después de su última acción. Lee las últimas líneas del log; un `sys.exit(1)` en una ruta de limpieza o un `KeyError` sobre el estado guardado son las causas habituales.

Ninguna herramienta de automatización está libre de riesgos al 100 %, así que automatiza con responsabilidad y bajo tu propio criterio.
