Developers Club geek daily blog

1 year, 2 months ago
When I only undertook programming (3 months ago), I quickly understood that it is better at once to begin to be engaged in the projects. It is impossible to be all day long at books or courses, but if you begin to do something special, then easily stay behind development from morning to the morning.

This article — the small tutorial how to make logical game with a bot. Game will look here so:


* I will in detail describe rules once again in the section about AI.

Conditionally I separate readers of article into three groups.
  1. Began to program a few hours ago.
    To you it will be difficult, better previously complete some small course on introduction to Android-development, deal with two-dimensional arrays and interfaces. And then load the project from a gitkhab. Comments and this article will help you to understand as as works.
  2. Already you are able to program, but you cannot call yourself experienced yet.
    It will be interesting to you because you very quickly will be able to make the game. I undertook a dirty job on creation of logic of game and an ui-component, I leave you creative part. You can make other mode of game (2 on 2, online, etc.), to change algorithms of a bot, to create levels, etc.
  3. Experienced.
    To you can be interesting to think over AI — to write it not so easily as it seems at first sight. Also I would be very glad to receive from you notes on a code — is sure, I made not all optimum.


Prelude


Now I again will create the project (that to miss nothing) and I will consistently describe all steps. I will try to write a code in a good form, but there will be also bad places to which I went for the sake of reduction of volume.

Let's follow the following plan:
  • Let's create the project
  • Let's write a bot
  • Let's write a class for game
  • Let's be engaged in ui

We create the project


In total as usual: we create the new project, the further-further-further-finish. Considering that the part of audience can be provided by Began to Program a Few Hours Ago group, I will provide the detailed instruction.

Instruction
Turn attention, the project is done in Android Studio.
Instead of "livermor" specify something special in Company Domain
We write tactical game about digits under Android

We write tactical game about digits under Android

We write tactical game about digits under Android

Change at the top of Android for Project. On a screen the example is given as well as where to create classes.
We write tactical game about digits under Android

We write a bot


Let's begin with the most complex and most interesting challenge — we will write a class for a bot.
You can watch video once again and think, as if you implemented algorithms.
Just in case I will provide rules once again:
rules
rivals go in turn. One plays in lines, another for ranks. The number selected by one player increases to its points and defines a number (line) of the courses for another. It is impossible to go to the same place two times in a row. That who has more points on the end of the game wins (when there is no possible course left).

The first idea which to me came — to count all courses up to the end. Or to course n-go. But how to count the courses? Let's enter concept of the best course. For certain, it is such course which maximizes a difference between your course and the best course of the rival. That is you count the best course, that the rival will count your best course, expecting that you count the best course, being based … And so to n. The latest course will represent just maximum number among.

How in your opinion, normal it is algorithm for a bot?

Actually, it is even worse, than just to select a maximum.
You already guessed in what a problem?

The matter is that we assume that the rival will make this best course. We can select-2, expecting that the rival will take-3 (its best course which will come true at the end of a batch), but the rival will take and will go in +6. Then all of us will count and we will go in-5, expecting that the rival descends in-4, and it will take again and will select +8. And so on — we always you make the long-term courses, and we always lose here and now.

The easiest way to make this algorithm operable — to deliver to n = 2. That is to assume that the rival will just select a maximum from the row, and to look for such course which maximizes a difference between our courses. By the way, it will make a bot quite competitive.

I went a little further and tried to make a bot more human — gave it greed. In other words, I ordered to a bot to do the short-term courses if it is possible to retrieve a difference in the specified quantity of points. In a code I called this difference a jackpot, and the bot breaks a jackpot if on the planning horizon it does not lead to early defeat (in code comments I described everything in more detail).

And the last before you create a class for a bot, I will describe more in details that it is.
The bot is necessary for the class Game to get a number of the course (in line or among). All data which change throughout game — points of players, a Boolean matrix with the permitted courses, number of the last perfect course — will be stored in the class Game. Respectively, creating entity of a class Bot, we need to give it only things, unchangeable during one batch: whether plays a bot in lines or for ranks and a matrix with numbers.

Bot has one public a method — to make the course which we address every time when we want to receive the course. Respectively, we transmit all changeable values to this method.

the address to those who program several hours
The fact that I called protected can be used for inheritance — that is creations of children of a bot,
public — for use of other classes,
private — intrigues about which it is better for other classes not to know.

If you practically understand nothing — it is normal, I also passed the first tutorials.
The class for Bot — the most difficult, will be easier further.

bot code
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;
    }
}



We write a class for game


In the beginning I warned that there will be bad places in a code. And here one of them. Instead of at first writing parent class for management of game, and then to expand it to a specific class of game with a bot, I will write a game class with a bot at once. I do it for reduction of the tutorial.

Game class, we will call it Game, needs two things:
1. The interface for work with ui-elements;
2. Matrix size.

the address to those who program several hours
Carefully, in the class Game AsyncTask and Handler are used — or deal with them previously, or just do not pay attention to them. If briefly, it is convenient classes for use of flows. In the android it is impossible to change interface elements not from the main flow. The classes stated above allow to solve this problem.

game code
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);
    }
}


We work on the user interface


The final part remained — to connect logic of game with the user interface. There will be less comments and explanations, we will just make step by step all necessary things.
Mark for those who program several hours
Be convinced that above at you costs Project, but not Android.
At the moment we have 3 classes: Bot and Game and already existing class MainActivity created by us. Now we should change several XML documents (which are led round by red), to create one more class for digits buttons and to create a drawable-element (I show a black arrow as it becomes).
We write tactical game about digits under Android

1. We prohibit the screen to turn:

We add to AndroidManifest under MainActivity — android:screenOrientation= "portrait"
We do to prohibit to overturn the screen (for simplification of the tutorial).
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. We add colors necessary to us:

We come into colors.xml, we delete the available colors, we add these:
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. We change an application subject:

In styles.xml we replace Theme.AppCompat.Light.DarkActionBar with 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. We set the sizes:

Let's replace the sizes in dimens.xml with the following::
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. We create backgrounds for buttons:

It is necessary to create three xml in the drawable folder:
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. We change the screen model:

For a matrix I will use GridLayout — perhaps, not the best solution, but it seemed to me quite simple and short.

Just replace the available code with mine — there empty GridLayout (we will fill it with a code in MainActivity) and two TextView-elements for indicators of points of players (RelativeLayout in other RelativeLayout — to align everything to the center in vertical direction. View "center" — for alignment of indicators of points to the center in horizontal direction).

Yes, also do not worry, in preview you will see nothing, except an upper text of Bot, and has to be.
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. We create the class MyButton inheriting Button:

We create the class for buttons that it was more convenient to receive coordinates of each button in a matrix.
Code
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. And, at last, we will edit the class MainActivity:

Code
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);
    }
}


Finish


You can start the project. If something goes not so, write in comments or to a pm. Just in case, once again I give the reference on gitkhab. I will be glad to hear ideas on a bot and notes on a code.

This article is a translation of the original post at habrahabr.ru/post/271899/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus