Trapaceando no Monkey Kick Off
Posted: maio 11th, 2009 | Author: Carlan Calazans | Tags: aprendizado, dev, dica, games, java | 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
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).
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 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
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 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 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 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
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