Как сделать крестики нолики в юнити

Обновлено: 07.07.2024

Как и профессиональный шахматист, этот алгоритм просчитывает действия соперника на несколько ходов вперёд — до тех пор, пока не достигнет конца партии, будь то победа, поражение или ничья. Попав в это конечное состояние, ИИ начислит себе положительное количество очков (в нашем случае +10) за победу, отрицательное (-10) — за поражение, и нейтральное (0) — за ничью.

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

Cube Dev , Удалённо , От 8000 $

Попробуйте сыграть вот в такую игру.

See the Pen Минимакс by Ahmad Abdolsaheb (@abdolsa) on CodePen.

  1. возвращает значение, если найдено конечное состояние (+10, 0, -10),
  2. проходит по всем пустым клеткам на поле,
  3. вызывает минимакс-функцию для каждой из них (рекурсия),
  4. оценивает полученные значения
  5. и возвращает наилучшее из них.

Если вы не знакомы с рекурсией, то вам стоит посмотреть эту лекцию из гарвардского курса CS50:

Чтобы разобраться в том, как устроен минимакс, давайте напишем его реализацию и смоделируем его поведение. Займёмся этим в двух следующих разделах.

Реализация минимакса

Мы рассмотрим ситуацию, когда игра подходит к концу (смотрите картинку ниже). Поскольку минимакс проходит по всем возможным состояниям игры (а их сотни тысяч), имеет смысл рассматривать эндшпиль — так нам придётся отслеживать меньшее количество рекурсивных вызовов функции (всего 9).

Пусть ИИ играет крестиками, человек — ноликами.

минимакс

Чтобы упростить работу с полем, объявим его как массив из 9 элементов со значениями, равными содержимому клеток. Заполним его крестиками и ноликами, как на картинке выше, и назовём origBoard .

Затем объявим переменные aiPlayer и huPlayer и присвоим им значения "X" и "O" соответственно.

Кроме того, нам потребуется функция, которая ищет победные комбинации и возвращает истинное значение в случае успешного поиска, и функция, которая хранит индексы доступных клеток.

Итак, давайте определим минимакс-функцию с двумя аргументами: newBoard (новое поле) и player (игрок). Затем найдём индексы свободных клеток на поле и передадим их в переменную availSpots .

После этого нужно собрать очки с каждой из пустых клеток. Для этого создадим массив ходов moves и пройдём в цикле по всем пустым клеткам, помещая индексы и очки каждого хода в объект move .

Затем зададим индекс пустой клетки, который хранился в виде числа в origBoard , равным свойству-индексу объекта move . Потом сходим за текущего игрока на пустую клетку нового поля newBoard и вызовем функцию minimax от другого игрока и получившегося поля newBoard . После этого нужно поместить свойство score объекта, возвращённого функцией minimax , в свойство score объекта move .

И наконец, функция сбрасывает изменения newBoard и помещает объект move в массив moves .

Затем минимаксу нужно выбрать наилучший ход move из массива moves . Ему нужен move с наибольшим счётом, если ходит ИИ, и с наименьшим, если это ход человека. Таким образом, если значение player равно aiPlayer , алгоритм инициализирует переменную bestScore очень маленьким числом и идёт циклом по массиву moves : если ход move приносит больше очков score , чем bestScore , алгоритм запоминает этот move . В случае ходов с одинаковыми очками алгоритм запоминает первый из них.

В случае, когда player равен huPlayer , всё аналогично — только теперь bestScore инициализируется большим числом, а минимакс ищет ход move с наименьшим количеством очков.

В итоге минимакс возвращает объект, хранящийся в bestMove .

Вот и вся минимакс-функция. Исходный код реализации алгоритма вы можете найти на GitHub и CodePen.

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

Минимакс в действии

Пользуясь схемой ниже, разберем пошаговую модель алгоритма.

Примечание: На схеме большие числа обозначают порядковый номер вызова функции, а уровни — то, на сколько ходов вперёд прошёл алгоритм.

минимакс

  1. Алгоритму подаются origBoard и aiPlayer . Он составляет список из трёх найденных пустых клеток, проверяет конечность состояния, и проходит циклом по всем пустым клеткам. Затем алгоритм меняет newBoard , помещая aiPlayer в первую пустую клетку. После этого он вызывает сам себя от newBoard и huPlayer и ждёт, пока второй вызов вернёт значение.
  2. Пока первый вызов функции всё ещё работает, запускается второй, создавая список из двух пустых клеток, проверяя конечность состояния и проходя циклом по всем пустым клеткам. Затем второй вызов изменяет newBoard , помещая huPlayer в первую пустую клетку. После этого он вызывает сам себя от newBoard и aiPlayer и ждёт, пока третий вызов вернёт значение.
  3. Алгоритм составляет список пустых клеток и фиксирует победу игрока после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным (-10).

Поскольку второй вызов обнаружил две пустые клетки, минимакс изменяет newBoard , помещая huPlayer во вторую свободную клетку. Затем он вызывает сам себя от newBoard и aiPlayer .

Во втором вызове функции алгоритм получает значения, возвращённые с нижнего уровня третьим и четвёртым вызовами функции. Поскольку ход huPlayer принёс эти два результата, алгоритм выбирает наименьший из них. Так как они одинаковы, алгоритм выбирает первый и передаёт его первому вызову функции.

На этот момент первый вызов функции получил оценку хода aiPlayer в первую пустую клетку. Затем он изменяет newBoard , помещая aiPlayer во вторую пустую клетку. После этого он вызывает сам себя от newBoard и huPlayer .

После этого первый вызов изменяет newBoard , помещая aiPlayer в третью пустую клетку. Затем он вызывает сам себя от newBoard и huPlayer .

Седьмой вызов получил лишь одно, положительное значение от нижних уровней. Поскольку это значение было получено в ход aiPlayer , алгоритм возвращает наибольшее из полученных значений. Поэтому он возвращает положительное значение (+10) на уровень выше, шестому вызову.

Поскольку шестой вызов обнаружил две пустых клетки, минимакс изменяет newBoard , помещая huPlayer во вторую пустую клетку. Затем он вызывает сам себя от newBoard и aiPlayer .

На этом этапе шестой вызов должен выбрать между счётом (+10), который вернул седьмой вызов, и счётом (-10), который вернул девятый вызов. Поскольку ход huPlayer принёс эти два результата, алгоритм выбирает наименьший из них и возвращает его на уровень выше в виде объекта с полями счёта и индекса.

Наконец, все три ветви первого вызова оцениваются (-10, +10, -10). Поскольку ход aiPlayer принёс эти три результата, алгоритм выбирает объект, содержащий наибольшее количество очков (+10) и его индекс (4).

В рассмотренном выше сценарии минимакс решает, что оптимальным выбором будет ход в центральную клетку поля.

Выводы

К этому моменту вы должны были понять, как устроен алгоритм минимакс. Попробуйте написать его реализацию самостоятельно или посмотрите пример на GitHub или CodePen и оптимизируйте его.

Если вас заинтересовала тема ИИ в играх, советуем почитать наши материалы по этой теме:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters

package test ;
import java.util.Scanner ;
public class PlusZerro
// Размер игровово поля
private static final byte bPoleSize = 3 ;
// Задаем поле как (!)одномерный массив bPoleSize*bPoleSize (неожиданно?)
private static String [] pole = new String [bPoleSize * bPoleSize];
// А тут у нас задается текущий игрок
private static byte bPlayerNum = 0 ;
public static void main ( String [] args ) throws Exception
int iTmp = 0 ;
Scanner sc = new Scanner ( System . in);
// Инициализация поля
for ( int i = 0 ; i bPoleSize * bPoleSize; i ++ )
pole[i] = Integer . toString( ++ iTmp);
// Играем, пока не наступит конец игры
while ( ! isGameEnd())
nextPlayer();
while ( true )
System . out . println( " \n Ход игрока " + bPlayerNum);
showPole(); // Рисуем поле
System . out . print( " Наберите число, куда вы хотите вставить " + ( 1 == bPlayerNum ? " крестик " : " нолик " ) + " : " );
if (sc . hasNextInt()) < // проверяем, есть ли в потоке целое число
iTmp = sc . nextInt() - 1 ; // считывает целое число с потока ввода и сохраняем в переменную
if (isValidInput(iTmp))
break ;
>
System . out . println( " Вы ввели неправильное число. Повторите ввод " );
sc . next();
>
try
putX(iTmp); // Вставляем на поле крестик или нолик
> catch ( Exception e)
System . out . println( " Что-то пошло не так ;( " );
>
>
showPole();
>
/**
* Проверяем корректность ввода. Введенное число должно быть по размеру поля
* и поле должно быть в этом месте еще не заполнено
*/
private static boolean isValidInput ( int iIn )
if (iIn >= bPoleSize * bPoleSize) return false ;
if (iIn 0 ) return false ;
switch (getX(iIn))
case ' O ' :
case ' X ' :
return false ;
>
return true ;
>
/**
* Функция задает номер следующего игрока
*/
private static void nextPlayer ()
bPlayerNum = ( byte ) ( 1 == bPlayerNum ? 2 : 1 );
>
/**
* Определяем, наступил конец игры или нет
* Условия:
* 1) Победили крестики
* 2) Победили нолики
* 3) Кончились ходы
*/
private static boolean isGameEnd ()
int i, j;
boolean bRowWin = false , bColWin = false ;
// Проверка победы на колонках и столбиках
for (i = 0 ; i bPoleSize; i ++ )
bRowWin = true ;
bColWin = true ;
for (j = 0 ; j bPoleSize - 1 ; j ++ )
bRowWin &= (getXY(i,j) . charAt( 0 ) == getXY(i,j + 1 ) . charAt( 0 ));
bColWin &= (getXY(j,i) . charAt( 0 ) == getXY(j + 1 ,i) . charAt( 0 ));
>
if (bColWin || bRowWin)
System . out . println( " Победил игрок " + bPlayerNum);
return true ;
>
>
// Проверка победы по диагоналям
bRowWin = true ;
bColWin = true ;
for (i = 0 ; i bPoleSize - 1 ; i ++ )
bRowWin &= (getXY(i,i) . charAt( 0 ) == getXY(i + 1 ,i + 1 ) . charAt( 0 ));
bColWin &= (getXY(i, bPoleSize - i - 1 ) . charAt( 0 ) == getXY(i + 1 , bPoleSize - i - 2 ) . charAt( 0 ));
>
if (bColWin || bRowWin)
System . out . println( " Победил игрок " + bPlayerNum);
return true ;
>
// Проверка существования новых ходов
for (i = 0 ; i bPoleSize * bPoleSize; i ++ )
switch (getX(i))
case ' O ' :
case ' X ' :
break ;
default :
return false ;
>
>
if (bPoleSize * bPoleSize i)
System . out . println( " Ничья. Кончились ходы. " );
return true ;
>
// Продолжаем игру
return false ;
>
/**
* Получает значение координаты на поле
*/
private static String getXY ( int x , int y )
return pole[x * bPoleSize + y];
>
/**
* Получает значение координаты на поле
*/
private static char getX ( int x )
return pole[x] . charAt( 0 );
>
/**
* Вставляет на поле крестик или нолик
*/
private static void putX ( int x )
pole[x] = 1 == bPlayerNum ? " X " : " O " ;
>
/**
* Вывести игровое поле
*/
private static void showPole ()
for ( int i = 0 ; i bPoleSize; i ++ )
for ( int j = 0 ; j bPoleSize; j ++ )
System . out . printf( " %4s " , getXY(i, j));
>
System . out . print( " \n " );
>
>
>

You can’t perform that action at this time.

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.

Описание игры:
Классические крестики-нолики, но с возможностью играть как с компютером, так и с живым человеком по интернету.

Стиль графики:
На фоне - листок тетради в клетку, небрежно начерченные от руки клетки для игры, такие-же небрежные крестик и нолик.

Иерархия страниц:


Вот так будет выглядеть плеер на сайте(фон в клеточку, в квадратике сам плеер с игрой):


На разработку берем месяц, на такой проект месяца, чтобы довести игру до ума, хватит вполне.
На данный момент нужен художник. Графики, как я уже объяснил, минимум. Оплаты нет, ибо проект для фана. Имя в титрах будет.
Оплачивать хостинг, домен буду я.





Так что же такое Bolt? А главное – зачем и кому может пригодится?

Зачем же тогда Unity создали этот инструмент?

На самом деле все довольно просто. Основная задача визуального программирования – это наладить тесное сотрудничество между программистами и теми членами команды, которые далеки от понимания кода (художники и дизайнеры). Используя визуальные сценарии будет намного проще выполнять такие задачи как:

  • создание события диалога между персонажами
  • добавление новых навыков игрока
  • создание визуальных эффектов
  • добавление ловушек на уровни
  • регулирование триггеров
  • подбор анимации для сцены и многое другое.

При этом Bolt дает возможность совмещать и визуальные сценарии и код, если это необходимо. Либо открывать и редактировать код уже готовых визуальных узлов. Вся суть сводится к тому, чтобы предоставить разработчику возможность и инструменты для наиболее быстрого и оптимального создания игр тем методом, который ему представляется наиболее удобным.

Практически сразу после релиза первой версии Bolt, Unity анонсировали разработку Bolt 2, которая учтёт все возможные замечания и недостатки первой версии и расширит его базовые возможности, масштабируемость, производительность и простоту использования. Примечательно, что новые версии Bolt будут автоматически добавляться во все текущие Unity планы для всех пользователей совершенно бесплатно.

Но если Вы графический дизайнер, либо обладаете самыми начальными знаниями в написании кода, то это именно то, что Вам нужно. Этот инструмент даст Вам возможность целиком и полностью сосредоточится на визуальных эффектах и логике построения игры.

Пошаговая инструкция по работе с Bolt доступна в официальном туториале от Unity. Но чтобы сделать этот инструмент понятным русскоязычной аудитории, UNITY3DSCHOOL обязательно запишет обучающий курс, посвященный данной теме.

Читайте также: