Orna bot: una solución de farmeo automático para un videojuego móvil, involucrando la teoría del color
No me gusta el farmeo lento e interminable en los juegos. ¿A vosotros sí? Este es un bot pequeño e inofensivo que hice para Orna, un juego similar a "Pokémon Go" pero con un toque de RPG.
El juego
Orna es un juego basado en GPS (como Pokémon Go) que se juega como un RPG clásico. Ofrece el gameplay habitual: explorar, matar monstruos, conseguir loot, subir de nivel, mejorar al personaje, elegir una clase… etc. De verdad, si lo probáis, veréis que da incontables horas de diversión tanto en la calle con los amigos como en casa uno solo.

El tema… ¿esto son trampas?
Bueno, sí… pero son inofensivas. Soy el primero que se opone a las trampas en los juegos, al uso de bots y cosas por el estilo, pero este es un caso un poco diferente. El juego es principalmente PvE (jugador contra entorno), con mecánicas PvP (jugador contra jugador) que son opcionales y no clasificatorias. Y prácticamente no hay “competición” en el PvE, por lo que mejorar al personaje jugando mucho (o automatizando el farmeo) no afecta a otros jugadores.
De cualquier forma, esta publicación no busca animar a nadie a intentar hacer trampas en este ni en ningún otro juego, es simplemente para hablar de cómo implementé una solución.
El problema, el farmeo eterno
Estaba estancado en un bucle: me encontraba en un nivel en el que, si jugaba desde casa, no podía hacer mucho ni avanzar demasiado. Matar monstruos era fácil y rápido, pero repetitivo. Y algo así suele picarme en el cerebro de cierta forma. Después de pasar un tiempo haciendo A, luego B, luego C… me dije a mí mismo… “¿y si automatizo esto?”
Así nació orna-bot v1.
Orna bot, la primera iteración, 2022.
Quería hacer algo rápido, fácil y feo: una especie de prueba de concepto y una forma de comprobar si podía hacerlo por mí mismo. Resultó ser un proyecto chulo y un poco loco, donde aprendí muchísimo. Sobre colores. Sí. No me quiero adelantar.
Antes que nada, la plataforma. No quería entrometerme demasiado con Android; un par de búsquedas rápidas en Google me hicieron darme cuenta de que, obviamente, Android no quiere que ninguna aplicación simule pulsaciones en la pantalla, así como así. Parecía que requería mucho trabajo lograrlo, y tuve otra idea (se me hizo bola, la verdad).
A veces, cuando jugaba desde casa, usaba scrcpy, un programa de Genymotion que permite al usuario conectar el teléfono a un ordenador mediante USB, ver la pantalla e interactuar con la ventana permitiendo los clics. Era la plataforma perfecta.
Con esto, podía usar Python para buscar la ventana, analizarla y enviar clics de forma fácil.
El plan era sencillo: mirar la pantalla, encontrar a los monstruos, hacer clic en uno, entrar en combate, matarlo, curarse y empezar de nuevo. Pero… ¿cómo?
Bienvenidos a: La teoría del color
Primero, había que buscar un monstruo. Sí, aparecen “más o menos” en el centro, “alrededor” de un área… pero al alejar la cámara lo suficiente como para ver toda la zona al mismo tiempo, los iconos de los monstruos se vuelven demasiado pequeños y podíais pasarlos por alto fácilmente… ¿entonces qué?

No os voy a mentir, me costó un tiempo darme cuenta de lo que mi cerebro intentaba decirme: puedes saber fácilmente dónde estaban los monstruos por su color. ¿Pero cómo? Le di unas vueltas, probé múltiples rangos de color… Tuve que aprender un poco más sobre colores para entender la respuesta: los colores no tenían nada de especial.
Lo especial era la saturación.
Sí, sí, es simple y trivial, pero recordad que soy un desarrollador de backend, no tengo ni la más remota idea de diseño, teoría del color, ni nada parecido.
Tan pronto como me di cuenta de esto, la solución era fácil: hacía una captura de pantalla de la ventana, recortaba únicamente la sección específica de la imagen donde aparecían los monstruos, obtenía el HSV of cada píxel y filtraba. No había necesidad de buscar el modelo exacto de un monstruo, su forma ni nada parecido. Bastaba con un grupo de píxeles correctos.
Podéis ver aquí la imagen base y los resultados que obtuve en una de mis primeras versiones (perdonad por la baja definición).

Una vez que refiné eso hasta el punto en que supe que solo tenía los píxeles donde estaban los monstruos, fue tan simple como hacer clic aleatoriamente en uno de ellos y entrar en combate.
El bucle: enfoque stateless
Con esa parte resuelta, tuve que hacer un par de comprobaciones: ¿estaba en la vista del mundo?, ¿qué estaba haciendo el juego?, ¿dónde podía hacer clic? Decidí que lo mejor para esta versión era hacer que el programa fuera agnóstico a los estados anteriores: en cada iteración del bucle, analizaba la pantalla, decidía dónde estaba y actuaba en consecuencia.
Por lo tanto, implementar la lógica fue bastante fácil.
- Si estaba en la pantalla principal, buscaba una pelea.
- Si estaba en una pelea, ¡peleaba!
- Si ganaba o perdía la pelea, regresaba (¡y me curaba si perdía!) Ya os hacíais la idea.
No quería predefinir las coordenadas de los botones hardcodeadas, así que para encontrar botones (como los de ataque) y ayudarme a definir dónde estábamos, utilicé OpenCV para buscar la imagen del botón en la pantalla.
La idea rudimentaria era: hacer una captura de pantalla, recortar la imagen alrededor de donde sabía que podía estar el botón que quería localizar e intentar encontrar dicha imagen en la pantalla. Aunque pudiera parecer un poco tonto, conocer de antemano dónde aparecerían los botones hizo que estas comprobaciones fueran bastante rápidas y eficientes.
Terminó implementando un mejor registro de logs, una vista de estadísticas en consola (con recuento de bajas, muertes y experiencia ganada), aleatoriedad para evitar que me descubrieran (probablemente estaba infringiendo los términos de servicio) y validación de casos especiales. Al final, fue un pequeño experimento divertido que funcionó sorprendentemente bien, pero que abandoné después de unos meses, ya que requería tener el ordenador encendido y el teléfono conectado para ejecutarse. Eso iba a cambiar en la v2.
Aquí podéis ver un vídeo que grabé para enseñarle a un amigo el bot en ejecución:
Orna bot 2, hagámoslo bien esta vez
En 2025, volví a jugar a Orna un poco y me acordé bien rápido de mi proyecto. Como ya conocía los problemas originales, decidí que abordaría el peor aspecto de la versión anterior: tener que tener el ordenador conectado al teléfono para ejecutar el script.
El principal reto ahora era cómo leer la pantalla y permitir que la aplicación simulara clics en Android. Tras trastear un poco, logré que funcionaran las primeras pruebas: utilizando el Servicio de Accesibilidad de Android, podía concederle acceso a la aplicación para hacer todas esas cosas. Siguió siendo un dolor de cabeza, pero creo que se debió más a mi inexperiencia en Android que a la implementación en sí.
Ups… esto ya es un backend
Este proyecto lo escribí en Kotlin, por lo que mis instintos de backend se activaron para implementar algo un poco más estándar y teniendo en cuenta las buenas prácticas. Ya sabéis: diferentes capas de arquitectura, una capa de dominio para la lógica, infraestructura para herramientas de terceros, inversión de dependencias… lo típico. Esta última fue muy útil al principio, ya que probé distintas formas de leer la pantalla; finalmente me decanté por Tesseract, leyendo texto en lugar de imágenes.
No iba a migrar la versión de Python a Android… quería hacer algo mejor y más robusto.
.
└── ornabot/
├── domain/
│ ├── screen/
│ │ ├── reader/
│ │ └── touch/
│ └── strategy/
├── infra/
│ └── screen/
│ ├── capture/
│ │ └── android
│ ├── reader
│ └── touch
├── ui/
│ └── theme
├── AutomationActivity.kt
├── MainActivity.kt
└── res/
Friendship with Stateless ended, now Strategy is my best buddy
No quería implementar una máquina stateless como en la primera iteración; quería algo un poco más refinado y estándar. Decidí implementar un patrón Strategy: cada clase que implementaba la Strategy principal ejecutaba una comprobación: ¿es esta mi pantalla? Si era así, ejecutaba sus acciones; si no, no hacía nada. Esta implementación facilitó la adición de otras estrategias sin modificar la capa de automatización.
interface Strategy {
fun matches(state: GameState): Boolean
fun execute()
}
Implementé una forma de luchar contra monstruos normales y otra para luchar en Raids (combates contra monstruos gigantes que podían durar fácilmente desde un par de minutos hasta 20, donde la estrategia de combate tenía que ser radicalmente distinta). También implementé… ¡pesca! Porque por qué no, era una mecánica chula y útil para encontrar botín.
class CombatStrategy(
private val touchService: TouchService,
private val ocrService: OCRService
)
Adopté un enfoque cercano a TDD para el código: tomaba un par de capturas de pantalla del juego y construía el código de la estrategia a su alrededor, añadiendo más y más casos para estar seguro de que, con la configuración correcta, la aplicación siempre supiera dónde estaba y cuáles eran las siguientes acciones.
Irónicamente, simular los toques en la pantalla no fue la parte difícil. Entender lo que mostraba el juego sí lo fue. Los colores seguían siendo útiles para localizar monstruos, pero una vez dentro de los menús y el combate, el reconocimiento de OCR con Tesseract resultó la herramienta más fiable a la hora de definir el estado del juego.
El resultado quedó chulo: ahora podía llevar el teléfono conmigo cuando salía de casa, guardarlo en el bolsillo y dejar que farmeara.
¿Lo hice? Un par de veces.
¿Pasé la mayor parte del tiempo mirando cómo mi bot funcionaba de forma autónoma sin ser capaz de apartar la mirada…?
