図1.競技場。
かつて、ある記事が目を引きました。 見出しを読んだ直後に、この種のアイデアが頭に浮かびましたが、その中には同じものがありました。このゲーム用の汎用ボットを作成するというアイデアです。
当時、私はゲームの自動化の経験があまりありませんでしたが、何よりも欲望がありました。 したがって、ノートブックを取り出して、私はこのプロセスを熟考し始めました...
分析
正しい順序で正しいボタンを押す動作中のプログラムがすでに手元にあることを想像して、最後から分析し始めました。 彼女がこれを行えるようにするには、これらのボタンの座標を知る必要があります。少なくともそこに到達することを期待して、画面上のどこでも突かないでください。 次に、ボットがボタンを押す順序を指定する必要があります。ボタンを突くことはありませんか? 必要なクリックのシーケンスを提供するには、塗りつぶしの色を選択するための最適なアルゴリズムを選択する必要がありますが、フィールドの状態に関するデータなしでこれを行う方法はありますか? そうです、何もありません! したがって、競技場のセルの色に関するデータを取得する必要があります。 素晴らしいですが、疑問が生じます。競技場自体はどこにありますか? 「画面上のどこか」という答えは受け入れられないため、見つける必要があります。 これですべてが完了しました。次のステップの計画を作成します。
- 競技場とそのパラメーターを認識します。
- ゲーム内のセルの色に関するデータを取得します。
- 塗りつぶしオプションの最適なシーケンスを見つけます。
- 各色の塗りボタンの位置を見つけます。
- ゲームプロセスを自動化します。
競技場とそのパラメーターの認識
最初のステップは、何らかの形で画面上の競技場の位置を見つけることでした。 しかし、最初に、スクリーンショットを取得しても害はありません。 これを行うには、メソッドBufferedImage createScreenCapture(Rectangle screenRect)を持つjava.awt.Robotクラスが必要でした。
それから何? そして、プログラムに運動場の位置を見つけさせるための多くの試み、BufferedImageクラスのgetRGBメソッドへの数万回の呼び出し、テンプレートから運動場の位置を見つけようとする試みなどがありました。 これらの結果はすべて失敗しました。 あるゲームで機能したものは、別のゲームでは機能しませんでした。 そして一度、そのような計画が私の頭に浮かびました:
- 画像を二値化します。
- 必要に応じて、重要なポイントが白で背景が黒になるように反転します。
- 白いドットの発生数を水平および垂直にカウントします。
- 不要なエントリを除外します。
- ゼロ以外の値の最長シーケンスを見つけます。 この長いシーケンスの開始位置は競技場の開始点になり、このシーケンスの長さはピクセル単位のサイズになります。
スクリーンショットを受け取ったら、それをARGB配列に変換するとよいでしょう。
int[] pixels = new int[w * h]; image.getRGB(0, 0, w, h, pixels, 0, w);
カラー画像を扱うのは難しいので、それを二値化する必要があります。つまり、白黒にして、白黒の色だけにする必要があります。 ここではすべてが単純です。次の色を取得して、その明るさを確認します。しきい値を下回っている場合、この色は黒になり、上にある場合は白になります。 ソフトウェア実装では、次のようになります。
int red = (color >> 16) & 0xff; int green = (color >> 8) & 0xff; int blue = color & 0xff; int mean = (qr + qg + qb) / 3; if (mean > thresholdValue) color = 0xFFFFFFFF; else color = 0xFF000000;
まず、現在の色の明るさを取得する必要があります。これは、RGBコンポーネントの合計の平均として、または明るさ= 0.3 *赤+ 0.59 *緑+ 0.11 *青の式に従って取得されます。 係数は、特定の色成分の人間の目による知覚を意味する理由で取得されます。
ここで、しきい値(thresholdValue)を選択する必要があります。191が非常に適切です。明るい背景があったゲームのすべてのサイトで、2値化により次のような画像が生成されました。
図2.モノクロ画像。
また、背景が暗いサイトでは、値が64の場合、同様の画像が得られますが、逆になります。 ちなみに、私は191をそのようなものだけでなく、法律255-64に従って選択しました。
そのような画像では、色で見ている場合よりも、運動場を見つけるのがはるかに簡単であることがわかります。 これらのデータからしきい値64または(255-64)を使用し、必要に応じて画像を反転するために、背景が明るいか暗いかを確認するだけです。 私のコードでは、このように見えます:
private boolean[] threshold(int[] pixels, int value) { boolean inverse = isBackgroundLight(MAX_COLOR_POINTS); if (inverse) value = 255 - value; boolean[] bw = new boolean[pixels.length]; for (int i = 0; i < pixels.length; i++) { int brightness = getBrightness(pixels[i]); bw[i] = (brightNess >= value) ^ inverse; } return bw; }
isBackgroundLightは、背景が明るいか暗いかを理解するために必要なポイントの数を受け入れます。 カラー画像からMAX_COLOR_POINTSを取得し、平均輝度を見つけます。 128を超える場合、背景は明るい、それより小さい場合は暗い。
さらに、受信したモノクロ画像では、白ドットの数を水平および垂直にカウントする必要があります。 これは、画像上で2つのパスで実行されます。 その結果、1440、1440、410、23、119、838の形式の値を持つ2つの配列を取得します。平均以下の値をフィルター処理し、配列を取得します:1、1、0、0、0、1 ...そして最後に、最長のシーケンスを決定する必要があります以下は、ゼロで分割されていないユニットです。 これも非常に簡単な作業です。
最後に、4つの値を取得します:競技場の位置(水平、垂直、競技場の幅と高さ)。 正方形なので、最後の2つの値は一致する必要があります。 各セルのサイズを調べるには、競技場のサイズをその寸法で除算する必要があります。 フィールドは14x14だけでなく、10x10、20x20などでもあるため、ディメンションはユーザーが設定します。
セルの色データを取得する
競技場のパラメーターを決定したので、セルの色を読み取ることができます。
int[][] table = new int[boardSize][boardSize]; int offset = cellSize / 2; for (int i = 0; i < boardSize; i++) { for (int j = 0; j < boardSize; j++) { table[i][j] = detectImage.getRGB(j*cellSize + offset, i*cellSize + offset); } }
エラーを避けるために、セルの中心に厳密に色を付けます。
さらに、データの操作を簡単にするために、これらの色を識別子に変換する必要があります:0、1、2、3 ...しかし、ゲームのバージョンが異なるとパレットが異なるため(color == 0xFF46B1E2)、この単純なことはできません。 たとえば、一方にはオレンジ色があり、他方にはライラックが代わりに使用されます...しかし、大丈夫です、それを見てみましょう-各インデックスの色を覚えています:
private byte[][] colorsToIds(int[][] tableColor) { int size = tableColor.length; byte[][] out = new byte[size][size]; int colorsReaded = 1; // for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { int color = tableColor[i][j]; for (byte k = 0; k < colorsReaded; k++) { // if (colors[k] == -1) { colors[k] = color; colorsReaded++; if (colorsReaded > MAX_COLORS) colorsReaded = MAX_COLORS; } // , ID if (color == colors[k]) { out[i][j] = k; break; } } } } return out; }
MAX-COLORSは6です。Flood-Itゲームではパレットに非常に多くの色があるためです。 しかし、突然誰かがパレットに10種類の色を使ってゲームを作成することに決めた場合、プログラムはそれらを認識できるようになります。
これで、このコードの機能を確認できます。 プログラムに図1の競技場の画像を供給した後、結果はすぐに現れました。
0 1 2 2 3 3 0 0 1 4 2 3 5 3
2 4 1 1 5 4 5 0 2 4 5 3 4 4
3 1 1 5 2 1 3 5 5 2 1 0 4 2
0 3 1 0 5 4 4 1 4 1 4 5 3 5
4 5 0 4 4 4 3 2 0 3 1 0 0 5
0 4 3 4 1 1 2 2 3 2 4 3 1 2
3 2 0 0 5 2 1 5 3 0 2 1 4 4
3 3 5 3 1 5 3 0 3 5 2 4 1 1
3 3 3 3 0 5 0 3 5 0 1 4 2 4
4 2 3 0 2 1 0 3 1 3 2 5 2 3
5 5 1 4 2 3 5 4 1 2 5 0 4 0
5 2 0 2 2 3 2 5 4 1 1 5 0 5
1 0 0 5 5 5 4 1 2 2 3 2 2 0
3 2 2 3 4 3 0 3 2 2 4 3 5 5
塗りつぶしオプションの最適なシーケンスを見つける
どんな方法で試しても、この記事の著者よりも優れたアルゴリズムを思いつくことはできませんでした。 そこで彼のアルゴリズムを採用し、より普遍的な方法で書き直しました。 彼は気にしないと思います。
private byte getNextFillColor(byte[][] table) { // int fillSize = (int) Math.pow(MAX_COLORS, FILL_STEPS); int[] fillRate = new int[fillSize]; // int[] fillPow = new int[FILL_STEPS]; for (int i = 0; i < FILL_STEPS; i++) { fillPow[i] = (int) Math.pow(MAX_COLORS, i); } // FILL_STEPS MAX_COLORS for (int i = 0; i < fillSize; i++) { byte[][] iteration = copyTable(table); for (int j = 0; j < FILL_STEPS; j++) { byte fillColor = (byte) (i / fillPow[j] % MAX_COLORS); fillTable(iteration, fillColor); } // fillRate[i] = getFillCount(iteration); } // FILL_STEPS int maxArea = fillRate[0]; int maxColor = 0; for (int i = 1; i < fillSize; i++) { if (fillRate[i] > maxArea) { maxColor = i; maxArea = fillRate[i]; } } // byte colorID = (byte) (maxColor % MAX_COLORS); fillTable(table, colorID); return colorID; }
前方の4つの計算ミスとパレットの6つの色に制限されていないため、自由に値を変更できます。
この機能に基づいて、完全な勝利シーケンスを取得する機能を構成します。
ArrayList<Byte> seq = new ArrayList<Byte>(); while(!gameCompleted(copyTable)) { seq.add(getNextFillColor(copyTable)); }
gameCompletedは、すべてのセルがいずれかの色で塗りつぶされているかどうかを確認し、塗りつぶされている場合はゲームが完了します。
アプリケーションを起動することで、図1を食べさせ、その結果、次のようになります。
1 2 1 3 0 5 4 0 3 1 0 5 3 1 4 1 5 2 4 0 5
しかし、この種の結果は私たちにとってあまり便利ではありません。
フィルボタンの位置を見つける
したがって、自由に入力できる一連の識別子、セルの色、フィールドの座標、およびそのサイズのパレットがあります。 ボタンがあります。 競技場を見ると、ボタンが競技場の左側にあり、同じレベルにあることがよくわかります。 もちろんこれに限定されますが、そうではありません。 突然どこかにボタンが上部または右側にあるゲームがありますか? さらに、ボタンの色はセルの色に近いですが、常に同じではありません。 これは、画像ポイントとパレットとの単純な比較に限定されないことを意味します。 それでは、すべてをまとめて、ボタン検索アルゴリズムを作成しましょう。
- ウィンドウ画像の4つの部分のいずれかを選択します。競技場の左側、上から右、最後に下から。
- パレットの各色について、画像内でそれに近い色を探します。 発見すると、このポイントの位置を覚えています。
- パレットからすべての色のポイントが見つかった場合、すべてが正常であり、タスクに対処しました。 それ以外の場合は、最初の手順に進み、画像の次の部分を選択します。
- 画像のすべての部分が表示されたが、ボタンが見つからなかった場合、それらは単に見つかりません! この場合、結果をより人間的な形で単純に提供します。
図3.画像のパーツへの分解。
上記のコードはすべて次のようになります。
public Point[] getButtons(int[] colors) { Point[] out = new Point[colors.length]; // int size = boardSize * cellSize; // , Rectangle[] partsOfImage = new Rectangle[] { new Rectangle(0, board.y, board.x, size), // new Rectangle(0, 0, w, board.y), // new Rectangle(board.x+size, board.y, w-board.x-size, size), // new Rectangle(0, board.y+size, w, h-board.y-size) // }; for (int i = 0; i < partsOfImage.length; i++) { Rectangle rect = partsOfImage[i]; BufferedImage part = image.getSubimage(rect.x, rect.y, rect.width, rect.height); // , boolean found = true; for (int j = 0; j < colors.length; j++) { if (colors[i] == -1) continue; Point pt = findButton(part, colors[j]); if (pt != null) { // pt.translate(rect.x, rect.y); out[j] = pt; } else { found = false; break; } } if (found) return out; } // return null; }
findButtonは、指定された色に似た色が見つかるまで、画像内のすべてのポイントを単純に繰り返します。 このような色を見つけるには、RGBコンポーネントのそれぞれを法とする差を計算し、特定の数と比較する必要があります-感度:
private boolean isEquals(int color1, int color2, int tolerance) { if (tolerance < 2) return color1 == color2; int r1 = (color1 >> 16) & 0xff; int g1 = (color1 >> 8) & 0xff; int b1 = color1 & 0xff; int r2 = (color2 >> 16) & 0xff; int g2 = (color2 >> 8) & 0xff; int b2 = color2 & 0xff; return (Math.abs(r1 - r2) <= tolerance) && (Math.abs(g1 - g2) <= tolerance) && (Math.abs(b1 - b2) <= tolerance); }
ゲーム自動化
したがって、すべてのボタンが見つかり、それらのクリックの目的のシーケンスが指定されたら、「自動クリック」に進むことができます。 プログラムがカーソルを正しい場所に移動し、マウスの左ボタンを押すために、同じクラスjava.awt.Robotが機能します。 関数は次のようになります。
public void clickPoint(Point click) { robot.mouseMove(click.x, click.y); robot.mousePress(InputEvent.BUTTON1_MASK); robot.delay(CLICK_DELAY); robot.mouseRelease(InputEvent.BUTTON1_MASK); }
説明してみましょう。まず、カーソルを画面上の目的の位置に移動し、次にマウスの左ボタンを押して、ブラウザがクリックを処理できるように数秒待ってからボタンを離します。 すべてがシンプルです。 また、クリックのインデックスのシーケンスと各ボタンの座標を使用して、自動クリッカーを作成できます。
public void autoClick(Point[] buttons, byte[] result) { for (int i = 0; i < result.length; i++) { clickPoint(buttons[result[i]]); } }
画面にボタンがないときの状況を処理するために、これらすべてが残っています。 この場合、画像を作成し、その上にすべての色(つまり、識別子0、1、2ではなく色)を順番に描画して表示します。
public BufferedImage sequenceToImage(byte[] ids, int[] palette) { final int size = 20; // // 10 final int CELLS_IN_ROW = 10; int width = CELLS_IN_ROW * size; if (width == 0) width = size; int rows = ids.length / CELLS_IN_ROW; BufferedImage out = new BufferedImage(width, (rows*size)+size, BufferedImage.TYPE_INT_RGB); Graphics G = out.getGraphics(); for (int i = 0; i < ids.length; i++) { G.setColor(new Color(palette[ids[i]])); G.fillRect(i % CELLS_IN_ROW * size, i / CELLS_IN_ROW * size, size, size); } G.dispose(); return out; }
おわりに
同じ図1で何が起こったのかを次に示します。
図4.結果。
ご清聴ありがとうございました。このトピックに関するヒントやアイデアをお待ちしております。
githubで利用可能なソース: Flood-It-Bot