Когда я только взялся за программирование (3 месяца назад), я быстро понял, что лучше сразу начинать заниматься своими проектами. Невозможно с утра до вечера сидеть за книгами или курсами, но если вы начнете делать что-то свое, то запросто просидите за разработкой с утра до утра.
Эта статья — небольшой туториал по тому, как сделать логическую игру с ботом. Игра будет выглядеть вот так:
*Подробно опишу правила еще раз в разделе про ИИ.
Читателей статьи условно разделяю на три группы.
Сейчас я заново создам проект (чтобы ничего не упустить) и последовательно опишу все шаги. Постараюсь писать код в хорошем тоне, но будут и плохие места, на которые я пошел ради сокращения объема.
Будем следовать следующему плану:
Все как обычно: создаем новый проект, далее-далее-далее-финиш. Учитывая, что часть аудитории может быть представлена группой «Начали программировать несколько часов назад», приведу подробную инструкцию.
Начнем с самой сложной и самой интересной задачи — напишем класс для бота.
Можете посмотреть видео еще раз и подумать, как бы вы реализовали алгоритмы.
На всякий случай приведу правила еще раз:
Первая идея, которая мне пришла, — просчитать все ходы до конца. Либо до n-го хода. Но как просчитывать ходы? Давайте введем понятие лучшего хода. Наверняка, это такой ход, который максимизирует разницу между вашим ходом и лучшим ходом соперника. То есть вы просчитываете свой лучший ход, основываясь на том, что соперник будет просчитывать ваш лучший ход, ожидая, что вы просчитываете свой лучший ход, основываясь… И так до n. Самый последний ход будет представлять собой просто максимальное число в ряду.
Как по-вашему, нормальный это алгоритм для бота?
На самом деле, это даже хуже, чем просто выбирать максимум.
Вы уже догадались, в чем проблема?
Дело в том, что мы предполагаем, что соперник будет совершать этот лучший ход. Мы можем выбрать -2, ожидая, что соперник возьмет -3 (его лучший ход, который оправдается в конце партии), но соперник возьмет да и пойдет в +6. Тогда мы все пересчитаем и пойдем в -5, ожидая, что соперник сходит в -4, а он опять возьмет да и выберет +8. И так далее — мы всегда совершаете долгосрочные ходы, и всегда проигрываем здесь и сейчас.
Самый простой способ сделать этот алгоритм работоспособным — поставить n = 2. То есть предполагать, что соперник просто выберет максимум из своего ряда, и самим искать такой ход, который максимизирует разницу между нашими ходами. К слову, это сделает бота вполне конкурентным.
Я пошел немного дальше и попробовал сделать бота человечнее — дал ему жадность. Другими словами, я приказал боту делать краткосрочные ходы, если можно извлечь разницу в указанное количество очков. В коде я обозвал эту разницу джекпотом, и бот срывает джекпот, если на горизонте планирования это не приведет к досрочному поражению (в комментариях к коду я все описал подробнее).
И последнее, прежде чем вы будете создавать класс для бота, опишу детальнее, что он из себя представляет.
Бот необходим классу Игра для того, чтобы получить номер хода (в строке или в ряду). Все данные, которые изменяются на протяжении игры — очки игроков, булева матрица с разрешенными ходами, номер последнего совершенного хода — будут храниться в классе Игра. Соответственно, создавая сущность класса Бот, нам необходимо передать ему только неизменяемые в течение одной партии вещи: играет ли бот за строки или за ряды и матрицу с числами.
У Бота есть один public метод — сделать ход, к которому мы обращаемся каждый раз, когда хотим получить ход. Соответственно, в этот метод мы передаем все изменяемые значения.
Вначале я предупреждал, что будут плохие места в коде. И вот одно из них. Вместо того, чтобы сперва написать родительский класс для управления игрой, а потом расширить его до конкретного класса игры с ботом, я сразу напишу класс игры с ботом. Делаю это для сокращения туториала.
Игровой класс, назовем его Game, нуждается в двух вещах:
1. Интерфейс для работы с ui-элементами;
2. Размер матрицы.
Осталась заключительная часть — связать логику игры с пользовательским интерфейсом. Здесь будет меньше комментариев и пояснений, мы просто сделаем шаг за шагом все необходимые вещи.
В AndroidManifest добавляем под MainActivity — android:screenOrientation=«portrait»
Делаем для того, чтобы запретить переворачивать экран (для упрощения туториала).
Заходим в colors.xml, удаляем имеющиеся цвета, добавляем эти:
В styles.xml заменяем Theme.AppCompat.Light.DarkActionBar на Theme.AppCompat.Light.NoActionBar:
Заменим размеры в dimens.xml на следующие::
Нужно создать три xml в папке drawable:
Для матрицы я буду использовать GridLayout — возможно, не самое лучшее решение, но оно показалось мне довольно простым и коротким.
Просто замените имеющийся код на мой — там пустой GridLayout (заполним его кодом в MainActivity) и два TextView-элемента для показателей очков игроков (RelativeLayout внутри другого RelativeLayout — для того, чтобы выравнять все по центру по вертикали. View «center» — для выравнивания показателей очков к центру по горизонтали).
Да, и не беспокойтесь, в preview вы ничего не увидите, кроме верхней надписи Бот, так и должно быть.
Создаем свой класс для кнопок, чтобы удобнее было получать координаты каждой кнопки в матрице.
Можете запускать проект. Если что-то пойдет не так, пишите в комментариях или в личку. На всякий случай, еще раз даю ссылку на гитхаб. Буду рад услышать идеи по боту и замечания по коду.
Эта статья — небольшой туториал по тому, как сделать логическую игру с ботом. Игра будет выглядеть вот так:
*Подробно опишу правила еще раз в разделе про ИИ.
Читателей статьи условно разделяю на три группы.
- Начали программировать несколько часов назад.
Вам будет сложно, лучше предварительно пройдите какой-нибудь небольшой курс по введению в Android-разработку, разберитесь с двумерными массивами и интерфейсами. А потом загрузите проект с гитхаба. Комментарии и эта статья помогут вам разобраться, что и как работает. - Уже умеете программировать, но еще не можете назвать себя опытными.
Вам будет интересно, потому что вы очень быстро сможете сделать свою игру. Я взял на себя грязную работенку по построению логики игры и ui-составляющей, вам же оставляю творческую часть. Вы можете сделать другой режим игры (2 на 2, онлайн и т.п.), изменить алгоритмы бота, создать уровни и т.д. - Опытные.
Вам может быть интересно подумать над ИИ — написать его не так легко, как кажется на первый взгляд. Так же я был бы очень рад получить от вас замечания по коду — уверен, далеко не все я сделал оптимально.
Прелюдия
Сейчас я заново создам проект (чтобы ничего не упустить) и последовательно опишу все шаги. Постараюсь писать код в хорошем тоне, но будут и плохие места, на которые я пошел ради сокращения объема.
Будем следовать следующему плану:
- Создадим проект
- Напишем бота
- Напишем класс для игры
- Займемся ui
Создаем проект
Все как обычно: создаем новый проект, далее-далее-далее-финиш. Учитывая, что часть аудитории может быть представлена группой «Начали программировать несколько часов назад», приведу подробную инструкцию.
Инструкция
Обратите внимания, проект делается в Android Studio.
Вместо «livermor» в Company Domain укажите что-то свое
Поменяйте вверху Android на Project. На скрине приведен пример, как и где создавать классы.
Вместо «livermor» в Company Domain укажите что-то свое
Поменяйте вверху Android на Project. На скрине приведен пример, как и где создавать классы.
Пишем бота
Начнем с самой сложной и самой интересной задачи — напишем класс для бота.
Можете посмотреть видео еще раз и подумать, как бы вы реализовали алгоритмы.
На всякий случай приведу правила еще раз:
правила
соперники ходят по очереди. Один играет за строки, другой за ряды. Выбранное одним игроком число прибавляется к его очкам и определяет ряд(строку) ходов для другого. Ходить в одно и то же место два раза подряд нельзя. Побеждает тот, у кого больше очков на конец игры (когда не осталось возможных ходов).
Первая идея, которая мне пришла, — просчитать все ходы до конца. Либо до n-го хода. Но как просчитывать ходы? Давайте введем понятие лучшего хода. Наверняка, это такой ход, который максимизирует разницу между вашим ходом и лучшим ходом соперника. То есть вы просчитываете свой лучший ход, основываясь на том, что соперник будет просчитывать ваш лучший ход, ожидая, что вы просчитываете свой лучший ход, основываясь… И так до n. Самый последний ход будет представлять собой просто максимальное число в ряду.
Как по-вашему, нормальный это алгоритм для бота?
На самом деле, это даже хуже, чем просто выбирать максимум.
Вы уже догадались, в чем проблема?
Дело в том, что мы предполагаем, что соперник будет совершать этот лучший ход. Мы можем выбрать -2, ожидая, что соперник возьмет -3 (его лучший ход, который оправдается в конце партии), но соперник возьмет да и пойдет в +6. Тогда мы все пересчитаем и пойдем в -5, ожидая, что соперник сходит в -4, а он опять возьмет да и выберет +8. И так далее — мы всегда совершаете долгосрочные ходы, и всегда проигрываем здесь и сейчас.
Самый простой способ сделать этот алгоритм работоспособным — поставить n = 2. То есть предполагать, что соперник просто выберет максимум из своего ряда, и самим искать такой ход, который максимизирует разницу между нашими ходами. К слову, это сделает бота вполне конкурентным.
Я пошел немного дальше и попробовал сделать бота человечнее — дал ему жадность. Другими словами, я приказал боту делать краткосрочные ходы, если можно извлечь разницу в указанное количество очков. В коде я обозвал эту разницу джекпотом, и бот срывает джекпот, если на горизонте планирования это не приведет к досрочному поражению (в комментариях к коду я все описал подробнее).
И последнее, прежде чем вы будете создавать класс для бота, опишу детальнее, что он из себя представляет.
Бот необходим классу Игра для того, чтобы получить номер хода (в строке или в ряду). Все данные, которые изменяются на протяжении игры — очки игроков, булева матрица с разрешенными ходами, номер последнего совершенного хода — будут храниться в классе Игра. Соответственно, создавая сущность класса Бот, нам необходимо передать ему только неизменяемые в течение одной партии вещи: играет ли бот за строки или за ряды и матрицу с числами.
У Бота есть один public метод — сделать ход, к которому мы обращаемся каждый раз, когда хотим получить ход. Соответственно, в этот метод мы передаем все изменяемые значения.
обращение к тем, кто программирует несколько часов
То, что я обозвал protected, может быть использовано для наследования — то есть создания детей бота,
public — для пользования другими классами,
private — внутренняя кухня, о которой другим классам лучше не знать.
Если вы практически ничего не поймете — это нормально, я так же проходил первые свои туториалы.
Класс для Бота — самый сложный, дальше будет легче.
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-элемент (показываю черной стрелкой, как это делается).
На данный момент у нас есть 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);
}
}
Финиш
Можете запускать проект. Если что-то пойдет не так, пишите в комментариях или в личку. На всякий случай, еще раз даю ссылку на гитхаб. Буду рад услышать идеи по боту и замечания по коду.
Комментарии (8)
missingdays
30.11.2015 16:29Интересная игра. Планируется ли пвп режим?
arturdumchev
30.11.2015 16:59-1Да, собираюсь онлайн-режим добавить попозже.
На одном девайсе уже есть.
arturdumchev
30.11.2015 17:27-1Если не хотите открывать уровни по IQ, то там можно на замок Профессора нажать, и он откроется:
ЗаголовокСодержимое
akalend
30.11.2015 16:37а планируете выложить apk?
arturdumchev
30.11.2015 16:57-1Можно с Google play загрузить — там такая же версия, как на видео. Либо с 4pda скачать apk. Там, правда, грозятся, что закроют тему.
AndrewN
Хм, писал такую же игру под Windows, использовал минимаксный алгоритм.
С регулируемой глубиной-сложностью.
Правда поле у меня было 8х8. При глубине поиска в 4 хода выиграть у бота уже практически невозможно, только если повезет. Но и задлумываться он начинает уже на несколько минут.
Вечером поищу приложение.
AndrewN
Залил сюда: rghost.ru/7bhjV8tXx
Немного запамятовал, все-таки на 4 уровне сложности думает быстро, а 5й был выпилен как раз из-за долгих ходов
Приложение было написано аж в 11 году, просто для интереса, как раз для разработки ИИ к игре
arturdumchev
Спасибо, сегодня гляну.