Assuntos relacionados à programação, pedaços de códigos, pequenas dicas, pequenos tutoriais, alguns vídeos, algumas fotos e etc.

Trapaceando no Monkey Kick Off

Posted: maio 11th, 2009 | Author: Carlan Calazans | Tags: , , , , | No Comments »

Monkey Kick Off é um game em desenvolvido em flash pela Totebo Interactive. O objetivo do game é ajudar um macaco a chutar a bola o mais longe possível ou pelo menos até a vila dos macacos a 4000 metros de distância. Para chutar a bola basta clicar com o botão esquerdo do mouse ou pressionar qualquer tecla no teclado.

Monkey Kick Off

Monkey Kick Off

Simples, não? A quem diga que até um macaco consegue :)

Análise do problema

É necessário um pouco de tempo até perceber que existem algumas variantes para conseguir chutar a bola a uma certa distância. Primeiro, há um momento, randômico, em que o macaco consegue levantar a bola até a altura máxima. Outra variante importante é o momento do chute. É preciso entender que existe um momento certo para se dar o chute. Este ponto fica entre a altura da cabeça e barriga do macaco.

Como a altura máxima da bola é randômico, esperar pelo momento certo pode levar muito tempo. O mesmo vale para a hora do chute, mas este é menos complicado, pois, pode ser calculado medindo o tempo gasto para a bola chegar no momento do chute.

Há outros fatores que também devem ser levados em consideração quando estamos jogando. As cores do game prejudicam a visão, se ficarmos olhando durante muito tempo os marcadores (os coqueiros ou a placa indicando que a vila dos macacos fica a 4000m) no plano de fundo começam a desaparecer. Se ficarmos muito tempo olhando para a tela, a visão pode ficar cansada e embaralhada.

A solução (ou a trapaça)

Depois de analisar o problema cheguei a conclusão que o meu computador pode fazer todo o trabalho por mim. Cheater, preguiçoso, eu? ;)

Resolvi criar um bot afim de monitorar o game, mais precisamente a minha tela, processar as variantes e chutar a bola. Seria mais ou menos assim, acesso a página do game e inicio o jogo. Logo após, inicio o bot em um console, lembrando de minimizar todas as janelas menos o navegador e posso ir tomar um café enquanto o aplicativo fica rodando.

Como mencionado acima, não existe um tempo correto para a bola chegar no ponto mais alto. É possível abordar este problema de várias formas que vão desde análise de imagens (screenshots) a redes neurais. A técnica mais simples é a análise de imagens e é nela que vou implementar o bot.

Para registro, estou utilizando a resolução de 1024 x 768 em dois monitores de 17″. Independente da resolução o bot poderá ser criado. Já fiz todo o processo que vou explicar a seguir em um macbook e tudo funciona normalmente. Para isso é necessário seguir alguns passos que listo no próximo parágrafo.

Para começar, é preciso mapear a altura máxima que a bola pode chegar, leia-se distância entre o topo e a esquerda, em relação a sua área de trabalho. Em seguida, recorte a imagem da bola de acordo com o passo anterior, pois, ela gira em seu eixo. Estes são os passos necessários para calibração do bot.

As etapas anteriores são um tanto quanto chatas, mas as que vem a seguir serão bem mais interessantes. Com a imagem da bola podemos compará-la com o screenshot que vamos tirar do ponto mapeado. E por último, se as imagens forem iguais, aguardamos o momento do chute e chutamos.

É claro que podem haver várias maneiras de se implementar os passos citados acima. Não estou levando em consideração o desempenho, pois, em todos os meus testes o desempenho foi suficiente. Inclusive se você que está lendo tiver alguma sugestão, por favor, sinta-se a vontade para compartilhar.

Agora, um pouco de código.

Passo 1 e 2 (calibração)

Tire uma screenshot do navegador com o game iniciado (foto abaixo), com um software de edição de imagens meça a distância do topo (padTop) e esquerda (padLeft) do canvas do game (linhas azuis).

Mapeamento

Mapeamento

Agora, configure a classe para tirar fotos somente do canvas, encontre a imagem do ponto (linhas vermelhas) mais alto que a bola pode chegar analisando todas as imagens e, por último, com um software de edição de imagens meça novamente a distância do topo e esquerda, mas desta vez em relação a bola. Essa é uma das partes chatas.

Segue abaixo a classe para tirar fotos do game canvas.

TakeShoots.java (canvas)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.awt.image.*;
import javax.imageio.*;

public class TakeShoots {
    private BufferedImage image;
    private Robot robot;
        private int padLeft, padTop, w, h;

