Пишем тактическую игру про цифры под Android

от автора

Когда я только взялся за программирование (3 месяца назад), я быстро понял, что лучше сразу начинать заниматься своими проектами. Невозможно с утра до вечера сидеть за книгами или курсами, но если вы начнете делать что-то свое, то запросто просидите за разработкой с утра до утра.

Эта статья — небольшой туториал по тому, как сделать логическую игру с ботом. Игра будет выглядеть вот так:


*Подробно опишу правила еще раз в разделе про ИИ.

Читателей статьи условно разделяю на три группы.

  1. Начали программировать несколько часов назад.
    Вам будет сложно, лучше предварительно пройдите какой-нибудь небольшой курс по введению в Android-разработку, разберитесь с двумерными массивами и интерфейсами. А потом загрузите проект с гитхаба. Комментарии и эта статья помогут вам разобраться, что и как работает.
  2. Уже умеете программировать, но еще не можете назвать себя опытными.
    Вам будет интересно, потому что вы очень быстро сможете сделать свою игру. Я взял на себя грязную работенку по построению логики игры и ui-составляющей, вам же оставляю творческую часть. Вы можете сделать другой режим игры (2 на 2, онлайн и т.п.), изменить алгоритмы бота, создать уровни и т.д.
  3. Опытные.
    Вам может быть интересно подумать над ИИ — написать его не так легко, как кажется на первый взгляд. Так же я был бы очень рад получить от вас замечания по коду — уверен, далеко не все я сделал оптимально.

Прелюдия

Сейчас я заново создам проект (чтобы ничего не упустить) и последовательно опишу все шаги. Постараюсь писать код в хорошем тоне, но будут и плохие места, на которые я пошел ради сокращения объема.

Будем следовать следующему плану:

  • Создадим проект
  • Напишем бота
  • Напишем класс для игры
  • Займемся ui

Создаем проект

Все как обычно: создаем новый проект, далее-далее-далее-финиш. Учитывая, что часть аудитории может быть представлена группой «Начали программировать несколько часов назад», приведу подробную инструкцию.

Инструкция

Обратите внимания, проект делается в Android Studio.
Вместо «livermor» в Company Domain укажите что-то свое

Поменяйте вверху Android на Project. На скрине приведен пример, как и где создавать классы.

Пишем бота

Начнем с самой сложной и самой интересной задачи — напишем класс для бота.
Можете посмотреть видео еще раз и подумать, как бы вы реализовали алгоритмы.
На всякий случай приведу правила еще раз:

правила

соперники ходят по очереди. Один играет за строки, другой за ряды. Выбранное одним игроком число прибавляется к его очкам и определяет ряд(строку) ходов для другого. Ходить в одно и то же место два раза подряд нельзя. Побеждает тот, у кого больше очков на конец игры (когда не осталось возможных ходов).

Первая идея, которая мне пришла, — просчитать все ходы до конца. Либо до n-го хода. Но как просчитывать ходы? Давайте введем понятие лучшего хода. Наверняка, это такой ход, который максимизирует разницу между вашим ходом и лучшим ходом соперника. То есть вы просчитываете свой лучший ход, основываясь на том, что соперник будет просчитывать ваш лучший ход, ожидая, что вы просчитываете свой лучший ход, основываясь… И так до n. Самый последний ход будет представлять собой просто максимальное число в ряду.

Как по-вашему, нормальный это алгоритм для бота?
На самом деле, это даже хуже, чем просто выбирать максимум.
Вы уже догадались, в чем проблема?
Дело в том, что мы предполагаем, что соперник будет совершать этот лучший ход. Мы можем выбрать -2, ожидая, что соперник возьмет -3 (его лучший ход, который оправдается в конце партии), но соперник возьмет да и пойдет в +6. Тогда мы все пересчитаем и пойдем в -5, ожидая, что соперник сходит в -4, а он опять возьмет да и выберет +8. И так далее — мы всегда совершаете долгосрочные ходы, и всегда проигрываем здесь и сейчас.

Самый простой способ сделать этот алгоритм работоспособным — поставить n = 2. То есть предполагать, что соперник просто выберет максимум из своего ряда, и самим искать такой ход, который максимизирует разницу между нашими ходами. К слову, это сделает бота вполне конкурентным.

Я пошел немного дальше и попробовал сделать бота человечнее — дал ему жадность. Другими словами, я приказал боту делать краткосрочные ходы, если можно извлечь разницу в указанное количество очков. В коде я обозвал эту разницу джекпотом, и бот срывает джекпот, если на горизонте планирования это не приведет к досрочному поражению (в комментариях к коду я все описал подробнее).

И последнее, прежде чем вы будете создавать класс для бота, опишу детальнее, что он из себя представляет.
Бот необходим классу Игра для того, чтобы получить номер хода (в строке или в ряду). Все данные, которые изменяются на протяжении игры — очки игроков, булева матрица с разрешенными ходами, номер последнего совершенного хода — будут храниться в классе Игра. Соответственно, создавая сущность класса Бот, нам необходимо передать ему только неизменяемые в течение одной партии вещи: играет ли бот за строки или за ряды и матрицу с числами.
У Бота есть один public метод — сделать ход, к которому мы обращаемся каждый раз, когда хотим получить ход. Соответственно, в этот метод мы передаем все изменяемые значения.

обращение к тем, кто программирует несколько часов

То, что я обозвал protected, может быть использовано для наследования — то есть создания детей бота,
public — для пользования другими классами,
private — внутренняя кухня, о которой другим классам лучше не знать.

Если вы практически ничего не поймете — это нормально, я так же проходил первые свои туториалы.
Класс для Бота — самый сложный, дальше будет легче.

код бота

package com.livermor.plusminus; //не забудьте заменить "livermor" на ваш Company Domain  public class Bot {      protected int[][] mMatrix; //digits for buttons     protected boolean[][] mAllowedMoves; //ходы, куда еще не сходили     protected int mSize; //размер матрицы     protected int mPlayerPoints = 0, mAiPoints = 0; //очки игроков     protected boolean mIsVertical; //играем за строки или ряды     protected int mCurrentActiveNumb; //номер последнего хода (от 0 до размера матрицы(mSize))      //рейтинги для ходов     private final static int CANT_GO_THERE = -1000; //если нет хода, то ставим ретинг -1000     private final static int WORST_MOVE = -500; // ход, когда мы неизбежно проигрываем     private final static int VICTORY_MOVE = 500; // ход, когда мы неизбежно выигрываем     private final static int JACKPOT_INCREASE = 9; //надбавка к рейтингу, если ход принесет куш     private static final int GOOD_ADVANTAGE = 6;//Куш (джекпот), равный разнице в 6 очков или больше      int depth = 3; //по умолчанию просчитываем на 3 хода вперед      public Bot(             int[][] matrix,             boolean vertical     ) {         mMatrix = matrix;         mSize = matrix.length;         mIsVertical = vertical;     }      //функция, возвращающая номер хода     public int move(             int playerPoints,             int botPoints,             boolean[][] moves,             int activeNumb     ) {         mPlayerPoints = playerPoints;         mAiPoints = botPoints;         mCurrentActiveNumb = activeNumb;         mAllowedMoves = moves;          return calcMove();     }      //можем задать другую глубину просчета     public void setDepth(int depth) {         this.depth = depth;     }      protected int calcMove() {         //функция для определения лучшего хода игрока         return calcBestMove(depth, mAllowedMoves,                 mCurrentActiveNumb, mIsVertical, mAiPoints, mPlayerPoints);     }      private int calcBestMove(int depth, boolean[][] moves, int lastMove, boolean isVert,                              int myPoints, int hisPoints) {          int result = mSize; //возвращаем размер матрицы, если нет доступных ходов         int[] moveRatings = new int[mSize]; //будем хранить рейтинги ходов в массиве          //если последний ход, возвращаем максимум в ряду (строке)         if (depth == 1) return findMaxInRow(lastMove, isVert);         else {              int yMe, xMe; // координаты ходов текущего игрока             int yHe, xHe; // координаты ходов оппонента              for (int i = 0; i < mSize; i++) {                  //если игрок ходит вертикально, то ходим по строкам (i) в ряду (lastMove)                 yMe = isVert ? i : lastMove;                 xMe = isVert ? lastMove : i;                  //если нет хода, ставим ходу минимальный рейтинг                 if (!mAllowedMoves[yMe][xMe]) {                     moveRatings[i] = CANT_GO_THERE;                     continue; //переходим к следующему циклу                 }                  int myNewP = myPoints + mMatrix[yMe][xMe];//считаем новые очки игрока                 moves[yMe][xMe] = false;//временно запрещаем ходить туда, куда мы сходили                  //считаем лучший ход для соперника                 int hisBestMove = calcBestMove(depth - 1, moves, i, !isVert, hisPoints, myPoints);                  //если случилось так, что у соперника нет ходов (т.е. вернулся размер матрицы), то..                 if (hisBestMove == mSize) {                     if (myNewP > hisPoints) //если у меня больше очков, то это победный ход                         moveRatings[i] = VICTORY_MOVE;                     else //если меньше, то это ужасный ход                         moveRatings[i] = WORST_MOVE;                      moves[yMe][xMe] = true;//Просчеты завершены, возвращаем ходы как было                     continue;                 }                  //теперь определим ход соперника, для того чтобы посчитать разницу между ходами                 yHe = isVert ? i : hisBestMove;                 xHe = isVert ? hisBestMove : i;                 int hisNewP = hisPoints + mMatrix[yHe][xHe];                 moveRatings[i] = myNewP - hisNewP;                  //и наконец сделаем надбавку к рейтингам ходов в случае, если можно сорвать куш                 //если глубина уже равна 1, то нет смысла делать рассчеты второй раз                 if (depth - 1 != 1) {                      //на этот раз нам хватит формулы поиска максимума                     hisBestMove = findMaxInRow(i, !isVert);                     yHe = isVert ? i : hisBestMove;                     xHe = isVert ? hisBestMove : i;                     hisNewP = hisPoints + mMatrix[yHe][xHe];                      int jackpot = myNewP - hisNewP;//считаем разницу для проверки ситуации куша                     if (jackpot >= GOOD_ADVANTAGE) { //если куш, то делаем надбавку                         moveRatings[i] = moveRatings[i] + JACKPOT_INCREASE;                     }                 }                  moves[yMe][xMe] = true;//Просчеты завершены, возвращаем ходы как было                              } // рейтинги ходов проставлены, пора выбирать ход с макс. рейтингом                          //начинаем с предположения, что максимум — это самый худший вариант (ходов вообще нет)             int max = CANT_GO_THERE;             for (int i = 0; i < mSize; i++) {                 if (moveRatings[i] > max) {                     max = moveRatings[i];//если есть ход лучше, пусть теперь он будет максимумом                     result = i;                 }             }         }          //возвращаем ход с максимальным рейтингом         return result;     }      //возвращает ход, соответствующий максимальному числу в указанном ряду(строке)     private int findMaxInRow(int lastM, boolean isVert) {          int currentMax = -10;         int move = mSize;          int y = 0, x = 0;         for (int i = 0; i < mSize; i++) {             y = isVert ? i : lastM;             x = isVert ? lastM : i;             int temp = mMatrix[y][x];             if (mAllowedMoves[y][x] && currentMax <= temp) {                 currentMax = temp;                 move = i;             }         }          return move;     } }  

Пишем класс для игры

Вначале я предупреждал, что будут плохие места в коде. И вот одно из них. Вместо того, чтобы сперва написать родительский класс для управления игрой, а потом расширить его до конкретного класса игры с ботом, я сразу напишу класс игры с ботом. Делаю это для сокращения туториала.

Игровой класс, назовем его Game, нуждается в двух вещах:
1. Интерфейс для работы с ui-элементами;
2. Размер матрицы.

обращение к тем, кто программирует несколько часов

Осторожно, в классе Game используются AsyncTask и Handler — либо разберитесь с ними предварительно, либо просто не обращайте на них внимания. Если в двух словах, это удобные классы для использования потоков. В андроид нельзя изменять элементы интерфейса не из основного потока. Указанные выше классы позволяют решить эту проблему.

код игры

package com.livermor.plusminus;  import android.os.AsyncTask; import android.os.Handler;  import java.util.Random;  public class Game {      //время задержки перед обновлениями очков, смены анимации     public static final int mTimeToWait = 800;     protected MyAnimation mAnimation; //класс AsyncTask для анимации      //матрица цифр и матрица допустимых ходов     protected int[][] mMatrix; //digits for buttons     protected volatile boolean[][] mAllowedMoves;     protected int mSize; //размер матрицы      protected int playerOnePoints = 0, playerTwoPoints = 0;//очки игроков      protected volatile boolean isRow = true; //мы играем за строку или за ряд     protected volatile int currentActiveNumb; //нужно для определения последнего хода     protected ResultsCallback mResults;//интерфейс, который будет реализовывать MainActivity      protected volatile Bot bot;//написанный нами бот     Random rnd; // для заполнения матрицы цифрами и определения первой активной строки      public Game(ResultsCallback results, int size) {         mResults = results; //передаем сущность интерфейса         mSize = size;          rnd = new Random();         generateMatrix(); //заполняем матрицу случайнами цифрами          //условный ход, нужен для определения активной строки         currentActiveNumb = rnd.nextInt(mSize);          isRow = true; //в нашей версии мы всегда будем играть за строку (просто для упрощения)          for (int yPos = 0; yPos < mSize; yPos++) {             for (int xPos = 0; xPos < mSize; xPos++) {                  //записываем сгенерированные цифры на кнопки с помощью нашего интерфейса                 mResults.setButtonText(yPos, xPos, mMatrix[yPos][xPos]);                  if (yPos == currentActiveNumb) // закрашиваем активную строку                     mResults.changeButtonBg(yPos, xPos, isRow, true);             }         }          bot = new Bot(mMatrix, true);     }      public void startGame() {         activateRawOrColumn(true);     }      protected void generateMatrix() {          mMatrix = new int[mSize][mSize];         mAllowedMoves = new boolean[mSize][mSize];          for (int i = 0; i < mSize; i++) {             for (int j = 0; j < mSize; j++) {                  mMatrix[i][j] = rnd.nextInt(19) - 9; //от -9 до 9                 mAllowedMoves[i][j] = true; // сперва все ходы доступны             }         }     }      //будем вызывать метод из MainActivity, которая будет следить за нажатиями кнопок с цифрами     public void OnUserTouchDigit(int y, int x) {          mResults.onClick(y, x, true);         activateRawOrColumn(false);//после хода нужно заблокирвоать доступные кнопки          mAllowedMoves[y][x] = false; //два раза в одно место ходить нельзя         playerOnePoints += mMatrix[y][x]; //берем из матрицы очки          mResults.changeLabel(false, playerOnePoints);//изменяем свои очки          mAnimation = new MyAnimation(y, x, true, isRow);//включаем анимацию смены хода         mAnimation.execute();          isRow = !isRow; //после хода меняем строку на ряд         currentActiveNumb = x; //по нашему ходу потом будем определять, куда можно ходить боту     }      //по завершению анимации разрешаем совершить ход боту     protected void onAnimationFinished() {          if (!isRow) {//в нашей версии бот играет только за ряды (вертикально)              //используем Handler, потому что предстоит работа с ui, который нельзя обновлять             //не из главного потока. Handel поставит задачу в очередь главного потока             Handler handler = new Handler();             handler.postDelayed(new Runnable() {                 @Override                 public void run() {                     botMove(); //                 }             }, mTimeToWait / 2);          } else //если сейчас горизонтальный ход, то активируем строку             activateRawOrColumn(true);     }      private void botMove() {          //получаем ход бота         int botMove = bot.move(playerOnePoints,                 playerTwoPoints, mAllowedMoves, currentActiveNumb);          if (botMove == mSize) {//если ход равен размеру матрицы, значит ходов нет             onResult(); //дергаем метод завершения игры             return; //досрочно выходим из метода         }          int y = botMove; // по рядам ходит бот         int x = currentActiveNumb;         mAllowedMoves[y][x] = false;         playerTwoPoints += mMatrix[y][x];         mResults.onClick(y, x, false); //имитируем нажатие на кнопку         mResults.changeLabel(true, playerTwoPoints); //меняем очки бота          mAnimation = new MyAnimation(y, x, true, isRow); //анимируем смену хода         mAnimation.execute();          isRow = !isRow; //меняем столбцы на строки         currentActiveNumb = botMove; //по ходу бота определим, где теперь будет строка     }      protected void activateRawOrColumn(final boolean active) {          int countMovesAllowed = 0; // для определения, есть ли допустимые ходы          int y, x;         for (int i = 0; i < mMatrix.length; i++) {              y = isRow ? currentActiveNumb : i;             x = isRow ? i : currentActiveNumb;              if (mAllowedMoves[y][x]) { //если ход допустим, то                 mResults.changeButtonClickable(y, x, active); //активируем, либо деактивируем его                 countMovesAllowed++; //если переменная останется нулем, то ходов нет             }         }         if (active && countMovesAllowed == 0) onResult();     }      //анимация: кнопки закрашиваются одна за другой     //сперва закрашиваем новые ходы — затем стираем предыдущие     protected class MyAnimation extends AsyncTask<Void, Integer, Void> {          int timeToWait = 35; //время задержки в миллисекундах         int y, x;         boolean activate;         boolean row;          protected MyAnimation(int y, int x, boolean activate, boolean row) {             this.activate = activate;             this.row = !row;             this.y = y;             this.x = x;         }          @Override         protected Void doInBackground(Void... params) {              int downInc = row ? x - 1 : y - 1;             int uppInc = row ? x : y;              if (activate)                 sleep(Game.mTimeToWait);//наш собственный метод для паузы              if (activate) { //когда активируем ходы, показываем анимацию от точки нажатия к границам                 while (downInc >= 0 || uppInc < mSize) {                     //Log.i(TAG, "while in Animation");                      sleep(timeToWait);                     if (downInc >= 0)                         publishProgress(downInc--); //метод AsyncTask для отображения прогресса                      sleep(timeToWait);                     if (uppInc < mSize)                         publishProgress(uppInc++);                 }              } else {//когда деактивируем ходы, показываем анимацию от границ к точке нажатия                  int downInc2 = 0;                 int uppInc2 = mSize - 1;                  while (downInc2 <= downInc || uppInc2 > uppInc) {                      sleep(timeToWait);                     if (downInc2 <= downInc) publishProgress(downInc2++);                     sleep(timeToWait);                     if (uppInc2 > uppInc) publishProgress(uppInc2--);                 }             }              return null;         }          @Override         protected void onProgressUpdate(Integer... values) {             int numb = values[0];              int yPos = row ? y : numb;             int xPos = row ? numb : x;              //вызываем методы интерфеса для изменения фона кнопок с цифрами (ходов)             if (activate) mResults.changeButtonBg(yPos, xPos, row, activate);             else mResults.changeButtonBg(yPos, xPos, row, activate);         }          @Override         protected void onPostExecute(Void aVoid) {              if (activate) //если только что активировали, то теперь нужно деактивировать старое                 new MyAnimation(y, x, false, row).execute();             else //теперь, когда завершили деактивацию, дергаем метод завершения анимации                 onAnimationFinished();         }          //наш метод для задержки         private void sleep(int time) {             try {                 Thread.sleep(time);             } catch (InterruptedException e) {                 e.printStackTrace();             }         }     }      protected void onResult() {         //метод интерфеса для отображения результатов         mResults.onResult(playerOnePoints, playerTwoPoints);     }      //Интерфейс для MainActivity, который будет изменять ui элементы     //*********************************************************************************     public interface ResultsCallback {          //для изменения ваших очков и очков соперника         void changeLabel(boolean upLabel, int points);          //для изменения цвета кнопок         void changeButtonBg(int y, int x, boolean row, boolean active);          //для заполнения кнопок цифрами         void setButtonText(int y, int x, int text);          //для блокировки/разблокировки кнопок         void changeButtonClickable(int y, int x, boolean clickable);          //по окончанию партии         void onResult(int one, int two);          //по нажатию на кнопку         void onClick(int y, int x, boolean flyDown);     } } 

Работаем над пользовательским интерфейсом

Осталась заключительная часть — связать логику игры с пользовательским интерфейсом. Здесь будет меньше комментариев и пояснений, мы просто сделаем шаг за шагом все необходимые вещи.

Пометка для тех, кто программирует несколько часов

Убедитесь, что вверху у вас стоит Project, а не Android.
На данный момент у нас есть 3 класса: созданные нами Bot и Game и уже существующий класс MainActivity. Сейчас нам предстоит изменить несколько xml-документов (обведенных красным), создать еще один класс для цифр-кнопок и создать drawable-элемент (показываю черной стрелкой, как это делается).

1. Запрещаем экрану поворачиваться:

В AndroidManifest добавляем под MainActivity — android:screenOrientation=«portrait»
Делаем для того, чтобы запретить переворачивать экран (для упрощения туториала).

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <!-- если будете копировать, то не забудьте поменять package на свой. вообще, конечно, лучше просто копируйте одну строчку >>> android:screenOrientation="portrait" --> <manifest package="com.livermor.plusminus"           xmlns:android="http://schemas.android.com/apk/res/android">      <application         android:allowBackup="true"         android:icon="@mipmap/ic_launcher"         android:label="@string/app_name"         android:supportsRtl="true"         android:theme="@style/AppTheme">         <activity android:name=".MainActivity"                   android:screenOrientation="portrait">             <intent-filter>                 <action android:name="android.intent.action.MAIN"/>                  <category android:name="android.intent.category.LAUNCHER"/>             </intent-filter>         </activity>     </application>  </manifest> 

2. Добавляем нужные нам цвета:

Заходим в colors.xml, удаляем имеющиеся цвета, добавляем эти:

colors.xml

<?xml version="1.0" encoding="utf-8"?> <resources>     <color name="colorPrimary"      >#7C7B7B</color>     <color name="colorPrimaryDark"  >#424242</color>     <color name="colorAccent"       >#FF4081</color>     <color name="bgGrey"            >#C4C4C4</color>     <color name="bgRed"             >#FC5C70</color>     <color name="bgBlue"            >#4A90E2</color>     <color name="black"             >#000</color>     <color name="lightGreyBg"       >#DFDFDF</color>     <color name="white"             >#fff</color> </resources> 

3. Меняем тему приложения:

В styles.xml заменяем Theme.AppCompat.Light.DarkActionBar на Theme.AppCompat.Light.NoActionBar:

styles.xml

<resources>     <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">         <item name="colorPrimary">@color/colorPrimary</item>         <item name="colorPrimaryDark">@color/colorPrimaryDark</item>         <item name="colorAccent">@color/colorAccent</item>     </style> </resources> 

4. Устанавливаем размеры:

Заменим размеры в dimens.xml на следующие::

dimens.xml

<resources>     <dimen name="button.radius">10dp</dimen>     <dimen name="sides">10dp</dimen>     <dimen name="up_bottom">20dp</dimen>     <dimen name="label_height">55dp</dimen>     <dimen name="label_text_size">40dp</dimen>     <dimen name="label_padding_sides">6dp</dimen> </resources> 

5. Создаем фоны для кнопок:

Нужно создать три xml в папке drawable:

bg_blue.xml

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android">     <solid android:color="@color/bgBlue"/>      <corners android:bottomRightRadius="@dimen/button_radius"              android:bottomLeftRadius="@dimen/button_radius"              android:topLeftRadius="@dimen/button_radius"              android:topRightRadius="@dimen/button_radius"/> </shape> 

bg_red.xml

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android">     <solid android:color="@color/bgRed"/>      <corners android:bottomRightRadius="@dimen/button_radius"              android:bottomLeftRadius="@dimen/button_radius"              android:topLeftRadius="@dimen/button_radius"              android:topRightRadius="@dimen/button_radius"/> </shape> 

bg_grey.xml

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android">     <solid android:color="@color/bgGrey"/>      <corners android:bottomRightRadius="@dimen/button_radius"              android:bottomLeftRadius="@dimen/button_radius"              android:topLeftRadius="@dimen/button_radius"              android:topRightRadius="@dimen/button_radius"/> </shape> 

6. Изменяем макет экрана:

Для матрицы я буду использовать GridLayout — возможно, не самое лучшее решение, но оно показалось мне довольно простым и коротким.
Просто замените имеющийся код на мой — там пустой GridLayout (заполним его кодом в MainActivity) и два TextView-элемента для показателей очков игроков (RelativeLayout внутри другого RelativeLayout — для того, чтобы выравнять все по центру по вертикали. View «center» — для выравнивания показателей очков к центру по горизонтали).
Да, и не беспокойтесь, в preview вы ничего не увидите, кроме верхней надписи Бот, так и должно быть.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:background="#000"     tools:context="com.livermor.myapplication.MainActivity">      <RelativeLayout         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_centerVertical="true"         android:background="@color/lightGreyBg">          <View             android:id="@+id/center"             android:layout_width="10dp"             android:layout_height="1dp"             android:layout_centerInParent="true"/>          <TextView             android:id="@+id/upper_scoreboard"             android:background="@drawable/bg_red"             android:layout_width="match_parent"             android:layout_height="55dp"             android:layout_alignParentLeft="true"             android:layout_alignParentTop="true"             android:layout_marginLeft="@dimen/sides"             android:layout_marginTop="15dp"             android:layout_toLeftOf="@id/center"             android:gravity="center_vertical|center_horizontal"             android:paddingLeft="@dimen/label_padding_sides"             android:paddingRight="@dimen/label_padding_sides"             android:text="Бот: 0"             android:textColor="@color/white"             android:textSize="@dimen/label_text_size"/>          <GridLayout             xmlns:android="http://schemas.android.com/apk/res/android"             android:id="@+id/my_grid"             android:layout_width="match_parent"             android:layout_height="match_parent"             android:layout_below="@+id/upper_scoreboard"             android:layout_gravity="center"             android:foregroundGravity="center"             android:layout_marginLeft="@dimen/sides"             android:layout_marginRight="@dimen/sides"             android:layout_marginBottom="@dimen/up_bottom"             android:layout_marginTop="@dimen/up_bottom"/>          <TextView             android:id="@+id/lower_scoreboard"             android:background="@drawable/bg_blue"             android:layout_width="match_parent"             android:layout_height="@dimen/label_height"             android:layout_alignParentRight="true"             android:layout_alignParentEnd="true"             android:layout_below="@+id/my_grid"             android:layout_marginBottom="15dp"             android:layout_marginRight="15dp"             android:layout_toRightOf="@id/center"             android:gravity="center_vertical|center_horizontal"             android:paddingLeft="@dimen/label_padding_sides"             android:paddingRight="@dimen/label_padding_sides"             android:text="Вы: 0"             android:textColor="@color/white"             android:textSize="@dimen/label_text_size"/>      </RelativeLayout>  </RelativeLayout> 

7. Создаем класс MyButton, наследующий Button:

Создаем свой класс для кнопок, чтобы удобнее было получать координаты каждой кнопки в матрице.

Код

package com.livermor.plusminus;  import android.content.Context; import android.util.AttributeSet; import android.widget.Button;  public class MyButton extends Button {          private MyOnClickListener mClickListener;//наш интерфейс учета кликов для MainActivity     int idX = 0;     int idY = 0;      //конструктор, в котором будем задавать координаты кнопки в матрице     public MyButton(Context context, int x, int y) {         super(context);         idX = x;         idY = y;     }      public MyButton(Context context) {         super(context);     }      public MyButton(Context context, AttributeSet attrs) {         super(context, attrs);     }      public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {         super(context, attrs, defStyleAttr);     }      @Override //метод View для отлавливания кликов     public boolean performClick() {         super.performClick();          mClickListener.OnTouchDigit(this);//будем дергать метод интерфейса         return true;     }      public void setOnClickListener(MyOnClickListener listener){         mClickListener = listener;     }      public int getIdX(){         return idX;     }      public int getIdY(){         return idY;     }      //Интерфейс для MainActivity     //************************************     public interface MyOnClickListener {          void OnTouchDigit(MyButton v);     } } 

8. И, наконец, отредактируем класс MainActivity:

Код

package com.livermor.plusminus;  import android.graphics.Typeface; import android.os.Handler; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.animation.AnimationSet; import android.view.animation.TranslateAnimation; import android.widget.Button; import android.widget.GridLayout; import android.widget.TextView; import android.widget.Toast;  public class MainActivity extends AppCompatActivity         implements Game.ResultsCallback, MyButton.MyOnClickListener {      private static final int MATRIX_SIZE = 5;// можете ставить от 2 до 20))      //ui     private TextView mUpText, mLowText;     GridLayout mGridLayout;     private MyButton[][] mButtons;      private Game game;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          mGridLayout = (GridLayout) findViewById(R.id.my_grid);         mGridLayout.setColumnCount(MATRIX_SIZE);         mGridLayout.setRowCount(MATRIX_SIZE);         mButtons = new MyButton[MATRIX_SIZE][MATRIX_SIZE];//5 строк и 5 рядов          //создаем кнопки для цифр         for (int yPos = 0; yPos < MATRIX_SIZE; yPos++) {             for (int xPos = 0; xPos < MATRIX_SIZE; xPos++) {                 MyButton mBut = new MyButton(this, xPos, yPos);                  mBut.setTextSize(30-MATRIX_SIZE);                 Typeface boldTypeface = Typeface.defaultFromStyle(Typeface.BOLD);                 mBut.setTypeface(boldTypeface);                 mBut.setTextColor(ContextCompat.getColor(this, R.color.white));                 mBut.setOnClickListener(this);                 mBut.setPadding(1, 1, 1, 1); //так цифры будут адаптироваться под размер                  mBut.setAlpha(1);                 mBut.setClickable(false);                  mBut.setBackgroundResource(R.drawable.bg_grey);                  mButtons[yPos][xPos] = mBut;                 mGridLayout.addView(mBut);             }         }                  mUpText = (TextView) findViewById(R.id.upper_scoreboard);         mLowText = (TextView) findViewById(R.id.lower_scoreboard);          //расположим кнопки с цифрами равномерно внутри mGridLayout         mGridLayout.getViewTreeObserver().addOnGlobalLayoutListener(                 new ViewTreeObserver.OnGlobalLayoutListener() {                     @Override                     public void onGlobalLayout() {                         setButtonsSize();                         //нам больше не понадобится OnGlobalLayoutListener                         mGridLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);                     }                 });          game = new Game(this, MATRIX_SIZE); //создаем класс игры         game.startGame(); //и запускаем ее      }//onCreate      private void setButtonsSize() {         int pLength;         final int MARGIN = 6;          int pWidth = mGridLayout.getWidth();         int pHeight = mGridLayout.getHeight();         int numOfCol = MATRIX_SIZE;         int numOfRow = MATRIX_SIZE;          //сделаем mGridLayout квадратом         if (pWidth >= pHeight) pLength = pHeight;         else pLength = pWidth;         ViewGroup.LayoutParams pParams = mGridLayout.getLayoutParams();         pParams.width = pLength;         pParams.height = pLength;         mGridLayout.setLayoutParams(pParams);          int w = pLength / numOfCol;         int h = pLength / numOfRow;          for (int yPos = 0; yPos < MATRIX_SIZE; yPos++) {             for (int xPos = 0; xPos < MATRIX_SIZE; xPos++) {                 GridLayout.LayoutParams params = (GridLayout.LayoutParams)                         mButtons[yPos][xPos].getLayoutParams();                 params.width = w - 2 * MARGIN;                 params.height = h - 2 * MARGIN;                 params.setMargins(MARGIN, MARGIN, MARGIN, MARGIN);                 mButtons[yPos][xPos].setLayoutParams(params);                 //Log.w(TAG, "process goes in customizeMatrixSize");             }         }     }      //MyButton.MyOnClickListener интерфейс     //*************************************************************************     @Override     public void OnTouchDigit(MyButton v) {         game.OnUserTouchDigit(v.getIdY(), v.getIdX());     }      //Game.ResultsCallback интерфейс     //*************************************************************************     @Override     public void changeLabel(boolean upLabel, int points) {         if (upLabel) mUpText.setText(String.format("Бот: %d", points));         else mLowText.setText(String.valueOf(String.format("Вы: %d", points)));     }      @Override     public void changeButtonBg(int y, int x, boolean row, boolean active) {          if (active) {             if (row) mButtons[y][x].setBackgroundResource(R.drawable.bg_blue);             else mButtons[y][x].setBackgroundResource(R.drawable.bg_red);          } else {             mButtons[y][x].setBackgroundResource(R.drawable.bg_grey);         }     }      @Override     public void setButtonText(int y, int x, int text) {         mButtons[y][x].setText(String.valueOf(text));     }      @Override     public void changeButtonClickable(int y, int x, boolean clickable) {         mButtons[y][x].setClickable(clickable);     }      @Override     public void onResult(int playerOnePoints, int playerTwoPoints) {          String text;         if (playerOnePoints > playerTwoPoints) text = "вы победили";         else if (playerOnePoints < playerTwoPoints) text = "бот победил";         else text = "ничья";          Toast.makeText(this, text, Toast.LENGTH_SHORT).show();          //через 1500 миллисекунд выполним метод run         new Handler().postDelayed(new Runnable() {             @Override             public void run() {                 recreate(); //начать новую игру — пересоздать класс MainActivity             }         }, 1500);     }      @Override     public void onClick(final int y, final int x, final boolean flyDown) {          final Button currentBut = mButtons[y][x];          currentBut.setAlpha(0.7f);         currentBut.setClickable(false);          AnimationSet sets = new AnimationSet(false);         int direction = flyDown ? 400 : -400;         TranslateAnimation animTr = new TranslateAnimation(0, 0, 0, direction);         animTr.setDuration(810);         AlphaAnimation animAl = new AlphaAnimation(0.4f, 0f);         animAl.setDuration(810);         sets.addAnimation(animTr);         sets.addAnimation(animAl);         currentBut.startAnimation(sets);          new Handler().postDelayed(new Runnable() {             @Override             public void run() {                  currentBut.clearAnimation();                 currentBut.setAlpha(0);             }         }, 800);     } } 

Финишная прямая

Можете запускать проект. Если что-то пойдет не так, пишите в комментариях или в личку. На всякий случай, еще раз даю ссылку на гитхаб. Буду рад услышать идеи по боту и замечания по коду.

ссылка на оригинал статьи http://habrahabr.ru/post/271899/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *