Когда я только взялся за программирование (3 месяца назад), я быстро понял, что лучше сразу начинать заниматься своими проектами. Невозможно с утра до вечера сидеть за книгами или курсами, но если вы начнете делать что-то свое, то запросто просидите за разработкой с утра до утра.
Эта статья — небольшой туториал по тому, как сделать логическую игру с ботом. Игра будет выглядеть вот так:
*Подробно опишу правила еще раз в разделе про ИИ.
Читателей статьи условно разделяю на три группы.
- Начали программировать несколько часов назад.
Вам будет сложно, лучше предварительно пройдите какой-нибудь небольшой курс по введению в Android-разработку, разберитесь с двумерными массивами и интерфейсами. А потом загрузите проект с гитхаба. Комментарии и эта статья помогут вам разобраться, что и как работает. - Уже умеете программировать, но еще не можете назвать себя опытными.
Вам будет интересно, потому что вы очень быстро сможете сделать свою игру. Я взял на себя грязную работенку по построению логики игры и ui-составляющей, вам же оставляю творческую часть. Вы можете сделать другой режим игры (2 на 2, онлайн и т.п.), изменить алгоритмы бота, создать уровни и т.д. - Опытные.
Вам может быть интересно подумать над ИИ — написать его не так легко, как кажется на первый взгляд. Так же я был бы очень рад получить от вас замечания по коду — уверен, далеко не все я сделал оптимально.
Прелюдия
Сейчас я заново создам проект (чтобы ничего не упустить) и последовательно опишу все шаги. Постараюсь писать код в хорошем тоне, но будут и плохие места, на которые я пошел ради сокращения объема.
Будем следовать следующему плану:
- Создадим проект
- Напишем бота
- Напишем класс для игры
- Займемся ui
Создаем проект
Все как обычно: создаем новый проект, далее-далее-далее-финиш. Учитывая, что часть аудитории может быть представлена группой «Начали программировать несколько часов назад», приведу подробную инструкцию.
Вместо «livermor» в Company Domain укажите что-то свое
Поменяйте вверху Android на Project. На скрине приведен пример, как и где создавать классы.
Пишем бота
Начнем с самой сложной и самой интересной задачи — напишем класс для бота.
Можете посмотреть видео еще раз и подумать, как бы вы реализовали алгоритмы.
На всякий случай приведу правила еще раз:
Первая идея, которая мне пришла, — просчитать все ходы до конца. Либо до n-го хода. Но как просчитывать ходы? Давайте введем понятие лучшего хода. Наверняка, это такой ход, который максимизирует разницу между вашим ходом и лучшим ходом соперника. То есть вы просчитываете свой лучший ход, основываясь на том, что соперник будет просчитывать ваш лучший ход, ожидая, что вы просчитываете свой лучший ход, основываясь… И так до n. Самый последний ход будет представлять собой просто максимальное число в ряду.
Как по-вашему, нормальный это алгоритм для бота?
На самом деле, это даже хуже, чем просто выбирать максимум.
Вы уже догадались, в чем проблема?
Дело в том, что мы предполагаем, что соперник будет совершать этот лучший ход. Мы можем выбрать -2, ожидая, что соперник возьмет -3 (его лучший ход, который оправдается в конце партии), но соперник возьмет да и пойдет в +6. Тогда мы все пересчитаем и пойдем в -5, ожидая, что соперник сходит в -4, а он опять возьмет да и выберет +8. И так далее — мы всегда совершаете долгосрочные ходы, и всегда проигрываем здесь и сейчас.
Самый простой способ сделать этот алгоритм работоспособным — поставить n = 2. То есть предполагать, что соперник просто выберет максимум из своего ряда, и самим искать такой ход, который максимизирует разницу между нашими ходами. К слову, это сделает бота вполне конкурентным.
Я пошел немного дальше и попробовал сделать бота человечнее — дал ему жадность. Другими словами, я приказал боту делать краткосрочные ходы, если можно извлечь разницу в указанное количество очков. В коде я обозвал эту разницу джекпотом, и бот срывает джекпот, если на горизонте планирования это не приведет к досрочному поражению (в комментариях к коду я все описал подробнее).
И последнее, прежде чем вы будете создавать класс для бота, опишу детальнее, что он из себя представляет.
Бот необходим классу Игра для того, чтобы получить номер хода (в строке или в ряду). Все данные, которые изменяются на протяжении игры — очки игроков, булева матрица с разрешенными ходами, номер последнего совершенного хода — будут храниться в классе Игра. Соответственно, создавая сущность класса Бот, нам необходимо передать ему только неизменяемые в течение одной партии вещи: играет ли бот за строки или за ряды и матрицу с числами.
У Бота есть один public метод — сделать ход, к которому мы обращаемся каждый раз, когда хотим получить ход. Соответственно, в этот метод мы передаем все изменяемые значения.
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. Размер матрицы.
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); } }
Работаем над пользовательским интерфейсом
Осталась заключительная часть — связать логику игры с пользовательским интерфейсом. Здесь будет меньше комментариев и пояснений, мы просто сделаем шаг за шагом все необходимые вещи.
На данный момент у нас есть 3 класса: созданные нами Bot и Game и уже существующий класс MainActivity. Сейчас нам предстоит изменить несколько xml-документов (обведенных красным), создать еще один класс для цифр-кнопок и создать drawable-элемент (показываю черной стрелкой, как это делается).
1. Запрещаем экрану поворачиваться:
В AndroidManifest добавляем под MainActivity — android:screenOrientation=«portrait»
Делаем для того, чтобы запретить переворачивать экран (для упрощения туториала).
<?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, удаляем имеющиеся цвета, добавляем эти:
<?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:
<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 на следующие::
<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:
<?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>
<?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>
<?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 вы ничего не увидите, кроме верхней надписи Бот, так и должно быть.
<?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/
Добавить комментарий