    public TakeShoots() {
            // settings for game canvas
            padLeft = 185;
            padTop = 209;
            w = 640;
            h = 480;
           
            // settings for ball
            //padLeft = 322;
            //padTop = 431;
            //w = 46;
            //h = 46;

            try {
                robot = new Robot();                    
            } catch (AWTException e) {

            }            
           
            System.out.println("Waiting 5 seg...");

            try {        
        Thread.sleep(5000);
      } catch (InterruptedException e){
     
      }

            System.out.println("Initializing thread...");
            TSThread t = new TSThread(3000);
            t.start();

        }

        class TSThread extends Thread {
         int howManyTimes;
                 File file;

         TSThread(int times) {
             this.howManyTimes = times;
         }
 
         public void run() {
             for(int i = 0; i <= this.howManyTimes; i++){
                           image = robot.createScreenCapture(new Rectangle(padLeft, padTop, w, h));
                                file = new File("image"+i+".png");
                                //file = new File("ball"+i+".png");

                                try{                                
                                    ImageIO.write(image,"png", file);
                                }catch(IOException e){
                                   
                                }

                                file = null;
                                image = null;              

                                try {
                    Thread.sleep(100);
                        } catch (InterruptedException e){
                       
                        }

             }
         }
     }

        public static void main(String args[]){
            new TakeShoots();
        }
}

Com a imagem do passo 1, recorte a imagem da bola no ponto mais alto em formato de quadrado. A bola possui o tamanho 46×46. Salve este arquivo com o nome ball.png no diretório do arquivo java. No meu caso a screenshot do game canvas em que a bola chega no ponto mais alto e a imagem da bola (ball.png) podem ser visualizadas abaixo:

Ponto mais alto

Ponto mais alto

Bola

Bola

Se desejar, alterando a classe TakeShoots.java, podemos automatizar o processo tirando screenshots somente da bola, é uma forma de ter certeza que é o ponto mais alto também. Para isso faça:

TakeShoots.java (ball)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.awt.image.*;
import javax.imageio.*;

public class TakeShoots {
    private BufferedImage image;
    private Robot robot;
        private int padLeft, padTop, w, h;

    public TakeShoots() {
            // settings for all game canvas
            //padLeft = 185;
            //padTop = 209;
            //w = 640;
            //h = 480;
           
            // settings for ball
            padLeft = 322;
            padTop = 431;
            w = 46;
            h = 46;

            try {
                robot = new Robot();                    
            } catch (AWTException e) {

            }            
           
            System.out.println("Waiting 5 seg...");

            try {        
        Thread.sleep(5000);
      } catch (InterruptedException e){
     
      }

            System.out.println("Initializing thread...");
            TSThread t = new TSThread(3000);
            t.start();

        }

        class TSThread extends Thread {
         int howManyTimes;
                 File file;

         TSThread(int times) {
             this.howManyTimes = times;
         }
 
         public void run() {
             for(int i = 0; i <= this.howManyTimes; i++){
                           image = robot.createScreenCapture(new Rectangle(padLeft, padTop, w, h));
                                //file = new File("image"+i+".png");
                                file = new File("ball"+i+".png");

                                try{                                
                                    ImageIO.write(image,"png", file);
                                }catch(IOException e){
                                   
                                }

                                file = null;
                                image = null;              

                                try {
                    Thread.sleep(100);
                        } catch (InterruptedException e){
                       
                        }

             }
         }
     }

        public static void main(String args[]){
            new TakeShoots();
        }
}

Passo 3

Com a imagem do passo 2 compare com outra imagem de tamanho 46×46 (ou não). Faça os testes você mesmo, compare a imagem calibrada (ball.png) com qualquer outra 46×46 no formato png. Faça também a comparação de ball.png com ela mesma e veja o resultado.

Estou utilizando uma forma bem simples para identificar se as imagens são iguais. Como se trata de uma imagem de tamanho pequeno, a comparação é feita pelos valores RGB das duas imagens pixel a pixel. Caso a imagem fosse maior que 46×46, uma outra forma seria, traçar linhas horizontais / verticais ou nas diagonais e comparar os valores RGB somente daqueles pontos. Em softwares de detecção de movimento é possível aprender muitas formas de tratar este problema.

Ah, lembrando, as imagens devem estar no mesmo diretório do arquivo java quando executar a classe.

CompareImages.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.awt.image.*;
import javax.imageio.*;

public class CompareImages {
    private BufferedImage image1, image2;
        private String filename1, filename2;

    public CompareImages() {                
                loadImages();
               
                long started = System.currentTimeMillis();
                boolean result = compareImage(image1, image2);
                System.out.println( (System.currentTimeMillis()) - started + " ms.");
                System.out.println("Result: " + result);
        }

        private void loadImages(){
                filename1 = "ball.png";
                filename2 = "ball.png";

                try{                
                    image1 = ImageIO.read(new File(filename1));
                    image2 = ImageIO.read(new File(filename2));
                }catch(IOException e){
                   
                }
        }

