Первый шаг на пути к Fullstack Developer или пишем крестики нолики, многопользовательский режим по сети

Крестики нолики на андроид, многопользовательский режим

Предыстория

С начала 2016 я плотно подсел на кодинг (начал я изучать Java в 2013, но потом забросил на 2 года) и первое что мне пришло в голову — написать крестики нолики. Тогда у меня не было много знаний и я сделал это без UI. Да, просто в консоли. Исходники остались, можете посмотреть здесь. Одновременно с этим я изучал JavaScript, HTML и CSS и конечно же попробовал написать уже с GUI, но все равно игра была как бы для одного юзера. Точнее на двоих, но с одного и того же устройства. После чего я написал уже крестики нолики на андроид, но опять же — игра на двоих, которая подразумевала офлайн с одного и того уже устройства. Исходники опять же остались на гитхабе и доступны по этой ссылке.

Да, это было конечно же прикольно и я подумывал написать так называемый ИИ чтобы можно было играть одному, но это не было таким интересным занятием. Моя мечта была написать еще и сервер чтобы можно было играть по истине вдвоем, с 2 разных девайсов. Но тогда, в 2016 году я готовился к поискам работы и нужно было писать очень много другого кода для андроид.

И вот, спустя 3 года (4?) я вернулся к этой затее. Я просто ностальгировал по своим первым приложениям и вдруг вспомнил что у меня осталсь нерешенное дело. Учить PHP я однажды пытался и вроде чет простое написал тогда (давным давно), но сейчас хотелось использовать уже существующие знания, поэтому я усердно гуглил Kotlin server side. И неожиданно наткнулся на туториал где все было понятно написано. Вот ссылка на него.

После чего трудность заключалась в том, чтобы запустить код на своем хостинге. Как оказалось, ничего бесплатного не существует (вот это да!) и я начал искать иные решения. Итого я пришел к тому, чтобы использовать свой ноутбук как сервер и подключаться к нему через его IP. Неожиданно, все сработало и я начал писать уже крестики нолики на основе этого туториала.

Итак, давайте разберем код сервера, который доступен по этой ссылке. Оказалось, писать сервер дело не такое уж и сложное как могло оказаться. С моей стороны я добавил всего 3 класса, можно посмотреть на весь код сразу по этому коммиту.

Первое что нужно было написать — модель данных, которая будет храниться. Оказалось, она хранится в памяти, что меня в принципе устраивало. Мне не нужно сохранять базу данных в постоянную память и потом к ней обращаться. Нет, мне по сути нужно всего лишь хранить шаги, которые были сделаны игроками в течение сессии. Итак, модель данных по сути ничто иное как информация по состоянию игры. Как и для любой модели нужен айдишник. И так как я буду использовать и переиспользовать один и тот же обьект, то написал константу айди игры по которой буду обращаться во всех случаях и которую опять же буду удалять и создавать заново. Далее идет айди первого игрока и второго. Ведь нам нужно как-то различать игроков и для этого нет ничего лучше чем Long. Забегая вперед скажу, что с андроида я буду слать айди игрока как время первого входа в приложение, оно довольно-таки уникально и вероятность совпадения с айди второго игрока минимальна. Далее нам нужно сохранить состояние игры — и для этого я использую 4 константы. Начальный этап (когда еще игра не началась и мы ждем игроков), уже началась (мы в процессе игры), победа и конечно же ничья. Чуть позже обьясню как это все работает. Так же мне нужно было хранить айди того игрока, чья сейчас очередь делать ход, чтобы соответственно отображать на андроид нужную информацию (ваш черед делать ход или ждем пока другой игрок сделает ход). И если наша игра закончилась победой одной из сторон, нужно сохранить айди победителя для финального состояния игры (вы победили или вы проиграли). Ну и конечно же нужно сохранять что наши игроки выбирали в течение игры — для этого я написал сущность ячейки(поля). Я сохраняю по айди ячейки значение которое является айди игрока который поставил туда крестик или нолик. И здесь была первая проблема. Изначально я клал в сущность игры список ячеек и это было ошибочное решение. Мы все же имеем дело с бд и нужно разделять сущности. А связывать их уже на выходе. Именно для этого и была создана сущность GameOutput. Которая является некоей составляющей, которая хранит и данные по игре и по ячейкам. Точнее она ничего не хранит, она является просто моделью передачи данных. Вот и все. Так же в этот файл я положил методы extensions для проверки состояния после каждого хода. Да, правильный сервер тот, который делает всю черную работу на своей стороне. Я бы мог просто передавать данные с одного устройства андроид на другой и там же уже пусть разбираются кто выиграл, а кто проиграл, но клиент должен быть тонким. Это значит, что на сервере нужно писать всю логику, а на клиент отдавать минимальное количество данных для оторажения. Ну и сами посудите, если у вас андроид и айос и еще какой фронт, то писать один и тот же код проверки состояния нужно будет несколько раз. А так у тебя один код на сервере и все замечательно. Так как код сервера можно менять буквально мгновенно — вносишь изменения в код, билдишь проект, перезапускаешь сервер, готово.

Итак, посмотрим на код, который собственно позволяет хранить модели данных (ссылка). Здесь все максимально просто и понятно — 2 строчки. Создаем 2 интерфейса для хранения моделей — привет Spring! Вам больше ничего не нужно делать. Отнаследовались от CrudRepository и все. Готово.

И осталось наверно самое сложное — написать Rest методы, по которым наш клиент (андроид) будет получать и класть данные в бд. Для этого пишем контроллер (ссылка). Указываем общий хвост («/game») по которому будем работать. И отдаем на вход оба репозитория. Теперь, нам нужно написать всего 3 метода. Первый метод стартовать игру. И так как у нас 2 разных игрока, нам нужно пробросить его айдишник, чтобы сохранить. Для этого я решил просто добавить его в конец параметром (можно было и в теле, но я подумал что так проще). Аннотация @GetMapping делает все что нам нужно — создает гет запрос и требует айди игрока для старта. Ну а дальше все просто. Метод старта игры я требую от обоих игроков. Ведь нам нужно знать кто присоединился к игре. Итак, если первый игрок создает игру, то я кладу в бд новый интанс игры и сохраняю его айди. Да, кто первый создал игру тот и первый игрок и у него будет крестик. А тот, кто создает игру (а на самом деле присоединяется к существующей) во вторую очередь, тот будет нолик. Здесь мы просто берем и кладем его айдишник в уже сущеструющий инстанс игры, ставим состояние Игра началась и уже можем принимать данные по ячейкам. Когда же у нас есть оба айди и игра не в начальном состоянии — т.е. игроки присоединились к ней, то я должен почистить базу данных и создать новую игру и требовать айди игроков по новой.

Дальше встает вопрос — а как один клиент узнает о том, какой был ход у другого? Есть возможность чтобы не только клиент слал запросы на сервер и получал данные, а чтобы так же сервер сам слал данные на клиенты когда нужно. Но это немного сложно реализовать. Поэтому я решил через определенное количество секунд (5 пока что оптимально) получать обновленные данные от сервера. Для этого написал гет запрос на получение актуального состояния игры. Если вы используете @GetMapping без аргуемента, значит клиенту тоже ничего не надо указывать. Здесь мы берем модель данных игры и проверяем на статус — закончилась ли игра или нет, есть ли новые данные (позже обьясню на стороне клиента как это работает).

Ну и конечно же самый сложный (ха-ха) метод — положить данные по ячейкам в бд. Для этого отправляем с клиента сущность ячейки (в ней уже есть айди игрока). Кладем эту сущность в бд и меняем айди игрока чей теперь черед играть. После опять же проверяем на наличие победителя или ничьи и отдаем данные на выход.

Итак, это все. Весь наш сервер. Еще раз как он работает. В начале мы ждем пока 2 игрока создадут игру и начнут ее. После чего уже принимаем новые данные по ячейкам и проверяем состояние игры и отдаем клиентам свежие данные. Когда игра окончена, что необязательно, ждем пока клиенты создадут новую игру.

Вместо тысячи слов давайте посмотрим на видео (ссылка).

Теперь окинем взлгядом клиентскую часть — андроид код (ссылка).

Сразу скажу — моя цель не была написать красивый чистый код по всем канонам программирования, нет, я хотел быстрее написать клиент и потестировать. Поэтому например я не стал писать интеракторы, а закинул всю логику прямо во вьюмодель (кстати посмотрите как это ужасно и не делайте так никогда (п.с. к слову о том, зачем нужны интеракторы)). Первое что вы увидите на входе в приложение — это требование прописать хост. Я не стал хардкодить IP своего ноутбука, а сделал все намного гибче.

Если вы не знаете как найти ip своего ноутбука, то воспользуйтесь гуглом. На Linux Ubuntu это легко делается через ifconfig -a. Так что в поле ввода на экране настроек вам нужно написать что-то типа http://192.168.222.220:8080. После перезапуска приложения можно начать играть. В конце статьи будет подробная инструкция как запустить все это дело и как потестировать. Ну а дальше все просто и понятно из андроид кода. Стартуем игру и запускаем таймер на 5 секунд — получаем данные и если наш черед делать ход — то таймер отменяем. После хода запускаем таймер опять и ждем пока второй игрок сделает свой ход и так до победного. Вот и все. В любой момент времени можно стартануть новую игру, но придется подождать подтверждения со стороны второго.

Итак, подробная инструкция.

1. Скачайте серверный код (ссылка) и откройте в своей любимой IDE (например Intellij Idea).
2. Через терминал перейдите в этот пакет cd xo-android. (см.примечание ниже)
3. Если у вас не установлен MAVEN сделайте это.
4. Билданите проект
5. В терминале пропишите следующее mvn spring-boot:run
6. Вы должны увидеть как стартует сервер

Spring стартанул

7. Качаем клиентский код (ссылка).
8. Запускаем андроид приложение на 2 девайсах.
9. Получаем ip ноутбука (на линукс ifconfig -a в терминале)
10. Вбиваем в андроид приложение http://<ip>:8080 (<ip> айпишник вашей машины)
11. Играем — жмем start game на первом девайсе, жмем так же на втором и играем.

12. Если вам нужно остановить сервер — то в терминале (на линукс) жмем Ctrl+C. После чего можно повторно его запустить.

Примечание. Возможно у вас будут проблемы с запуском сервера, так как я взял проект и просто добавил туда свои файлы и в итоге переименовал. Так что вам скорей всего нужно будет переименовать xo-backend в kotlin-spring-boot.

Если у вас возникли иные проблемы — пишите мне в телеграм @johnnysc

Ну и напоследок немного мотивации — я написал и запустил сервер в пятницу вечером и лег спать. Знали бы вы с каким нетерпением я ждал утро, чтобы написать клиент и начать тестировать.

Занимайтесь любимым делом, друзья мои и будьте так же счастливы как я.

Всем удачи!

Запущенный сервер на линукс
Запись опубликована в рубрике Программирование Java. Добавьте в закладки постоянную ссылку.