Крестики-нолики на микроконтроллере
Думаю все знают эту игру и каждый из нас играл в нее, вот и я как-то вспомнил о ней, и задумался над электронным вариантом игры. Первоначально я рассчитывал сделать вариант только для двух игроков, но потом добавил вариант игры против “электронного интеллекта” микроконтроллера.
Игровое поле размером 3х3 состоит из двухцветных светодиодов, красный цвет свечения для крестиков и зеленый соответственно для ноликов.
Микроконтроллер выбрал самый распространенный PIC16F628A, при этом для подключения всех 18 светодиодов у него не хватает линий ввода вывода, поэтому я решил использовать 8-ми разрядные последовательные сдвиговые регистры 74HC164N. С помощью таких регистров можно существенно нарастить количество выходных каналов.
Правда у этих регистров нет внутренней защелки, все загружаемые данные сразу передаются на выходные каналы.
Информация в регистр загружается последовательно, соответственно во время загрузки выходы будут многократно переключатся, что может вызвать нежелательное мерцание всех светодиодов, чтобы избежать этого устанавливается дополнительный транзистор к которому подключаются все светодиоды. Кроме этих регистров есть более продвинутые типа 74НС595, в которых есть внутрення защелка, кроме того выходные каналы можно переключить в высокоомное состояние.
На схеме выходы регистров DD2 и DD3 подключены к анодам светодиодов. Катоды всех светодиодов подключены к коллектору транзистора VT1. Микроконтроллер загружает данные в регистр DD2, при переполнении которого, информация с 13 вывода передается на вход регистра DD3.
После загрузки регистров, на базу транзистора VT1 подается сигнал включения, тем самым катоды светодиодов подключаются к общему проводу. В результате загораются те светодиоды, на анодах которых присутствует напряжение. Светодиод HL4 подключен непосредственно к микроконтроллеру, так как все выходы регистров заняты.
Резисторы R3, R4, R6-R21 ограничивают ток через светодиоды. Резистор R5 устанавливает ток базы транзисторов VT1. Микроконтроллер работает на частоте 4 МГц от внутреннего генератора.
После подачи питания микроконтроллер переходит в режим игры в котором противником игрока является “электронный интеллект” микроконтроллера.
При этом на игровом поле высвечивается символ “+” (плюс) зеленого цвета. Игрок может играть только “крестиками”, которым присвоен красный цвет свечения светодиодов. Микроконтроллер играет “ноликами”, которым соответственно присвоен зеленый цвет свечения светодиодов.
При выборе данного режима игры первой ход делает игрок, в следующей партии первый ход за микроконтроллером, затем опять за игроком, таким образом право первого хода передается по очереди.
Ход крестиками осуществляется с помощью кнопки SB1, после нажатия которой начинает мигать красным цветом светодиод HL1 с частотой 1Гц, тем самым указывая клетку игрового поля на которую можно сделать ход. При повторном нажатии на кнопку SB1, светодиод HL1 гаснет, и начинает мигать светодиод HL2.
При последующих нажатиях поочередно мигают остальные светодиоды поля, после светодиода HL9 снова начинает мигать светодиод HL1. Для того чтобы сделать ход на выбранную клетку необходимо удерживать нажатой кнопку SB1 в течении 1 секунды, после чего светодиод перестанет мигать и будет постоянно гореть красным цветом. После того как микроконтроллер сделает ход, игрок выбирает необходимую клетку как было описано выше и делает ход. Во время выбора клетки игроком, если она занята, то она пропускается, вместо нее мигает следующая свободная клетка.
В случае победы игрока, через секунду после того как сделан последний ход, на игровом поле высвечивется символ “Х” (крестик) красного цвета.
Если побеждает микроконтроллер то высвечивается символ “О” (нолик) зеленого цвета.
Ничья отображается в виде символа “Н” (Ничья), оранжевого цвета, то есть в каждом светодиоде зажигаются оба кристалла, красный и зеленый. Для начала новой игры необходимо нажать кнопку SB1.
Для переключения режима игры необходимо одновременно удерживать нажатыми кнопки SB1 и SB2 в течении 1 секунды. Переключение возможно после подачи питания, а также после завершения каждой партии в обоих режимах.
После перехода в режим игры для двух игроков, на игровом поле высвечивается символ “+” (плюс) красного цвета. Для началы игры необходимо нажать кнопку SB1 или SB2, соответственно первым ходит тот игрок, кто раньше нажмет кнопку.
То же правило справедливо для начала любой следующей партии. Здесь также кнопкой SB1 осуществляется ход крестиками, а кнопкой SB2 ход ноликами.
Выбор клетки для крестиков описан выше, для ноликов справедливы те же действия, с одной разницей, после нажатия кнопки SB2 начинает мигать светодиод HL9, при следующем нажатии HL8, то есть светодиоды переключаются в обратном направлении.
Если в течении 4 минут не были нажаты кнопки SB1 или SB2, то устройство переходит в режим пониженного энергопотребления, микроконтроллер отключает все светодиоды и переходит в спящий режим. Устройство “просыпается” после нажатия кнопки SB1, и возвращается в прежнее состояние.
В устройстве применены резисторы – типоразмера 1206 для поверхностного монтажа, конденсатор С2 – керамический К10-17б. Светодиоды L-59EGW можно заменить аналогичными двухцветными с диаметром корпуса 5мм, кнопки SB1, SB2 тактовые SWT 6×6-7, но подойдут и другие аналогичные.
Все детали смонтированы на печатной плате из двухстороннего фольгированного текстолита.
Источник питания – стабилизированный блок питания напряжением 3,7-5В, также можно использовать гальванические элементы или аккумуляторы, например 3 последовательно соединненные батарейки по 1,5В типоразмера AA или AAA.
Источник: http://radiolaba.ru/microcotrollers/krestiki-noliki-na-mikrokontrollere.html
Крестики-нолики – разработка игры: как создать игру
Вместо того, чтобы долго изучать программирование по статьям и лекциям, которые скучно описывают какие-то непонятные действия с числами и строками, лучше изучать программирование на красочных примерах, таких, как создание игр!
Естественно, вам потребуется хотя бы теоретическое знание о том, что такое: язык программирования, переменная, цикл, массив… Так же, для чтения этой статьи и изучения программы, желательно иметь представление о: JavaScript, HTML, и CSS (CSS не обязательно – требуется только для украшения).
Структура игры
Эта игра состоит из нескольких частей, которые написаны на HTML, CSS и JavaScript.
Общий принцип такой: на веб-страничке (HTML) делаются ссылки на файл с JavaScript-ом и CSS файл со стилями. Так же, в HTML размечаются те места, где будут элементы игры отображаться. А так же вставлена одна функция на JavaScript, которая запускается сразу после загрузки страницы, и задаёт параметры языка для игры, и даёт команду на создание нового игрового поля.
В CSS файле содержатся стили элементов игры, такие, как обводка клеток, и тип курсора мыши, при наведении на клетку.
В JS файле содержится основной код, который и управляет игрой.
Раз мы изучаем на примере, то практически каждая строчка будет содержать комментарий – подробное описание того, чего делает данная команда.
Исходный код CSS файла
CSS (каскадные таблицы стилей) – описывает стиль для отображения игрового поля на HTML страницы. Исходный код CSS приведу без подробных объяснений, только с комментариями, которые начинаются символами /* и заканчиваются символами */.
#gameinfo { font-size: 110%; }
#game table{ border:3px groove blue; background-image:url(“fon.jpg”); }
#game table tr td{
border:1px dotted #eeb; cursor:pointer; }
#game table tr td:hover{ border:1px solid #ffa; }
Код на HTML странице
HTML. Следующий код необходимо вставить на веб-страницу. Каждый фрагмент кода подписан.
внутрь тега HEAD:
Так же, нужно прописать параметр onload=”initGame();” в тег BODY.
внутрь BODY – туда, где будет управление игрой:
Кликните по клетке, чтобы открыть её | Размер поля: |
внутрь BODY – туда, где будет игровое поле:<\p>
Код JavaScript игры
Самый большой код. И самый главный. В нём есть и ИИ соперника, и функции отображения и построения поля, и проверка победы. Но, так как разбор программы сходу, думаю, будет немного сложноват, то сначала я опишу вкратце, какой фрагмент и функция программы за что отвечает.
Первые строки – переменные отвечают за общие переменные:
- gameImgDir – содержит сведения об расположении относительного адреса графических изображений.
- gTexts – содержит текстовые строки, полезны для поддержки многоязычности.
- gField – двумерный массив – содержит информацию о клетках.
Далее идут функции, которые вызываются со страницы. Посредством этих вызовов в игру передаются команды о выборе пользователя, и ответные реакции игры – сообщение о конце игры, расстановка крестиков и ноликов, расчёт хода компьютерного противника.
Вот, что каждая из них делает:
- createField(w,h) – создание нового поля, с размером w на h.
- setCell(x,y,t) – ставит в клетку с координатами [x,y] значение t (крестик, или нолик). А так же заменяет картинки на поле.
- isWin() – проверяет, кто победил.
- CompGame() – вызывает компьютерного игрока, который сначало методом перебора проверяет все варианты, запуская isWin(), а затем делает ход, вызывая setCell(x,y,t).
- onCellClk(x,y) – вызывается по клику, передаются координаты клетки. Вызывает isWin(), setCell(x,y,t) и CompGame(). Сообщает информацию о победе.
После того, как мы изучили основные принципы игры, теперь можно смотреть весь исходный код файла gamescript.js. Все основные моменты кода даны с комментариями (комментарии в JS идут после двойного слеша //).
var gameImgDir=”data/”; var gTexts=[]; gTexts['win1']='Win '; gTexts['win2']='!'; gTexts['playing']='…' gTexts['start']='Begin game!' var gField=[]; function createField(w,h){ gField=new Array(w); for (i=0;i
Источник: http://alexeyk.com/ru/text/js_tictactoegame.html
Крестики нолики на javascript
Приветствую вас дорогие друзья! В этом уроке я покажу, как можно сделать браузерную игру – крестики нолики, на javascript!Все вы знаете, что это за игра и как в нее играть, но еще раз напомню:
P.S. как и во всех подобных уроках по javascript, в конце статьи вы можете скачать исходный файл, а также посмотреть результат работы на demo примере!
Описание создаваемой игры
Давайте рассмотрим особенности игры:
- игра начинается сразу после загрузки страницы;
- право ходить первым выбирается случайным образом (либо вы начинаете ходить, либо компьютер);
- знак, который вы будете ставить, выбирается случайным образом (крестик или нолик);
- если выигрывает игрок, то победные символы (полоса крестиков или ноликов) выделяется зеленым цветом;
- если игрок проигрывает компьютеру, то полоса выделяется красным цветом;
- над полем имеется строка информации, где выводится результат (победа или поражение).
Логика
Я не стал придумывать сложных (универсальных) алгоритмов для игрового поля 3 на 3 клетки, я пошел другим путем – перебором! (об этом чуть позже). Я выделил три основных последовательных этапа, на которых и держится вся логика:
1 этап: проверяем – победил ли игрок?
На этом этапе мы проверяем, имеются ли 3 клетки (на одной линии), заполненные одинаковыми символами играющего (крестиками или нолями). Т.е. не смотря на то, какой этой ход (пусть даже первый) мы всегда сначала проверяем – выиграл ли играющий. Вот так выглядит победа:
2 этап: проверяем – может ли компьютер победить следующим ходом?
На этом этапе мы ищем линию, где были бы 2 клетки заполненные компьютером и одна пустая клетка – то есть пытаемся выиграть за счет невнимательности игрока. Вот так выглядит поражение (т.е. победа компьютера):
3 этап: не даем выиграть!
Здесь мы ищем такую же линию, как и на втором этапе, только 2 клетки должны быть заполнены игровыми знаками игрока, то есть на этом этапе не даем проиграть компьютеру, поставив знак в пустую клетку. Каждый из этапов представляем собой самостоятельную функцию – это вы можете увидеть в js коде ниже.
Реализация
Разметка игрового поля очень простая – в основном блоке размещена строка информации (класс – result) и 9 блоков, представляющих собой клетки (класс – block) HTML разметка клеток:
Ваш ход!
Вспомогательный класс cell[1..9] необходим для того, чтобы точно идентифицировать нужную клетку на игровом поле. CSS стили для игрового поля:
.krestiki_noliki{ width: 306px; margin: 0 auto; } .krestiki_noliki .block{ width: 100px; height: 100px; border: 1px solid #ccc; cursor: pointer; float: left; text-align: center; font-size: 100px; line-height: 94px; }
Теперь давайте рассмотрим весь JS код, после чего я расскажу об основных моментах:
$(document).ready(function(){ var znak_user = 'O'; var znak_comp = 'X'; var rand_num = Math.round((Math.random() * (9 – 1) + 1)); if( rand_num > 3 ){ var znak_comp = 'O'; var znak_user = 'X'; $('.cell'+rand_num).text(znak_comp); } var exit_flag = false; var win_user_array = ['123','456','789','147','258','369','159','357']; //Определяем победу игрока function check_3_user(znak){ for (var i = 0; i < 8; i++) { var first = 'cell' + win_user_array[i].substr(0,1); var second = 'cell' + win_user_array[i].substr(1,1); var third = 'cell' + win_user_array[i].substr(2,1); if( $('.'+first).text() == znak && $('.'+second).text() == znak && $('.'+third).text() == znak ){ $('.'+first+',.'+second+',.'+third).css("background-color", "#83e2c3"); $('.result').text('Вы выиграли!'); $('.krestiki_noliki .block').unbind('click'); exit_flag = true; } } } //Определяем возможность победы компьютера function check_2_comp(znak){ for (var i = 0; i < 8; i++) { var first = 'cell' + win_user_array[i].substr(0,1); var second = 'cell' + win_user_array[i].substr(1,1); var third = 'cell' + win_user_array[i].substr(2,1); if( $('.'+first).text() == znak && $('.'+second).text() == znak && $('.'+third).text() == '' && exit_flag == false ){ $('.'+third).text(znak); $('.'+first+',.'+second+',.'+third).css("background-color", "#EF7C7C"); $('.result').text('Вы проиграли!'); $('.krestiki_noliki .block').unbind('click'); exit_flag = true; } if( $('.'+first).text() == znak && $('.'+second).text() == '' && $('.'+third).text() == znak && exit_flag == false ){ $('.'+second).text(znak); $('.'+first+',.'+second+',.'+third).css("background-color", "#EF7C7C"); $('.result').text('Вы проиграли!'); $('.krestiki_noliki .block').unbind('click'); exit_flag = true; } if( $('.'+first).text() == '' && $('.'+second).text() == znak && $('.'+third).text() == znak && exit_flag == false ){ $('.'+first).text(znak); $('.'+first+',.'+second+',.'+third).css("background-color", "#EF7C7C"); $('.result').text('Вы проиграли!'); $('.krestiki_noliki .block').unbind('click'); exit_flag = true; } } } //Определяем ход компьютера function check_2_user(znak){ for (var i = 0; i < 8; i++) { var first = 'cell' + win_user_array[i].substr(0,1); var second = 'cell' + win_user_array[i].substr(1,1); var third = 'cell' + win_user_array[i].substr(2,1); if( exit_flag == false ){ if( $('.'+first).text() == znak && $('.'+second).text() == znak && $('.'+third).text() == '' ){ $('.'+third).text(znak_comp); exit_flag = true; } } if( exit_flag == false ){ if( $('.'+first).text() == znak && $('.'+second).text() == '' && $('.'+third).text() == znak ){ $('.'+second).text(znak_comp); exit_flag = true; } } if( $('.'+first).text() == '' && $('.'+second).text() == znak && $('.'+third).text() == znak ){ $('.'+first).text(znak_comp); exit_flag = true; } if(exit_flag) break; } } $('.krestiki_noliki .block').click(function(){ //Если клетка пустая if( $(this).text() == '' ){ $(this).text(znak_user); check_3_user(znak_user); check_2_comp(znak_comp); check_2_user(znak_user); if( exit_flag == false ){ for (var i = 1; i < 10; i++) { if( $('.cell'+i).text() == '' ){ $('.cell'+i).text(znak_comp); break; } } }else exit_flag = false; } }); });
Сначала мы объявляем переменные: znak_user – в этой переменной мы храним знак, которым будет играть пользователь (по умолчанию там хранится нолик – это английская будка “О”). znak_comp – в этой переменной мы храним знак, которым будет играть компьютер (по умолчанию там хранится крестик – это английская будка “икс”).
Далее мы генерируем случайное число (rand_num) от 1 до 9, оно необходимо нам для определения того, кто будет ходить первым, и какие будут использоваться знаки (крестик или нолик) у компьютера и игрока.
Логика такая: если случайное число больше 3, то компьютер играет ноликами, и он же (компьютер) делает первый ход.
Вы можете изменить эту логику так, как удобно вам, например, можно создать несколько случайных цифр, дабы сделать больше вариантов того, кто будет хоть первым и какими знаками.
exit_flag – этот флаг (переменная) отвечает за выход из функции, то есть, например, когда компьютер уже сделал свой ход, и нужно выйти из функции и передать ход игроку.
win_user_array – в этом массиве хранятся все победные варианты заполнения клеток. Чтобы стало понятно, давайте взглянем вот на эту картинку:
Каждый элемент массива – это строка из трех цифр, которая является выигрышной комбинацией, то есть, например, если заполнить клетки под цифрами 1, 2 и 3, то наступит победа (или поражение). Как видите, всего существует 8 победных вариантов, наша задача – перебрать все эти варианты. Далее идут 3 функции:
- check_3_user();
- check_2_comp();
- check_2_user();
Назначение этих функций описано (в виде трех этапов) в разделе Логика (выше). Эти функции вызываются по клику на любую из клеток поля. В каждую из функций передается параметр (znak) – это знак игрока или компьютера (крестик или нолик), например, в функцию, определяющую победу игрока (check_3_user), мы передаем знак игрока, для того, чтобы найти 3 одинаковых знака на одной линии.
После трех функций (если компьютер еще не сделал ход), компьютер заполняем одну из свободных клеток. Здесь, вы можете усложнить игровой процесс, например, сделав так, что если свободна центральная клетка (клетка под номером 5), то сначала ставить в нее, если она занята, то ставить в один из свободных углов (это клетки № 1, 3, 7 и 9) и так далее – в общем, здесь уже на ваше усмотрение.
Вот, в принципе и все, что требуется для создания подобной игры.
Теперь можете посмотреть игру на демо примере и скачать исходный файл (всего 1 файл).
08.06.2018 – спасибо за внимательность автору письма: Patvakan Baghdasaryan, исправлена ошибка, когда у компьютера было несколько возможных вариантов победы и закрашивались Все его победные ходы (от 4 до 6 клеток, вместо 3).
Demo пример
Скачать исходник
На этом у меня все, надеюсь, данный урок был полезен для вас, желаю вам удачи, пока!
Дата последнего изменения: 2019-01-17 09:16:46
Источник: https://sergey-oganesyan.ru/javascript-s-primerami/krestiki-noliki-na-javascript.html
Интеллектуальные системы. Алгоритм поиска оптимального хода в игре Крестики-нолики
Уверен, многие успели насладиться великолепным квестом Machinarium. Вы наверняка помните того робота, которого надо было обыграть в баре, чтобы получить необходимые болтики. Так вот, игра, в которую его надо было обыграть, очень похожа на крестики-нолики, но имеет поле 10 на 10 и продолжается, пока один из игроков не выстроит ряд из пяти фигурок.Продолжая тему интеллектуальных систем, предлагаю написать алгоритм для игры в крестики-нолики по правилам, предложенным в Machinarium.Сыграть! |
Прежде всего, следует отметить, что крестики-нолики – это игра с нулевой суммой. То есть игра, в которой выигрыш одного игрока означает проигрыш другого.
К играм с нулевой суммой также можно отнести шашки, шахматы, го и многие другие игры, список которых вы легко продолжите сами. Ко многим таким играм применимы рассуждения изложенные в первой части статьи.
Начнем с малого, с классических крестиков-ноликов на поле 3 на 3. Процесс игры можно представить в виде графа, узлы которого представляют собой состояния игрового поля после хода одного из игроков:
Пример дерева игры в крестики-нолики |
В простейшем случае, можно строить полный граф игры и выбирать ход, ведущий к победе (или к ничьей). Но такое решение крайне ресурсоемко. В дереве игры, в классические крестики-нолики, придется раскрыть 255168 возможных ходов! Думаю, излишне говорить, что дерево игры по обозначенным в начале статьи правилам, будет гораздо больше.
Чтобы сократить дерево игры, можно поступать по аналогии с человеческим решением: человек тоже не способен продумать все варианты развития игры в соответствии со своим ходом, но он может продумать возможные варианты на несколько ходов вперед и выбрать наилучший из них.
Количество шагов, на которое способен продумать игру человек, ограничено его интеллектуальными способностями. В случае с компьютерным решением, таким ограничением может стать память или время работы алгоритма. В простейшем случаем можно ограничиться глубиной раскрытия дерева.
В таком случае, мы, конечно, рискуем выбрать ход, который будет привлекательным при выбранной глубине раскрытия дерева решений, но окажется проигрышным в более долгосрочной перспективе… Но не будем пессимистами!
Самый интересный аспект такого подхода, это способ оценки выгоды от того или иного хода. В самом простейшем случае, можно выделить некоторые состояния игрового поля (ничья, победа и поражение) и присвоить каждому из них некоторый числовой коэффициент(оценку).
Пример оценок игрового поля(none = 0; draw = 2; win = 5; lose = -5) |
Такая оценка строится не для каждого узла дерева игры, а только для его листьев.
Оценка же каждого узла получается из следующих соображений: на каждом этапе раскрытия дерева решений, начиная с последнего (на котором получаются листы с оценками), выбирается ход, наиболее предпочтительный для игрока, чей ход приводит к порождению узлов. При этом, для игрока, за которого ведется игра, оценка должна стремиться к максимуму, а для противника к минимуму.
Так, играя (к примеру) за нолики, каждый нечетный ход представляет их интересы и выбирается ход с наибольшей оценкой (игрок MAX), а каждый четный – интересы крестиков, выбирается ход с наименьшей оценкой (игрок MIN). Такой подход называется минимаксной процедурой.
Пример дерева игры, раскрытого на глубину в три хода |
Для лучшего понимания алгоритма, привожу пример его реализации на вымышленном языке программирования:
// Оценка максимального выигрыша игрока.
// field состояние поля в результате хода игрока MIN. // player идентификатор игрока MAX.
function MAX(field, player, deph) : int if isTerminal(field) then return heuristic(field, player) end score = -INFINITY for child from children(field, player) do s = MIN(child, -player, deph+1) if s > score then score = s end return score
end
// Оценка минимальных потерь противника.
// field состояние поля в результате хода игрока MAX. // player идентификатор игрока MIN.
function MIN(field, player, deph) : int if isTerminal(field) then return -heuristic(field, player) end score = INFINITY for child from children(field, player) do s = MAX(child, -player, deph+1) if s < score then score = s end return score end Здесь следует пояснить ряд используемых функций.
- Функция isTerminal проверяет, не достигнуто ли терминальное состояние на игровом поле. Терминальное состояние, это состояние при котором наступил выигрыш одного из игроков, или исчерпаны все возможные ходы, или выполнено условие прекращения раскрытия дерева игры (достигнута максимальная глубина / закончилась память).
- Функция heuristic дает эвристическую оценку состояния игрового поля.
- Функция children, при каждом обращении к ней, возвращает следующего потомка текущей оцениваемой позиции в дереве игры. Т.е. раскрывает дерево игры на следующую глубину.
Представленное решение достаточно грубое и содержит много практически идентичного кода.
Если внимательно посмотреть на код, можно заметить, что вызов функции MAX эквивалентно вызову -MIN (справедливо и обратное утверждение). Исходя из этого наблюдения, можно сократить код до одной функции:// Оценивает состояние игрового поля после хода MIN.
// field состояние поля в результате хода игрока MIN. // player идентификатор игрока MAX.
function MAXIMIN(field, player, deph) : int if isTerminal(field) then return heuristic(field, player) end score = -INFINITY for child from children(field, player) do s = MAXIMIN(child, -player, deph+1) if s > score then score = s end return score
endИли:// Оценивает состояние игрового поля после хода MAX.
// field состояние поля в результате хода игрока MAX.
// player идентификатор игрока MIN.
function MINIMAX(field, player, deph) : int if isTerminal(field) then return -heuristic(field, player) end score = INFINITY for child from children(field, player) do s = MINIMAX(child, -player, deph+1) if s < score then score = s end return score end При выборе одного из вариантов(MAXIMIN или MINIMAX) будьте внимательны, очень важно не напутать с аргументами при первом вызове этих функций. Если вершина раскрыта за игрока MAX (последний ход был игрока MAX), то следует вызывать функцию MINIMAX или -MAXIMIN. Что же, с помощью минимаксной процедуры количество раскрываемых вершин существенно сократилось, но для реализации алгоритма, способного потягаться с сильным игроком, может понадобиться раскрывать дерево игры на достаточно большую глубину, а вместе с тем производить оценку все большего количества вершин. Это может приводить к существенным издержкам как по времени, так и по памяти. Поэтому, не смотря на то, что минимаксная процедура существенно сокращает количество раскрываемых вершин, она все еще не дает приемлемых результатов.
Если вернуться к приводимому ранее примеру дерева игры, то можно заметить следущее, после того, как был найден ход, приводящий крестиков как минимум к ничьей, этот ход становится меньшим из того, на что они согласны. Когда начнется рассмотрение второго хода(на глубине 1), и будет найдена проигрышная вершина, раскрывать следующую (на глубине 2) уже не будет смысла, так как для кружков худшее на что они согласны, это оценка -5, которая в свою очередь меньше чем самые худшие ожидания для крестиков. Стало быть, второй ход(вторая вершина на глубине 1) выбран не будет.
Осознать это наблюдение с первого раза довольно трудно, но тем не менее оно может помочь сократить количество раскрываемых вершин в дереве игры.
На подобных рассуждениях строится следующий алгоритм, называемый альфа-бета отсечения.
Давайте отвлечемся от крестиков-ноликов и рассмотрим дерево гипотетической игры Кружки-ромбики (граф раскрывается для кружков, те кружки выбирают ход с максимальной оценкой, а ромбики с минимальной):
Дерево гипотетической игры кружки-ромбики |
Предположим, что мы раскрыли крайнюю левую вершину за кружки на глубине 3, получили оценку 7 и перешли к раскрытию следующей вершины на этой глубине. Обратите внимание, что следующая вершина последняя в этой ветке и после ее раскрытия мы перейдем на глубину выше, где выбор будут делать ромбики. Ромбики же, получив оценку первой вершины 7, очевидно, не станут делать выбор вершины с большей оценкой и вторая, раскрытая нами вершина с оценкой 9, выбрана не будет. И стоило ее раскрывать? Чтобы сократить количество раскрываемых вершин, можно воспользоваться оценкой, полученной для предыдущей вершины на этой же глубине. Для MAX, эта оценка будет предельной максимальной оценкой, а для MIN предельной минимальной. Такие оценки принято называть альфа и бета границами. Вот как это работает: раскрытие корневой вершины начинается с эмпирически подобранными параметрами альфа и бета. В простейшем случае им присваиваются максимально и минимально допустимые оценки. Далее, дерево раскрывается на максимальную глубину, которая может определяться фиксированным значением глубины, временем или памятью. Производится эвристическая оценка полученных листов. Оценки сравниваются на предмет попадания в диапазон альфа-бета. Если раскрытие происходит за игрока MAX и оценка оказывается больше беты, или раскрытие происходит за игрока MIN и оценка оказывается меньше альфа, то дальнейшее рассмотрение вершин становится не целесообразным.
Пример бета отсечения |
На приведенном выше рисунке представлен случай с бета отсечением. После раскрытия всех листьев в левой ветке и получив оценку 7 для первой вершины на глубине два, мы переходим к раскрытию второй вершины. Получая оценку для каждой дочерней ее вершины, сравниваем ее с полученной ранее оценкой 7. Если оценка вновь раскрытой вершины окажется больше, то нет смысла раскрывать остальные вершины, тк очевидно что выбор будет сделан в ползу вершины с оценкой 7. Аналогичные рассуждения применимы для параметра альфа.// Оценка максимального выигрыша игрока player в позиции position.
function MAX(position, alpha, beta, deph, player) : int if isTerminal(position) then return heuristic(position, player) end score = alpha for child from children(position, player) do s = MIN(child, score, beta, deph+1, -player) if s > score then score = s if score >= beta then return score end return score
end
// Оценка минимальных потерь игрока player в позиции position.
function MIN(position, alpha, beta, deph, player) : int if isTerminal(position) then return -heuristic(position, player) end score = beta for child from children(position, player) do s = MAX(child, alpha, score, deph+1, -player) if s < score then score = s if score
Источник: https://www.dokwork.ru/2012/11/tictactoe.html
Как написать бота, которого будет нельзя обыграть в «крестики-нолики», или Знакомство с правилом «минимакс»
Вполне возможно, что после сотен партий в «крестики-нолики» вы задумывались: каков же оптимальный алгоритм? Но если вы здесь, то вы наверняка ещё и пробовали написать реализацию этой игры. Мы пойдём дальше и напишем бота, который будет невозможно обыграть в «крестики-нолики». Предугадав ваш вопрос «почему?», ответим: благодаря алгоритму «минимакс».
<\p>
Как и профессиональный шахматист, этот алгоритм просчитывает действия соперника на несколько ходов вперёд — до тех пор, пока не достигнет конца партии, будь то победа, поражение или ничья.
Попав в это конечное состояние, ИИ начислит себе положительное количество очков (в нашем случае +10) за победу, отрицательное (-10) — за поражение, и нейтральное (0) — за ничью.
В то же время алгоритм проводит аналогичные расчёты для ходов игрока. Он будет выбирать ход с наиболее высоким баллом, если ходит ИИ, и ход с наименьшим, если ходит игрок. Используя такую стратегию, минимакс избегает поражения.
Попробуйте сыграть вот в такую игру.
See the Pen Минимакс by Ahmad Abdolsaheb (@abdolsa) on CodePen.
Алгоритм «минимакс» проще всего описать в виде рекурсивной функции, которая:
- возвращает значение, если найдено конечное состояние (+10, 0, -10),
- проходит по всем пустым клеткам на поле,
- вызывает минимакс-функцию для каждой из них (рекурсия),
- оценивает полученные значения
- и возвращает наилучшее из них.
Если вы не знакомы с рекурсией, то вам стоит посмотреть эту лекцию из гарвардского курса CS50:
Чтобы разобраться в том, как устроен минимакс, давайте напишем его реализацию и смоделируем его поведение. Займёмся этим в двух следующих разделах.
Реализация минимакса
Мы рассмотрим ситуацию, когда игра подходит к концу (смотрите картинку ниже). Поскольку минимакс проходит по всем возможным состояниям игры (а их сотни тысяч), имеет смысл рассматривать эндшпиль — так нам придётся отслеживать меньшее количество рекурсивных вызовов функции (всего 9).
Пусть ИИ играет крестиками, человек — ноликами.
Чтобы упростить работу с полем, объявим его как массив из 9 элементов со значениями, равными содержимому клеток. Заполним его крестиками и ноликами, как на картинке выше, и назовём origBoard.
var origBoard = [“O”,1,”X”,”X”,4,”X”,6,”O”,”O”];
Затем объявим переменные aiPlayer и huPlayer и присвоим им значения “X” и “O” соответственно.
Кроме того, нам потребуется функция, которая ищет победные комбинации и возвращает истинное значение в случае успешного поиска, и функция, которая хранит индексы доступных клеток.
/* начальное состояние доски O | | X ——— X | | X ——— | O | O */
var origBoard = [“O”,1 ,”X”,”X”,4 ,”X”, 6 ,”O”,”O”]; // человек
var huPlayer = “O”; // ИИ
var aiPlayer = “X”; // возвращает список индексов пустых клеток доски
function emptyIndices(board){ return board.filter(s => s != “O” && s != “X”);
} // победные комбинации с учётом индексов
function winning(board, player){ if( (board[0] == player && board[1] == player && board[2] == player) || (board[3] == player && board[4] == player && board[5] == player) || (board[6] == player && board[7] == player && board[8] == player) || (board[0] == player && board[3] == player && board[6] == player) || (board[1] == player && board[4] == player && board[7] == player) || (board[2] == player && board[5] == player && board[8] == player) || (board[0] == player && board[4] == player && board[8] == player) || (board[2] == player && board[4] == player && board[6] == player) ) { return true; } else { return false; }
}
Итак, давайте определим минимакс-функцию с двумя аргументами: newBoard (новое поле) и player (игрок). Затем найдём индексы свободных клеток на поле и передадим их в переменную availSpots.
// основная минимакс-функция
function minimax(newBoard, player){ //доступные клетки var availSpots = emptyIndices(newBoard);
Кроме того, нам нужно отслеживать конечные состояния и возвращать соответствующие значения. Если побеждает «нолик», нужно вернуть -10, если «крестик» — +10. Если размер массива availSpots равен нулю, значит, свободных клеток нет, игра закончится ничьёй, и нужно вернуть ноль.
// проверка на терминальное состояние (победа / поражение / ничья) //and returning a value accordingly if (winning(newBoard, huPlayer)){ return {score:-10}; } else if (winning(newBoard, aiPlayer)){ return {score:10}; } else if (availSpots.length === 0){ return {score:0}; }
После этого нужно собрать очки с каждой из пустых клеток. Для этого создадим массив ходов moves и пройдём в цикле по всем пустым клеткам, помещая индексы и очки каждого хода в объект move.
Затем зададим индекс пустой клетки, который хранился в виде числа в origBoard, равным свойству-индексу объекта move. Потом сходим за текущего игрока на пустую клетку нового поля newBoard и вызовем функцию minimax от другого игрока и получившегося поля newBoard. После этого нужно поместить свойство score объекта, возвращённого функцией minimax, в свойство score объекта move.
И наконец, функция сбрасывает изменения newBoard и помещает объект move в массив moves.
// массив для хранения всех объектов var moves = []; // цикл по доступным клеткам for (var i = 0; i < availSpots.length; i++){ //create an object for each and store the index of that spot var move = {}; move.index = newBoard[availSpots[i]]; // совершить ход за текущего игрока newBoard[availSpots[i]] = player; //получить очки, заработанные после вызова минимакса от противника текущего игрока if (player == aiPlayer){ var result = minimax(newBoard, huPlayer); move.score = result.score; } else{ var result = minimax(newBoard, aiPlayer); move.score = result.score; } // очистить клетку newBoard[availSpots[i]] = move.index; // положить объект в массив moves.push(move); }
Затем минимаксу нужно выбрать наилучший ход move из массива moves. Ему нужен move с наибольшим счётом, если ходит ИИ, и с наименьшим, если это ход человека.
Таким образом, если значение player равно aiPlayer, алгоритм инициализирует переменную bestScore очень маленьким числом и идёт циклом по массиву moves: если ход move приносит больше очков score, чем bestScore, алгоритм запоминает этот move. В случае ходов с одинаковыми очками алгоритм запоминает первый из них.
В случае, когда player равен huPlayer, всё аналогично — только теперь bestScore инициализируется большим числом, а минимакс ищет ход move с наименьшим количеством очков.
В итоге минимакс возвращает объект, хранящийся в bestMove.
// если это ход ИИ, пройти циклом по ходам и выбрать ход с наибольшим количеством очков var bestMove; if(player === aiPlayer){ var bestScore = -10000; for(var i = 0; i < moves.length; i++){ if(moves[i].score > bestScore){ bestScore = moves[i].score; bestMove = i; } } }else{ // иначе пройти циклом по ходам и выбрать ход с наименьшим количеством очков var bestScore = 10000; for(var i = 0; i < moves.length; i++){ if(moves[i].score < bestScore){ bestScore = moves[i].score; bestMove = i; } } } // вернуть выбранный ход (объект) из массива ходов return moves[bestMove]; }
В следующем разделе мы смоделируем работу нашей программы, чтобы понять, как она работает.
Минимакс в действии
Пользуясь схемой ниже, разберем пошаговую модель алгоритма.
- Алгоритму подаются origBoard и aiPlayer. Он составляет список из трёх найденных пустых клеток, проверяет конечность состояния, и проходит циклом по всем пустым клеткам. Затем алгоритм меняет newBoard, помещая aiPlayer в первую пустую клетку. После этого он вызывает сам себя от newBoard и huPlayer и ждёт, пока второй вызов вернёт значение.
- Пока первый вызов функции всё ещё работает, запускается второй, создавая список из двух пустых клеток, проверяя конечность состояния и проходя циклом по всем пустым клеткам. Затем второй вызов изменяет newBoard, помещая huPlayer в первую пустую клетку. После этого он вызывает сам себя от newBoard и aiPlayer и ждёт, пока третий вызов вернёт значение.
- Алгоритм составляет список пустых клеток и фиксирует победу игрока после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным (-10).
- Алгоритм составляет список пустых клеток и фиксирует победу игрока после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным (-10).
- В пятом вызове функции алгоритм составляет список пустых клеток и фиксирует победу ИИ после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным +10.
- Шестой вызов составляет список из двух пустых клеток, проверяет конечность состояния и идёт циклом по всем пустым клеткам. Затем он изменяет newBoard, помещая huPlayer в первую пустую клетку. Потом он вызывает сам себя от newBoard и aiPlayer и ждёт, пока седьмой вызов вернёт значение.
- Новый вызов составляет список из одной пустой клетки, проверяет конечность состояния и изменяет newBoard, помещая aiPlayer в пустую клетку. После этого он вызывает сам себя от newBoard и huPlayer и ждёт, пока этот вызов вернёт значение.
- Восьмой вызов составляет пустой список пустых клеток и фиксирует победу aiPlayer после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным (+10), на уровень выше, седьмому вызову.
- После этого алгоритм составляет список пустых клеток и фиксирует победу aiPlayer после проверки конечности состояния. Поэтому он возвращает объект с полем счёта, равным (+10), на уровень выше.
В рассмотренном выше сценарии минимакс решает, что оптимальным выбором будет ход в центральную клетку поля.
Конец!
К этому моменту вы должны были понять, как устроен алгоритм минимакс. Попробуйте написать его реализацию самостоятельно или посмотрите пример на GitHub или CodePen и оптимизируйте его.
Если вас заинтересовала тема ИИ в играх, советуем почитать наши материалы по этой теме:
Перевод статьи «How to make your Tic Tac Toe game unbeatable by using the minimax algorithm»
Источник: https://tproger.ru/translations/tic-tac-toe-minimax/
Создаём игру крестики-нолики на Kivy
Kivy – кросcплатформенный графический фреймворк на Python, направленный на создание новейших пользовательских интерфейсов даже для приложений, работающих с сенсорными экранами. Приложения, написанные на Kivy, могут работать не только на таких традиционных платформах как Linux, OS X и Windows, но также на Android, iOS и Rapberry Pi.
Это означает, что в разработке можно использовать различные библиотеки, как Requests, SQLAlchemy или даже NumPy. Допускается даже доступ к нативным мобильным API посредством дочерних проектов Kivy. Еще одна отличительная черта Cython – оптимизированный конвейерный обработчик OpenGL. При его помощи можно легко добиться сложных GPU эффектов, не прибегая к сложным конструкциям в коде.
По сути Kivy это набор модулей Python/Cython, которые можно легко установить при помощи pip. Но существует ряд зависимостей. Вам понадобится Pygame, он используется в качестве рендера, Cython для компиляции скоростных составляющих графических элементов и GStreamer для работы с мультимедиа. Как правило, все это легко установить при помощи стандартного менеджера пакетов или pip.
После установки всех зависимостей, можно установить и сам Kivy. Текущая версия – 1.8.0, которая работает как с Python2, так и с Python3. Код, использованный в этой статье должен успешно работать под версиями Python 2.7 и 3.3.
pip install kivy
Если у вас возникли проблемы с установкой через pip, то всегда можно воспользоваться easy_install. Существует несколько специальных репозиториев и готовых сборок для популярных дистрибутивов OS. На официальном сайте Kivy вы найдете более подробную информацию по теме.
Приложение Kivy всегда начинается с инициализации и запуска класса App. Именно он отвечает за создание окон, интрефейса с OS, а также предоставляет входную точку при создании элементов интерфейса пользователя. Начнем с простейшего приложения Kivy:
from kivy.app import App class TicTacToeApp(App): pass if __name__ == “__main__”: TicTacToeApp().run()
Уже это приложение можно запустить, вы увидите пустое черное окно. Впечатляет, не так ли? Мы можем строить GUI при помощи виджетов Kivy.
Каждый из них представляет собой простой графический элемент со своим функционалом от стандартного – кнопки, лейблы, текстовые поля ввода, или вспомогательные элементы для позиционирования дочерних, до более абстрактных – диалоги выбора файлов, камера или видео проигрыватель.
Большинство основных виджетов Kivy легко сочетаются друг с другом, тем самым образуя новые варианты GUI, то есть, нет необходимости изобретать велосипед. В этой статье я приведу несколько примеров подобного использования виджетов.
Так как “Hello, world!” уже стало практически неизбежным примером, давайте поскорее его рассмотрим и перейдем к более интересным примерам. Воспользуемся обычным лейблом для отображения текста:
from kivy.uix.label import Label
Используем лейбл в качестве корневого виджета. У каждого приложения должен быть корневой виджет, он играет роль верхушки дерева элементов и автоматически принимает размер окна. Позднее мы рассмотрим как строить полноценные GUI при помощи большего количества виджетов, но на данном этапе нам достаточно, при помощи нового метода, вернуть корневой виджет:
class TicTacToeApp(App): def build(self): return Label(text=”Hello World!”, font_size=100, color=(0, 1, 0, 1))
Метод build исполняется при запуске приложения, и виджет, который он возвращает, автоматически занимает корневое положение. В нашем случае это лейбл, которому мы задали несколько параметров – text, font_size и color. Для каждого виджета существует свой набор параметров, контролирующих его поведение. Все их можно изменять динамически, но мы установим их лишь один раз при инициализации.
Обратите внимание, что эти свойства являются частью Kivy, а не Python. Они доступны как стандартные атрибуты класса, при этом они предоставляют дополнительный функционал через систему событий Kivy. Позднее мы рассмотрим примеры создания свойств работающих совместно с событийной системой Kivy.
Так вот, предыдущего примера вполне достаточно для отображения текста. В чем вы можете убедиться, запустив наше приложение. Если у вас возникли трудности с параметрами, которые мы задавали, поэкспериментируйте с ними.
Собственный виджет: крестики-нолики
Конечно, Kivy не имеет в своем составе виджета для игры в крестики-нолики, поэтому нам придется создать свой. Мы создадим новый класс виджета и опишем в нем необходимое нам поведение:
from kivy.uix.gridlayout import GridLayout class TicTacToeGrid(GridLayout): pass
Очевидно, что пока этот класс не содержит в себе никакого функционала, за исключением того, что он унаследовал от GridLayout, которому мы укажем требуемое количество столбцов, а он уже, в свою очередь, разместит все дочерние элементы в пределах созданной сетки. Нам понадобится три столбца и девять дочерних элементов.
Настало время рассмотреть язык Kivy (kv) – специальный язык для описания дерева виджетов Kivy. Он одновременно очень прост и позволяет избежать большого объема монотонного кода Python.
Python предоставляет нам возможность динамически манипулировать GUI элементами, но иногда нам надо просто описать базовую структуру GUI.
В принципе, kv можно легко заменить python кодом, но в дальнейших примерах мы используем kv, как и большинство программистов.
Kivy изначально содержит в себе все необходимое для работы с kv. Проще всего создать новый файл с таким же именем как и App класс и описывать весь GUI в нем. То есть создадим файл tictactoe.kv и напишем в нем следующее:
: cols: 3 # Number of columns
В этом весь синтаксис kv, для каждого виджета мы создаем свое правило, описывающее его поведение, настройки и дочерние виджеты. В примере демонстрируется правило для виджета tictactoe, которое указывает, что каждый объект класса TicTacToeGrid уже при создании получает значение 3 для свойства cols.
Мы еще вернемся к kv, но сейчас давайте создадим несколько кнопок для начала игры.
from kivy.uix.button import Button
from kivy.properties import ListProperty class GridEntry(Button): coords = ListProperty([0, 0])
Мы наследуем свойства и методы виджета Button для взаимодействия с мышью или касаниями тач скрина и запуска соответствующих событий.
То есть мы можем выполнять собственные функции, когда пользователь нажал на кнопку, и можем устанавливать свойство text кнопки в X или О. Мы также создали новое свойство для нашего виджета – coords – координаты, они очень нам помогут в дальнейшем.
Создание новых свойств практически идентично созданию нового атрибута класса в Python – достаточно добавить self.coords = [0, 0] в GridEntry.__init__.
Также как и для TicTacToeGrid создадим ряд правил для нового виджета:
: font_size: self.height
Как и раньше, первое правило описывает начальные параметры при инициализации виджета, на этот раз мы указываем размер шрифта для лейбла кнопки. Основное удобство использования языка kv заключается в том, что он автоматически определяет, что мы ссылаемся на собственный размер кнопки, и теперь если мы изменим размер кнопки, то и размер шрифта изменится соответственно.
Теперь заполним TicTacToeGrid виджетами GridEntry. Мы столкнемся с новыми элементами Kivy: при создании виджетов GridEntry мы сразу задаем им координаты, передав соответствующие параметры. Этот функционал полностью поддерживается Kivy.
class TicTacToeGrid(GridLayout): def __init__(self, *args, **kwargs): super(TicTacToeGrid, self).__init__(*args, **kwargs) for row in range(3): for column in range(3): grid_entry = GridEntry( coords=(row, column)) grid_entry.bind(on_release=self.button_pressed) self.add_widget(grid_entry) def button_pressed(self, button): print ('{} button clicked!'.format(button.coords))
Мы использовали метод bind для привязки обработчика button_pressed к событию on_release виджета GridEntry, так метод button_pressed будет выполнен каждый раз после того, как пользователь отпустил кнопку. Мы также могли использовать событие on_press, то есть событие при первом нажатии на кнопку. Таким образом, можно привязать свой обработчик к любому событию Kivy.
Мы добавили каждый виджет GridEntry при помощи метода add_widget. То есть каждый является дочерним элементом TicTacToeGrid и будет расположен в сетке в соответствии с заданными ранее нами параметрами.
Теперь нам осталось только вернуть этот виджет в качестве корневого и мы сможем посмотреть на наше приложение.
def build(self): return TicTacToeGrid()
Запустив главное Python приложение вы увидите только что созданный интерфейс игры. Все отлично, надпись текста была заменена на сетку из девяти кнопок, на каждую из которых можно нажать (она автоматически изменит цвет) и отпустить (в консоли вы увидите текст из обработчика). Конечно, можно и дальше изменять внешний вид кнопок, но пока мы оставим их как есть.
Так, а кто победил?
Нам понадобится следить за состоянием поля, чтобы определить победителя. Для этого изменим несколько свойств Kivy:
from kivy.properties import (ListProperty, NumericProperty) class TicTacToeGrid(GridLayout): status = ListProperty([0, 0, 0, 0, 0, 0, 0, 0, 0]) current_player = NumericProperty(1)
Таким образом, мы добавили способ отслеживания состояния поля и очередность хода (1 – О, -1 – Х). Разместив эти числа в списке мы сможем определить победителя, сумма столбца, строки или диагонали должна быть равна +-3. Теперь мы сможем обновить игровое поле после хода.
def button_pressed(self, button): # Create player symbol and colour lookups player = {1: 'O', -1: 'X'} colours = {1: (1, 0, 0, 1), -1: (0, 1, 0, 1)} # (r, g, b, a) row, column = button.coords # The pressed button is automatically # passed as an argument # Convert 2D grid coordinates to 1D status index status_index = 3*row + column already_played = self.status[status_index] # If nobody has played here yet, make a new move if not already_played: self.status[status_index] = self.current_player button.text = {1: 'O', -1: 'X'}[self.current_player] button.background_color = colours[self.current_player] self.current_player *= -1 # Switch current player
Теперь после запуска приложения вы наглядно увидите, что именно мы сделали. После каждого нажатия на кнопку на ней появляется О или Х, а цвет фона будет изменятся, показывая чей ход следующий. Так же нажать на кнопку можно только один раз, так как при помощи массива мы отслеживаем состояние поля.
Теперь уже можно играть в крестики-нолики, но один важный элемент все же пока отсутствует – громадное окно во весь экран, оповещающее что мы выиграли! Но сначала необходимо добавить немного кода для определения окончания игры.
Здесь мы воспользуемся еще одним удобным элементов Kivy. Каждый раз при изменении свойства Kivy запускается метод on_propertyname и срабатывает соответствующее событие.
Таким образом, мы запросто можем создать метод, который будет отрабатывать при каждом изменении свойства кнопки, как на Python, так и на kv.
В нашем случае мы будем проверять список состояний каждый раз при его обновлении и выполнять определенный метод, если игра закончилась.
# … def on_status(self, instance, new_value): status = new_value # Sum each row, column and diagonal. # Could be shorter, but let's be extra # clear what’s going on sums = [sum(status[0:3]), # rows sum(status[3:6]), sum(status[6:9]), sum(status[0::3]), # columns sum(status[1::3]), sum(status[2::3]), sum(status[::4]), # diagonals sum(status[2:-2:2])] # Sums can only be +-3 if one player # filled the whole line if 3 in sums: print(“Os win!”)
elif -3 in sums: print(“Xs win!”)
elif 0 not in self.status: # Grid full print(“Draw!”)
Таким образом, мы определяем победу или ничью, но результат мы сообщаем только в консоль. Будет логично каждый раз сбрасывать состояние игрового поля для начала новой игры и показывать счет.
# Note the *args parameter! It's important later when we make a binding
# to reset, which automatically passes an argument that we don't care about
def reset(self, *args): self.status = [0 for _ in range(9)] # self.children is a list containing all child widgets for child in self.children: child.text = '' child.background_color = (1, 1, 1, 1) self.current_player = 1
Теперь мы можем изменить метод on_status, чтобы сбросить состоянии игры в начальное и показать победителя при помощи виджета ModalView.
from kivy.uix.modalview import ModalView
Это виджет всплывающего окна, который отображается поверх всех остальных виджетов. Он автоматически закрывается при нажатии на него.
# …
winner = None if -3 in sums: winner = 'Xs win!'
elif 3 in sums: winner = 'Os win!'
elif 0 not in self.status: winner = 'Draw…nobody wins!' if winner: popup = ModalView(size_hint=(0.75, 0.5)) victory_label = Label(text=winner, font_size=50) popup.add_widget(victory_label) popup.bind(on_dismiss=self.reset) popup.open()
По сути ничего нового здесь нет, мы добавили текстовый лейбл к ModalView и при помощи методов виджета ModalView показали само окно. Также мы воспользовались событием on_dismiss, которое срабатывает при закрытии окна.
Наконец, мы установили общее для всех виджетов свойство size_hint, которое в нашем случае задает размер модального окна относительно основного окна. Пока открыто модальное окно, вы можете изменять размер главного окна и вы увидите, что модальное окно будет также изменяться в размерах.
В данном случае мы воспользовались привязкой свойства hint_size, а обработку полностью взял на себя Kivy.
Вот и все, игра готова. Теперь мы можем не только играть в крестики-нолики, но наша программа также выдает сообщение при окончании игры и сбрасывает состояние игры.
Пора поэкспериментировать
Мы рассмотрели основы работы с Kivy, но, я надеюсь, что вы увидели принцип построения приложений на Kivy. Мы строим приложения из отдельных виджетов, которые взаимодействуют друг с другом через изменение параметров или срабатывании событий. Мы также коротко коснулись языка kv и привели пример использования автоматического связывания свойств с событиями.
В архиве{:target=”blank”} вы найдете полную копию программы для самопроверки.
Мы также добавили дополнительный виджет, Interface, чтобы продемонстрировать как полностью построить структуру игры на языке kv, то есть мы показали еще один способ добавлять дочерние элементы.
Проверить его вы сможете убрав комментарии со строки return Interface() в TicTacToeGrid.build.
Мы не изменили ничего глобального, а всего лишь показали более углубленный пример использования языка kv, где мы использовали привязку свойств, чтобы автоматически обновлять текст на кнопках и изменять размеры поля, таким образом, чтобы оно всегда оставалось пропорциональным. Вы можете поэкспериментировать со свойствами, чтобы окончательно разобраться: за что отвечает каждое из них или изменить типы виджетов и посмотреть как будут реагировать на них другие виджеты.
Источник: http://devacademy.ru/posts/sozdayom-igru-krestiki-noliki-na-kivy/
Adblockdetector