        private boolean compareImage(BufferedImage image1, BufferedImage image2) {
                        if(image1.getWidth() != image2.getWidth() || image1.getHeight() != image2.getHeight())
                                return false;

                        for(int x = 0; x < image1.getWidth(); x++) {
                        for(int y = 0; y < image1.getHeight(); y++) {
                                        if(image1.getRGB(x, y) != image2.getRGB(x, y))
                                               return false;
                        }
                    }
                    return true;
     }

        public static void main(String args[]){
            new CompareImages();
        }
}

Passo 4

Junte todos os passos anteriores em uma nova classe, quando a classe detectar que as imagens são iguais pressione e solte o botão esquerdo do mouse. Aproveitei este passo e criei um arquivo de configuração que é lido somente ao executar a classe em questão, isso evita recompilar o arquivo java só para ajustar os valores de configuração.

KickOffTrick.java (final)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.awt.image.*;
import javax.imageio.*;
import java.util.*;

public class KickOffTrick {
        private Properties properties;
        private final String propertiesFileName = "kickoff.properties";
    private BufferedImage ball1, ball2;
    private Robot robot;
        private int padLeft, padTop, w, h;

    public KickOffTrick() {

            readPropertiesFile(propertiesFileName);

            padLeft = Integer.parseInt(properties.getProperty("paddingleft"));
            padTop = Integer.parseInt(properties.getProperty("paddingtop"));
            w = Integer.parseInt(properties.getProperty("shootwidth"));
            h = Integer.parseInt(properties.getProperty("shootheight"));

            try{                
                    ball1 = ImageIO.read(new File(properties.getProperty("ballimagefilename")));
            }catch(IOException e){
                    e.printStackTrace();
            }

            try {
                robot = new Robot();                    
            } catch (AWTException e) {
                    e.printStackTrace();
            }            
           
            System.out.println("Waiting 5s...");
            System.out.println("Prepare yourself...");

            try {        
        Thread.sleep(5000);
      } catch (InterruptedException e){
     
      }

            System.out.println("Initializing thread...");
            TSThread t = new TSThread();
            t.start();

        }

        private void readPropertiesFile(String filename){
            properties = new Properties();
            try {
        properties.load(new FileInputStream(filename));
          } catch (IOException e) {
                    e.printStackTrace();
          }
        }

        class TSThread extends Thread {
                 boolean result;

         TSThread() {

         }
 
         public void run() {            
                         while(true){
                           ball2 = robot.createScreenCapture(new Rectangle(padLeft, padTop, w, h));                                
                                result = compareImage(ball1, ball2);
                               
                                if(result){
                                    try {
                          Thread.sleep(Integer.parseInt(properties.getProperty("sleepbeforekick")));
                              } catch (InterruptedException e){
                                      e.printStackTrace();
                              }

                                    robot.mousePress(InputEvent.BUTTON1_MASK);
                                    robot.mouseRelease(InputEvent.BUTTON1_MASK);
                                   
                                    try {
                          Thread.sleep(Integer.parseInt(properties.getProperty("sleepafterkick")));
                              } catch (InterruptedException e){
                                      e.printStackTrace();
                              }
                                }
                               
                                ball2 = null;

                                try {
                    Thread.sleep(Integer.parseInt(properties.getProperty("sleepballshoot")));
                        } catch (InterruptedException e){
                                e.printStackTrace();
                        }

             }
         }

                 private boolean compareImage(BufferedImage image1, BufferedImage image2) {
                        if(image1.getWidth() != image2.getWidth() || image1.getHeight() != image2.getHeight())
                                return false;

                        for(int x = 0; x < image1.getWidth(); x++) {
                        for(int y = 0; y < image1.getHeight(); y++) {
                                        if(image1.getRGB(x, y) != image2.getRGB(x, y))
                                               return false;
                        }
                    }
                    return true;
             }
     }

        public static void main(String args[]){
            new KickOffTrick();
        }
}

O arquivo de configuração ficou assim:

kickoff.properties

# ball left distance
paddingleft = 322
# ball top distance
paddingtop = 431
# ball shoot width
shootwidth = 46
# ball shoot height
shootheight = 46
# ball filename (for comparation)
ballimagefilename = ball.png
# sleep before kick the ball in ms
sleepbeforekick = 110
# sleep after kick the ball in ms
sleepafterkick = 10000
# sleep interval for take another shoot
sleepballshoot = 50

Não utilize os valores pré-selecionados, eles só estão preenchidos para você ter uma cola.

Concluindo

Talvez com este exemplo você não consiga bater nenhum record, mas vai ter a oportunidade de aprender coisas novas e interessantes. Eu me diverti pensando em como implementar a solução, espero que você se divirta lendo (ou fazendo todos os passos) ;)

Em breve os arquivos estarão no github.com se alguém se interessar.



Leave a Reply