Эволюция на React+Redux

    КДПВ

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


    Сначала немного об игре, прячу под спойлер для тех, кто пришел за техническими подробностями:


    Об игре

    Игра состоит из колоды карт и фишек еды. Каждый ход делится на фазы:


    Фаза развития: все выкладывают карты по очереди. Карту можно положить двумя способами — рубашкой, как животное, или же как свойство на уже существующее.


    Фаза питания: первый игрок кидает кубики и выкладывает фишки еды в кормовую базу. По очереди каждый игрок берет оттуда по одной фишке и кормит ею свое животное.


    Фаза вымирания: те животные, кому не хватило еды, умирают, затем игроки получают новые карты из колоды и начинают все заново.


    Когда колода закончится, все подсчитывают очки за животных и накопленные свойства.


    Свойства самые разные, я не буду перечислять все, а приведу пару примеров: "Жировой Запас": животное может взять дополнительную фишку еды и “отложить” её как, собственно, жировой запас, так что в голодный ход оно выживет. Есть ещё парные свойства, связывающие два вида, например, "Сотрудничество": Когда одно животное получает еду, второе получает фишку еды бесплатно.


    И одно, особенное свойство "Хищник +1": животному для выживания требуется на единицу больше еды, но зато оно может атаковать и кушать других.


    Собственно, в этом и заключается игра — не просто брать фишки еды, а ещё и защищаться от хищников.


    Если хотите ещё примеров — то есть “Большое +1” (Большому животному нужна дополнительная еда, но зато скушать его может только хищник с таким же свойством) или же "Камуфляж" — животное можно атаковать, только если у хищника есть свойство "Острое Зрение".


    Некоторые, например "Паразит +2", можно выложить только на животное соперника, тогда ему потребуется на 2 фишки еды больше, что усложнит его выживание.


    В целом, игра отличается довольно простыми базовыми правилами, однако просчитывать все взаимодействия довольно интересно и иногда сложновато. Отдельно стоит упомянуть дополнения, которых примерно три штуки, они переворачивают всё вверх дном. То есть, если первое ещё нормальное, просто добавляет девять новых свойств (хоть и с хитрой механикой), то второе, "Континенты", делит стол на три части и вся игра происходит на трех непересекающихся континентах. А "Растения" убирают из игры кубики, и кормовой базой становятся, собственно, растения, которыми тоже можно управлять.


    Так, вот, теперь о проекте, его я прятать под кат не буду, вы же за этим и пришли:


    Как-то раз, я решил изучить тогда ещё новомодные React и Redux… Нет, неправильно начинать сразу с них, сначала про то, что позволило мне дописать хоть одну игру в своей жизни и вообще спасло проект:


    Тесты




    Дело в том, что писал я вечерами после работы и, естественно, не каждый день, однако даже спустя месяц я мог открыть проект, в котором ничего не помню, и спокойно начать кодить очередную фичу. Не уверен, что у меня получились именно юнит-тесты, потому что в основном я тестирую так:


    it('User0 creates Room, User1 logins', () => {
      const serverStore = mockServerStore(); // На самом деле не mock, а просто серверный стор, с подмененным сетевым middleware
      const clientStore0 = mockClientStore().connect(serverStore); // Аналогично не mock
      const clientStore1 = mockClientStore().connect(serverStore);
    
      // Диспатчим логин
      clientStore0.dispatch(loginUserFormRequest('/test', 'User0', 'User0'));
      // Диспатчим создание комнаты
      clientStore0.dispatch(roomCreateRequest());
    
      const Room = serverStore.getState().get('rooms').first();
    
      clientStore1.dispatch(loginUserFormRequest('/test', 'User1', 'User1'));
    
      expect(clientStore0.getState().get('room'), 'clientStore0.room').equal(Room.id);
      expect(clientStore0.getState().getIn(['rooms', Room.id]), 'clientStore0.rooms').equal(Room);
    
      expect(clientStore1.getState().get('room'), 'clientStore1.room').equal(null);
      expect(clientStore1.getState().getIn(['rooms', Room.id]), 'clientStore1.rooms').equal(Room);
    });

    То есть, с одной стороны я старался тестировать максимально изолированный кусок функциональности, с другой — диспатчу действие на клиенте, который сам “отсылает” его на сервер, получает ответ, а я только проверяю создание комнаты.


    Кстати, если заметили — тесты у меня синхронные и работают за счет синхронного мока для socket.io. Не нашел ничего подобного на npm, поэтому завелосипедил. Нет, я признаю, на самом деле это очень спорный момент, потому что весь проект также должен быть синхронным, но на каждый помидор я отвечу KISS. Конечно, я пытался переписать всё на асинхронные тесты (с async/await), однако понял, что клиентский dispatch должен будет отдавать promise с сервера, и мне придется корячить сетевой middleware только для тестов, а как-то не хочется всё менять. Однако, в теории, это возможно.


    Пример более продвинутого теста:


    Когда существо со свойством "Хищник" нападает на существо со свойством "Мимикрия", тот оно, если возможно, перенаправит атаку на другое существо того же игрока:


    it('$A > $B m> $C', () => { // Это типа существо A нападает на B, а то мимикрирует под C
      const [{serverStore, ParseGame}, {clientStore0, User0, ClientGame0}, {clientStore1, User1, ClientGame1}] = mockGame(2);
      // mockGame(количество игроков) создает сервер и клиенты игроков и возвращает массив из [{serverStore, ParseGame}, ...и тут пошли игроки]
      // ParseGame принимает описание игры в yml формате и возвращает ID'шник игры. 
      // А внутри оно создает игру и запускает в нее игроков.  
      const gameId = ParseGame(`
    phase: 2 // Фаза кормления (потому что нападать можно только в нее)
    food: 10 // Количество фишек еды на столе = 10 штук, просто так
    players: // Массив игроков
    - continent: $A carn // Существо с id "$A" и свойством Хищник, которое в игре зовется TraitCarnivorous, и резолвится по подстроке.
    - continent: $B mimicry, $C // Два существа - одно с ID "$B" и свойством Мимикрия, а другое просто с ID "$C".
    `);
      const {selectAnimal, selectTrait} = makeGameSelectors(serverStore.getState, gameId); // Я не использую reselect (а зря), поэтому тут такие хелперские селекторы
      expect(selectTrait(User1, 0, 0).type).equal('TraitMimicry'); // Надо бы удалить, но тут я проверяю что у второго игрока у первого животного первое свойство и правда мимикрия.
      // А активирует навык "Хищник" на существо Б
      clientStore0.dispatch(traitActivateRequest('$A', 'TraitCarnivorous', '$B')); 
      expect(selectAnimal(User0, 0).getFoodAndFat()).equal(2); // А получило еду за успешную охоту
      expect(selectAnimal(User1, 0).id).equal('$B'); // Однако В живо
      expect(selectAnimal(User1, 1)).undefined; // А вот С мертвое = В успешно перенаправило атаку.
    });

    Таких тестов на мимикрию у меня 7 штук:


    А атакует Б с мимикрией, С с камуфляжем (Б не может перенаправить атаку на С, ведь оно невидимое, и А съедает Б)


    А атакует Б с мимикрией, просто С (вышеописанный случай)


    А (Хищник), Б (Мимикрия), С (Мимикрия): А атакует Б, Б перенаправляет атаку на С, С перенаправляет атаку на Б обратно, но игра не входит в бесконечный цикл, а А съедает Б


    А (Хищник), Б (Мимикрия), С, D: А атакует Б, и игра спрашивает у игрока 2, каким именно существом (C или D) он хотел бы пожертвовать? Тот отвечает что C, и А съедает C.


    А (Хищник), Б (Мимикрия), С (Мимикрия), D: А атакует Б, игра спрашивает у игрока 2, каким именно существом (C или D) он хотел бы пожертвовать? Тот отвечает, что C, то опять мимикрирует, и игра спрашивает во второй раз, каким именно существом (B или D) на этот раз тот пожертвует. Игрок отвечает, что B, и оно умирает.


    А (Хищник), Б (Мимикрия), С, D: А атакует Б, и игра спрашивает у игрока 2, каким именно существом (C или D) он хотел бы пожертвовать? А тот не отвечает, и игра сама принимает решение, кого убить.


    Асинхронный тест, аналогичный предыдущему, но где игрок никак не отвечает за отведенный промежуток времени в 1мс. В качестве "игрок не ответил" я использую await new Promise(resolve => setTimeout(resolve, 1));


    И последний тест, видимо, связан с каким-то багом: он проверяет, что, после охоты на существо с мимикрией, наступает новый раунд. Не помню, зачем.


    К чему это всё? К тому, что я могу не беспокоиться, что где-то у меня мимикрия сработает неправильно. Я могу переписать всю логику охоты или "задавания вопросов", а тесты покажут, что я облажался всё работает.


    Поэтому, кстати, не надо проверять детали. Только существенный логический исход, типа существо С умерло, существо А получило еду итд. Одно время я пытался проверять какие-то скрытые параметры (типа, у игрока стоит флаг "походил"), однако, по итогу, я просто стал проверять, что игрок не может походить снова.


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


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


    Клиент и дизайн.


    Да и сейчас игровая часть клиента меня вообще не устраивает, но я не могу придумать ничего лучше. В идеале, должен был получиться “Material UI Hearthstone” с крутым “visual language”, который “synthesizes the classic principles of good design with the innovation and possibility of technology and science” Material design. Introduction, а получились серые прямоугольнички с Roboto посередине. Нет, ладно, на самом деле меня вообще не колышет дизайн, но есть же ещё сам “стол”, то место, где лежат карты, еда и существа. И вот тут-то полный швах, начиная от того, что мне не вместить всю информацию, и заканчивая тем, что у меня парадоксально много свободного места.


    Дело вот в чем — во-первых, я отвратительный дизайнер и из стилей предпочитаю брутализм. Во-вторых, мне лень. И, в-третьих, сама игра подкладывает свинью — у игрока может быть как одно, так и двадцать существ. И на них также может быть от одного до двадцати свойств. А самих игроков — от двух до восьми. Так что я не представляю как сделать что-то вменяемое, что будет масштабироваться от пары объектов до сотни. Возможно, вариант сделать всё “как в Hearthstone” с его принципом “как настольная игра” здесь не самый лучший.


    React


    Пусть оно так себе на вид, зато работает, и в этом большая заслуга React'а и его детерминированности.


    It fills you with determination

    Не всегда хватает воли для жесткого MVC/MVVM, однако React таки заставляет выносить всю логику вовне и гарантирует, что при состоянии X (которое легко узнать), UI будет вот такой-то. Как я прочитал у кого-то "React — это функция, которая принимает состояние и возвращает UI". Вместе с Redux это избавляет от сайд-эффектов и "наполняет определенностью", я точно знаю, что, где и когда у меня происходит. Это очень круто, плюс, я не испытываю отвращения к jsx, наоборот, не надо запоминать всякие фишки шаблонов типа {%<{{x | filter % sdfsdf}}>%}, а так же не надо определять области видимости. Не знаю, как с этим в vue и angular 2, но в первом, ох уж эти скоупы. Да и в целом проще дебажить.


    Ну и всякие фичи типа порталов меня прямо поразили. Действительно, я пишу компонент для комнаты, почему бы в нём же не протянуть что-то в header? И не гокодерски запихнуть туда, а только при наличии в нем компонента <PortalTarget name='header'/>


    export class Room extends Component {
      ...
      render() {
        const {room, roomId, userId} = this.props;
        return (<div className='Room'>
          <Portal target='header'>
            <RoomControlGroup inRoom={true}/> // <= вот эта штука рисуется в Header'е
          </Portal>
          <h1>{T.translate('App.Room.Room')} «{room.name}»</h1>
          <div className='flex-row'>
            <Card className='RoomSettings'>
              <CardText>
                <RoomSettings {...this.props}/>

    Мультиязычность мне показалось самым удобным сделать через i18n-react, для дизайна я использую использую react-mdl. Отдельные лучи любви вперемешку с ненавистью высылаю библиотеке react-dnd, она крута.


    Однако, у React’а есть и минус — анимации. Что-то сложнее чем CSS Transitions сделать уже не так просто. Да и получается, что состояние одно, а UI должен быть разным.


    Я решил эту проблему отвратительнейшим образом, породив чудовищного монстра — AnimationService. Вкратце, он сует свой middleware в клиента, отлавливает все действия и запускает анимацию для первого из них, остальные кладет в очередь и, как только анимация завершена, запускает следующее. Что дает кучу багов, например с тем, что пока карты красиво летят вам в руку, вы не можете выйти из игры.


    С другой стороны — я могу анимировать компоненты с Velocity.js как-то так:


    export const createAnimationServiceConfig = () => ({ // уже по названию можно определить, что дело нечисто
      animations: ({subscribe, getRef}) => { // subscribe - подписаться на Action, getRef - получить компонент по строке
    
        // Подписываться так:
        subscribe("тип действия", (done (надо вызвать по окончанию анимации), actionData, getState) => {
          // Вот тут можно императивно анимировать
        ...

    На самом деле, зря я его написал, и единственная анимация, для которой пригодился этот монстр — это раздача карт (зато как в Hearthstone!!11!), так что хватит о нём.


    Итак, в общем, с React'ом почти всё хорошо, во многом благодаря тому, что он не лезет не в свое дело, а логикой занимается Redux.


    Redux


    Именно он делает всю работу и на клиенте, и на сервере. И даже общаются между собой они через middleware с socket.io. Я сделал некое подобие RPC, выглядит как-то так (приготовьтесь, сейчас будет большой кусок кода из game.js)


    // Game Create
    // Request на конце обозначает, что действие клиентское
    export const gameCreateRequest = (roomId, seed) => ({
       type: 'gameCreateRequest' // Да, типы действий у меня строкой, сорри
      , data: {roomId, seed} // Это данные
      , meta: {server: true} // Middleware на клиенте поймает этот параметр и перешлет действие серверу
    });
    
    // Это действие сервер вышлет тем клиентам, которые начинают игру
    const gameCreateSuccess = (game) => ({
      type: 'gameCreateSuccess'
      , data: {game}
    });
    
    // А это - всем клиентам
    const gameCreateNotify = (roomId, gameId) => ({ 
      type: 'gameCreateNotify'
      , data: {roomId, gameId}
    });
    
    // Вызывается самим сервером
    export const server$gameCreateSuccess = (game) => (dispatch, getState) => {
      // Сначала сервер создает игру в своем Store
      dispatch(gameCreateSuccess(game)); 
    
      // Потом высылаем всем Notify, что игра создана
      dispatch(Object.assign(gameCreateNotify(game.roomId, game.id) 
        , {meta: {users: true}}));
    
      // Потом каждому игроку высылаем свою версию игры.
    
      selectPlayers4Sockets(getState, game.id).forEach(userId => { 
        dispatch(Object.assign(gameCreateSuccess(game.toOthers(userId).toClient())
          , {meta: {userId, clientOnly: true}}));
      });
    
      // Немного криво сделано, потому что раньше игра высылалась игрокам сразу вместе с картами и, соотвественно, требовалось высылать каждому игроку свою копию игры. 
      // Теперь все не так и метод можно переписать на что-нибудь типа:
      // dispatch(Object.assign(
      //   gameCreateSuccess(game.toOthers(null).toClient())
      //   , {meta: {clientOnly: true, users: selectPlayers4Sockets(getState, game.id)}}
      // ));
      // Но мне лень.¯\(°_o)/¯ 
    };
    
    // ... Ещё 40 действий ...
    
    // И потом ноу хау:
    
    export const gameClientToServer = {
      gameCreateRequest: ({roomId, seed = null}, {userId}) => (dispatch, getState) => {
        // Тут всякие проверки, создание игры и прочее, и потом
        dispatch(server$gameCreateSuccess(game));    
      }
      // ...
    }
    
    export const gameServerToClient = {
      // А это то, что поймает клиент
      gameCreateSuccess: (({game}, currentUserId) => (dispatch) => {
        dispatch(gameCreateSuccess(GameModelClient.fromServer(game, currentUserId)));
        dispatch(redirectTo('/game'));
      })
      ...
    }

    Объект gameClientToServer состоит из разрешенных серверу на прием действий, так что напрямую действие типа "shutdownServer" послать не получится. А обратный просто переводит какие-то модели или ещё что-нибудь из JSON объектов в, собственно, модели.


    Работает это так:


    1) Юзер жмет кнопку “Начать игру”.
    2) React-redux диспатчит действие gameCreateRequest
    3) Клиентское middleware:


    const nextResult = next(action);
    if (action.meta && action.meta.server) {
      action.meta.token = store.getState().getIn(['user', 'token']);
      socket.emit('action', action);
    }
    return nextResult;

    nextResult нужен для тестов (которые у меня, напомню, синхронные), если вызывать next(action) после socket.emit(), то клиентский reducer обработает действие отсылки позже ответа от сервера.


    4) Сервер принимает действие:


    socket.on('action', (action) => {
      if (clientToServer[action.type]) { // clientToServer есть объект, собранный из всех xxxClientToServer, будь то roomClientToServer или gameClientToServer
        const meta = {connectionId: socket.id} // Иногда серверу в ActionCreator'е нужен id сокета. Например, для логина юзера.
        if (!~UNPROTECTED.indexOf(action.type)) { // Если тип действия не в массиве UNPROTECTED, то валидируем токен
          // валидация токена
        }
        const result = store.dispatch(clientToServer[action.type](action.data, meta));
        // собственно вот тут и вызывается gameClientToServer.gameCreateRequest со всеми параметрами

    5) Как я писал выше, вызывается server$gameCreateSuccess, которые диспатчит gameCreateSuccess только серверу, затем gameCreateNotify и gameCreateSuccess каждому из игроков
    6) Reducer сервера ловит gameCreateSuccess и создает игру
    7) Middleware сервера ловит gameCreateNotify и отправляет его всем клиентам (чтобы они знали, что игра в такой-то комнате началась)
    8) Так же оно ловит последующие gameCreateSuccess (с игрой для каждого игрока), отправляет и не пускает к серверному Reducer’у (потому что в meta указано clientOnly: true)


    Вот как-то так оно все и работает.


    Окружение


    Работает оно на herokuapp на бесплатном аккаунте. Что не очень хорошо, так как они требуют 6 часов даунтайма. Однако, в связи с полумертвой посещаемостью (иногда, ночью, по будням играют 3 чувака из Сибири), меня это не очень беспокоит.


    Потому же, меня не беспокоит и то, что логин через ВК у меня не читается из базы, а запрашивается каждый раз заново. Забавно, конечно — как-то раз я подумал, что проект достаточно вырос для использования базы данных, прикрутил бесплатную монго от mlab.com, даже пишу туда ВК токены и… просто запрашиваю новые. Нет, я не спорю что когда-нибудь я все-таки буду при логине запрашивать статистику и Oauth токены, но пока что БД бесполезна чуть более, чем полностью.


    Состояние всех игр хранится прямо в redux. Я где-то видел сумрачных гениев, что хранят состояние в базе, но лично я не понимаю, зачем. Возможно, я не прав.


    Собирается первым вебпаком, второй тогда ещё не вышел. В разработке клиент идет через webpackMiddleware, а сервер — через nodemon+babel-node. Единственный минус — при изменении на бекенде приходится долго ждать пока пересоберётся фронтенд. Я пытался сделать hot reloading для ноды, но как-то не пошло. Да и зачем, для сервера у меня есть тесты.


    Вкратце ещё упомяну “нетрадиционный” логгинг — в файл писать не вариант, ибо heroku всё стирает, а всякие специализированные сервисы либо неудобные, либо платные, поэтому я нашел замечательный модуль для winston — winston-google-spreadsheet. Да, он пишет логи в гуглотабличку. Мне нравится больше чем тот же loggly.


    Выводы:


    Технические:


    React, хоть уже и устарел (:trollface:), но сознание переворачивает, и, я считаю, к ознакомлению обязателен.
    То же и про Redux.


    Синхронные тесты хороши, но именно настолку или пошаговую игру я бы сделал через асинхронно и с promise’ами. То есть, отправил — дождался ответа. Тогда на сервере не придется страдать от невозможности задать какому-либо действию коллбек.


    Любые коллекции надо делать Map’ами или объектами. В самом начале я подумал — хммм, KISS, зачем мне объект с животными, когда я могу хранить их в списке. В результате, game.getAnimalById идет поиск по массиву. Да, ошибка, мне стыдно, когда-нибудь я это перепишу.


    Гуманитарные:


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


    Во-вторых — я взял неправильную игру. Основная сложность и геймплей эволюции — в вычислении комбинаций и их взаимодействия. Компьютер забирает все просчеты себе и человеку остается лишь выбрать из пары вариантов. Таким образом, геймплей пусть и не уничтожен, но порушен знатно, так как продумывать его следует наперед,. Ну и, спасибо авторам, они радуют дополнениями, которые ставят всё с ног на голову. То есть был у игрока один "континент" с животными, а тут их хоп, три. Круто! Интересно! Половину игры перепиши, ага-да :D


    Суммируя — у меня получилось то, что я хотел. Код, я считаю, местами даже красивый, а в целом — не отвратительный (кроме AnimationService, конечно). Вот тут можете форкнуть / прислать пулл-реквест / помочь с разработкой / запостить issue / перевести на английский ru-ru.json / помочь с дизайном (это все ещё не тонкие намеки), чуть ниже можете высказать всё, что думаете обо всяких хипстерах, лезущих кодить на богомерзком недоязыке. Чтобы не попасть в Я пиарюсь, кину ссылку на сайт в комменты.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 28
    • 0

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


      http://evo2.herokuapp.com

      • 0

        UPD: Столько людей игра не видела за всю жизнь о_о.


        Так, чтобы уменьшить фрустрацию, вот правила настолки: http://rightgames.ru/sites/default/files/evo-rules-baseset-148x195-ru_scr.pdf


        Чтобы начать игру: подождать пока кто-нибудь зайдет к вам в комнату и сверху "начать игру".
        Сыграть существо: перетащить карточку.
        Положить на него свойство: перетащить карточку.
        Положить парное свойство: перетащить на первое существо, потом клик на второе.
        Активировать свойство: либо нажать, либо перетащить.

        • 0
          Ах да, естественно только хром

          отличные времена настали…
          • +1

            Ну, вкратце, да. Я просто использую все последние стандарты, для ускорения разработки. Копаться почему всякие ИЕ требуют каких-то извратов — пожалуйста, код открыт, можете прислать пулл-реквест! :)

        • +3
          Спасибо за open source!
          Сам являюсь поклонником этой игры и настолок в принципе.
          Обе найденные на просторах интернета реализации как-то стухли. И конечно они были без исходников.
          Теперь в случае чего можно будет поиграть с друзьями, даже если нас отделяют тысячи километров.
          • 0
            Если вам нужно что-то сложнее обычного transition, используйте css transition group, если еще сложнее, то такое состояние нужно выносить в стор. Особенно, если его нужно уметь согласовывать с другими сложными анимациями и/или отменять.

            В качестве «борьбы с асинхронщиной», могу посоветовать посмотреть в сторону redux-saga.

            PS. ну и, собственно, как начать игру я так и не разобрался :)
            • 0
              Чуток потыкавшись, у меня получилось. Даже один кон сыграл. Вот только геймплей действительно кажется сильно урезанным, в отличие от настолки. Всё происходит стремительно и не так лампово.
              • 0

                Это потому что нету анимаций и дизайн страшненький. У того же Hearthstone (я не пиарю его, просто сильно ориентируюсь) получается вполне лампово.

              • 0

                А когда анимация чуть ли не по path, с поворотами и прочим? Зачем в стор, если проще не хранить её и чуть что не так — сбрасывать.


                Заходите в комнату — ждете ещё человек, потом "Начать игру" сверху. Перетаскиваете карту из "руки" на мелкий зеленый "стол"

                • 0
                  А когда анимация чуть ли не по path, с поворотами и прочим?
                  Я имел в виду не саму логику анимации, а ее состояние.

                  Зачем в стор, если проще не хранить её и чуть что не так — сбрасывать.
                  Конечно, можно и так, никто ж не запрещает. Но сбрасывать — это лишь частный случай, и тут помогает transition group. Но когда вам придется среагировать в компоненте на окончание другой анимации в другом компоненте, вы вынесете работу с этим состоянием наверх.
                • 0
                  Создать комнату
                • 0
                  Во-первых, переводить настолки в онлайн — дело неблагодарное. В том плане, что тонкостей и правил много, вещи, которые решаются между игроками буквально парой слов, превращаются в мегабайты кода, запросов и костылей. А настольщики всегда будут недовольны какой-то мелочью, которую вот никак не сделать. Плюс — это всегда мультиплеер, причем долгий по геймплею, а значит и игроков будет мало.


                  А вот у меня есть кейс, когда онлайн-настолка лучше. Я играю в основном с племянником (+ еще кто найдется), а мои дети сильно младше, а карточки красивые… Короче и не играют, и в процессе мешают, и не отойдешь никуда — разграбят)

                  А по поводу геймплея, мне подумалось что Империал 2030 хорошо бы пошел в онлайне — там много бойлерплейта по типу начисления налогов и т.п., часто сбиваемся. Автоматизация бы контролировала процесс. А дети империал грабят еще охотнее — там же фигурки танков и заводов, деньги цветные, акции…
                  • +1

                    круто, статью утащил в закладки, буду обращаться)


                    ну и поиграть тоже время найти надо

                    • +1
                      Облегчат жизнь https://github.com/acdlite/redux-actions и https://github.com/redux-saga/redux-saga
                      • +1
                        Сам сейчас работаю на веб-версией настольной игры. Вы задумывались о проблемах с копиратом?
                        • 0

                          Задумывался. Но, игру я делал потому что у меня нет друзей и чтобы поупражняться в коде. Так что я не планирую получать с неё доход, а если авторы игры потребуют — удалю не вопрос.
                          Более того, я потом добавлю куда-нибудь в футер "все права не мои, вот сайт авторо игры" и буду надеяться, что не тронут)

                        • +1
                          Спасибо, сыграл одну игру, очень даже неплохо. Можно немного улучшить оформление и добавить чат комнаты (я не смог найти, может быть он всё-таки есть). Очень интересно, спасибо! Посоветую друзьям.
                          • +1
                            Огромное спасибо за труды! С игрой знаком достаточно хорошо (правда, в классическую версию давно не играл — сейчас, в основном, в «Случайные мутации» рубимся). Сыграли партию, протестировали. Из багов бросились в глаза неформатированные сообщения об ошибках (названия свойств не подставляются). Долго не могли найти чат после старта игры — надо визуализировать как-то получше его. Остальные нюансы, принимаемые сперва за баги, оказывались следствием нашей невнимательности (пожалуй, стоит поработать над более подробными пояснениями при ошибках или запретах действий — например, сделать очевиднее причины невозможности «прицеливания» хищником на животных с определенными свойствами). Короче говоря, над оформлением/UI/UX можно поработать, но в целом все круто! Надеюсь, что не забросите это дело.
                            • 0

                              Не заброшу, но и не буду так активно разрабатывать, увы. Поработать есть над чем, поэтому и open source ;)

                            • 0
                              По поводу анимаций, рекомендую посмотреть в сторону библиотеки GSAP.
                              На собственном опыте столкнулся со сложностями анимации компонентов,
                              Эта библиотека вкупе с правильным использованием лайфхуков компонентов помогла решить большинство проблем
                              • 0
                                Шикарно! Очень не хватает «интеллекта» и вообще 2ого дополнения.
                                • 0

                                  Если вы про "Время летать", то оно там есть, в настройках комнаты можно включить

                                • 0
                                  сколько ж времени ушло на разработку? неужто 2 дня, если судить по коммитам на гитхабе?)
                                  • 0

                                    с июня 2016, я просто зафакапил чутка, пока переносил с другого аккаунта и стирал личный емейл и прочую мету =(

                                  • 0
                                    Вообще говоря, порт любой настолки — это титанический труд, снимаю шляпу. Знаю по собственному опыту, даже простая игрулина типа «Магии» эпохи заката СССР (кто старше 33, тот поймёт и всплакнет) занимает человеко-месяцы упорного и вдумчивого труда. Так что от всей души желаю удачи в этом начинании.

                                    По поводу дизайна могу сообщить от что — на просторах Сети в своё время встретил вот такую реализацию — . Вполне может подойти в качестве рескина дла вашего творения.
                                    • 0
                                      npm i — выдает такую ошибку
                                      /evolution-web/globals.js:28
                                      if (!process.env.JWT_SECRET) throw new Error('JWT_SECRET undefined');
                                      • 0

                                        после npm i автоматически начинается билд, которому уже нужны переменные окружения.


                                        просто действуйте дальше по инструкции. Нужны только JWT_SECRET и MONGO_URL

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.