Мафия на Go, Vanila JS и WebSocket'aх

lcivd5kwwdn6oujpg2i2ppbgovs.png

Речь пойдет о web-реализации популярной карточной игры «Мафия». Она писалась для развлечения и получения опыта в разработке игр. Первая версия была написана за две недели свободного от работы времени и за такое же время переписана до второй версии. Плюс такой игры — отсутствие ведущего.
Отталкиваясь от целей разработки, я принял решения о реализации/нереализации фич.
Что точно нужно было сделать:

  • Работающая по минимому игра, повторяющая правила классической игры
  • Озвучка команд ведущего на клиентских устройствах
  • Продолжение игры даже после перезагрузки вкладки браузера


Что делать не планировалось или можно было отложить:

  • Регистрация в игре
  • Интерфейс для администрирования
  • Постоянное хранение данных об игре в базе данных
  • Синхронизация времени между устройствами


Backend


https://github.com/mrsuh/mafia-backend
Написан на Go. Хранит в себе состояние игры и отвечает за её логику.

Во время игры можно обратиться к серверу, чтобы узнать полную информацию:

curl 'http://127.0.0.1:8000/info?game=23' | python -m json.tool


Вывод информации об игре

{
"event": "greet_mafia",
"event_status": 2,
"id": 23,
"is_over": false,
"iter": 1,
"players": [
{
"addr": "172.18.0.1:51438",
"createdAt": "2018-09-23T14:39:29.631475779Z",
"id": 33309,
"name": "Anton",
"role": 4
},
{
"addr": "172.18.0.1:51440",
"createdAt": "2018-09-23T14:39:32.867080927Z",
"id": 5457,
"name": "username:0",
"role": 2
},
{
"addr": "172.18.0.1:51442",
"createdAt": "2018-09-23T14:39:32.882463945Z",
"id": 14214,
"name": "username:2",
"role": 1
},
{
"addr": "172.18.0.1:51444",
"createdAt": "2018-09-23T14:39:32.895209072Z",
"id": 63759,
"name": "username:1",
"role": 3
}
],
"win": 0
}


Или узнать состояние сервера:

curl 'http://127.0.0.1:8000/health' | python -m json.tool


Вывод информации о состоянии сервера

{
"runtime.MemStats.Alloc": 764752,
"runtime.MemStats.NumGC": 0,
"runtime.MemStats.Sys": 4165632,
"runtime.MemStats.TotalAlloc": 764752,
"runtime.NumGoroutine": 14
}


Для определения активен ли еще игрок backend посылает heartbeat. Если игрок не ответил после определенного интервала, то он выбывает из игры. В то же время, если игрок переподключился до окончания интервала (пропала сеть), то он может продолжить игру.

Для стабильной работы backend был покрыт Unit тестами со стандартной библиотекой Go, где проверяются основные сценарии работы.


go test mafia-backend/src -cover
ok      mafia-backend/src       1.315s  coverage: 70.7% of statements


Frontend


https://github.com/mrsuh/mafia-frontend
Написан на чистом JS и собран с помощью Grunt.
Не несет в себе никакой логики.

При возникновении события с backend рендерит нужную страницу, отображает данные, которые ему прислали и проигрывает звук нового события.

Frontend хранит ID игры и игрока в LocalStorage или строке запроса браузера (если необходимо запустить в одном браузере несколько вкладок для разных игроков). Полное отсутствие логики, а также хранение основных параметров игры дают возможность даже после перезагрузки страницы восстановить состояние игры.

Браузер запрещает автовоспроизведение звуков без участия пользователя (например, нажатия на кнопку). Чтобы воспроизводить звуки на каждое событие, которе приходит с backend был сделан всего 1 JavaScript объект Audio. Каждый игрок должен нажать кнопку для начала игры и в этот момент объект Audio становится активным (доступным для воспроизведения), и впоследствии у него можно менять параметр src для воспроизведения разных звуков без участия пользователя.

Также для тестирования работы игры был написан «бот», который может играть сам с собой.
Достаточно открыть вкладку браузера, где в параметрах указано, что нужно запустить тест

http://127.0.0.1?master=1&test=1&sound=0&testUsersCount=5


и разрешить открывать новые вкладки из JavaScript для этого домена.
После начала игры откроются еще 5 вкладок с игроками и они начнут играть между собой.

Протокол взаимодействия


Протокол WebSocket был выбран по причине необходимости постоянного двухстороннего обмена данными между backend и frontend и его поддержки обоими языками.

События игры


Вся игра разделена на события:

События
  • game
    • create
    • join
    • start
    • over
    • reconnect

  • day
  • night
  • citizens-greeting
    • start
    • role
    • end

  • mafia-greeting
    • start
    • players
    • end

  • court
    • start
    • players
    • end

  • mafia
    • start
    • players
    • end

  • doctor
    • start
    • players
    • end

  • girl
    • start
    • players
    • end

  • sherif
    • start
    • players
    • end



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

Docker


Всю игру можно поднять с помощью Docker:
docker-compose.yml

version: '3'

services:
    mafia-frontend:
        image: mrsuh/mafia-frontend:latest
        container_name: mafia_frontend
        ports:
            - 9080:80

    mafia-backend:
        image: mrsuh/mafia-backend:latest
        container_name: mafia_backend
        ports:
            - 8000:8000


Достаточно установить Docker (если вы этого еще не сделали), скопировать к себе текст docker-compose.yml и выполнить команду:

docker-compose up


После этого можно открывать вкладку с игрой в браузере:

http://127.0.0.1:9080


Заключение


Вот тут можно посмотреть что итоге получилось (скорость воспроизведения увеличена в 1.5 раза).


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

P.S.: Спасибо Лере за озвучку игры.

© Habrahabr.ru