Приложение Дурак для Windows Store


    Поль Сезанн, «Игроки в карты»

    Давным-давно, в Windows 95 была игра Microsoft Hearts. Игра в карты по сети, с оппонентами по всему миру. Если мне не изменяет память, то в Windows for Workgroups 3.11 (да, я застал все эти артефакты!) была версия для игры по локальной сети, с использованием так называемого NetDDE.

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

    Ситуация осложнялась тем, что до сих пор я никогда не занимался разработкой «бэкенда». Гуглинг привёл меня сразу в нужное место — SignalR.

    Здесь мне хочется сказать несколько восторженных слов в адрес SignalR. Прекрасно документированная библиотека, подошедшая как нельзя лучше к моим нуждам. Зануды скажут, что она только под винду, ну и пусть они скрипят зубами от зависти. Хотя вроде есть её колхозные клиенты для iOS, детально этот вопрос я не изучал.

    Дальше вопрос хостинга. Тут я размышлял недолго, под руками был Azure.

    Итак, чего же мне хотелось?


    • Способ подключения игроков должен повторять способ Microsoft Hearts. Игроки подключаются один за одним, как только набралось нужное количество — игра стартует. Для себя я решил ограничиться игрой «один на один» — и никаких ничьих!
    • В самом начале игроков будет немного — как же они узнают друг о друге? Тут возникла идея рассылать всем желающим поиграть push-уведомления в тот момент, когда кто-то запускает приложение и подключается к игровому серверу. Чтобы не троттлить пользователей пушами, сделал ограничение «не чаще чем раз в N минут».
    • Хочется подробнейшей статистики, призов, достижений и пр.
    • Хочется карт разного дизайна
    • Хочется играть со своим знакомым, а не с кем «кого бог послал».

    Что я использую у Azure?


    • AppService — собственно, приложение, к которому подключаются все клиенты.
    • SQL server + SQL database — для хранения игровой статистики.

    Всё.

    Недавно я еще использовал и их службу рассылки push-уведомлений. Но показалось дорого (10 баксов в месяц), кроме того, выяснилось, из-за глюка биллинга у Microsoft я больше года платил за две этих службы! Бодание с поддержкой привело к тому, что они признали ошибку и предложили аж месяц компенсации. Спустя какое-то время я полностью отказался от этой службы, добавив еще одну таблицу в свою базу данных для хранения подписантов на пуши и рассылаю их самостоятельно из основного приложения.

    В данный момент времени стоимость хостинга ежемесячно около 400 р. Это только стоимость SQL сервера. Исходящий трафик у меня небольшой и он вполне укладывается в бесплатные 5 Гб в месяц.

    Разработка


    Разработка проходила на Visual Studio 2015, для клиента использовался MVVM фреймворк MVVM light.

    Немного серверной «кухни» (эстетам и слабонервным лучше не смотреть)


    подключение пользователей, рассылка пушей
    public override Task OnConnected()
            {
                if (((DateTime.Now - LastPush).TotalSeconds) > 360)
                {
                    LastPush = DateTime.Now;
                    Task.Run(() => SendNotifications());
                }
                return base.OnConnected();
            }


    создание анонимной игры
    /// <summary>
    /// старт анонимной игры. При наличии уже подключенного соперника начинается игра
    /// </summary>
    /// <returns>ID начатой игры</returns>
    async public Task<String> ConnectAnonymous(PlayerInformation pi)
            {
                MainSemaphore.WaitOne();
                try
                {
                    string res = String.Empty;
                    string p_ip = Context.Request.GetHttpContext().Request.UserHostAddress;
                    if (NextAnonymGame == null)
                    {
                        NextAnonymGame = new FoolGame(Context.ConnectionId, pi, p_ip, false);
                        res = NextAnonymGame.strGameID;
                    }
                    else
                    {
                        await NextAnonymGame.Start(Context.ConnectionId, pi, p_ip);
                        ActiveGames.Add(NextAnonymGame.strGameID, NextAnonymGame);
                        res = NextAnonymGame.strGameID;
                        NextAnonymGame = null;
                    }
                    return res;
                }
                finally
                {
                    MainSemaphore.Release();
                }
            }


    создание игровой комнаты
    /// <summary>
    /// создание игровой комнаты
    /// </summary>
    /// <returns>кодовое слово игровой комнаты</returns>
    public String CreatePrivateGame(PlayerInformation pi)
            {
                MainSemaphore.WaitOne();
                try
                {
                    string p_ip = Context.Request.GetHttpContext().Request.UserHostAddress;
                    FoolGame game = new FoolGame(Context.ConnectionId, pi, p_ip, true);
                    WaitingPrivateGames.Add(game.strGameID, game);
                    return game.PrivatePass;
                }
                finally
                {
                    MainSemaphore.Release();
                }
            }


    подглядеть верхнюю карту в колоде
    
    /// <summary>
    /// возвращает верхнюю карту из стека-колоды
    /// </summary>
    /// <param name="gameid"></param>
    /// <returns></returns>
    async public Task PeekCard(string gameid)
            {
                FoolGame game = null;
                game = ActiveGames.FirstOrDefault(games => games.Value.strGameID == gameid).Value;
                if (game != null)
                {
                    game.GameSemaphore.Wait();
                    await Task.Delay(35);
                    try
                    {
                        await Clients.Caller.PeekedCard(game.Deck.Peek());
                    }
                    finally
                    {
                        game.GameSemaphore.Release();
                    }
                }
            }


    отправить сообщение сопернику
    
    /// <summary>
    /// чат
    /// </summary>
    /// <param name="gameid">ID игры (и имя группы)</param>
    /// <param name="ChatMessage">текст сообщения</param>
    /// <returns></returns>
    async public Task ChatMessage(string gameid, string ChatMessage)
            {
                FoolGame game = null;
                game = ActiveGames.FirstOrDefault(games => games.Value.strGameID == gameid).Value;
                if (game != null)
                {
                    game.GameSemaphore.Wait();
                    await Task.Delay(35);
                    try
                    {
                        await Clients.OthersInGroup(gameid).ChatMessage(ChatMessage);
                    }
                    finally
                    {
                        game.GameSemaphore.Release();
                    }
                }
            }
    



    О клиенте


    Для идентификации игроков первоначально использовалcя функционал LiveId, затем Graph API. При первом запуске приложения игроку предлагается предоставить доступ к его аккаунту (из него я беру только имя и так называемый анонимный id, выглядящий примерно так: «ed4dd29dda5f982a»). Впрочем, игрок может играть и анонимно, но тогда статистика его игр не ведётся.

    Для каждого неанонимного игрока хранятся:

    1. дата первой игры/дата последней игры
    2. имя/ник игрока
    3. количество сыгранных партий/сколько из них выиграно
    4. последний IP адрес
    5. полученные призы

    Если в игре играют два неанонимных игрока, то перед её началом я получаю статистику игр именно этих игроков (сколько игр они сыграли друг с другом и кто сколько выиграл). Для этого в SQL-запросе и используются полученные анонимные id.

    На скриншоте слева вверху можно увидеть пример (кликабельно):



    Скриншот общей статистики (кликабельно):



    Кроме того, ведётся «соревнование» по странам (тут участвуют и анонимные игроки, информация берётся из IP-адреса):



    Игроки могут обмениваться короткими сообщениями.

    FoolHubProxy.On<string>("ChatMessage", (chatmessage) => synchrocontext.Post(delegate
                {
                    PlayChatSound();
                    ShowMessageToast(chatmessage);
                }, null));

    Пример обработчика ситуации «я беру карты, а соперник добавляет мне еще 'вдогонку'»:

    FoolHubProxy.On<byte, bool>("TakeOneMoreCard", (addedcard, lastcard) => synchrocontext.Post(delegate
                {
                    CardModel card = new CardModel(addedcard, DeckIndex);
                    CardsOnTable_Low.Add(card);
                    OpponentsCards.Remove(OpponentsCards.Last());
                    if (lastcard)
                    {
                        AppMode = AppModeEnum.defeated;
                    }
                }, null));

    О призах, печеньках и пр.


    Каждый месяц первая пятерка игроков, одержавших больше всех побед, получает по бейджику «За взятие Берлина». Участникам топ-50 вручаются бейджики за лучший процент побед (тоже пятерым). Кроме того, есть бейджики за выигрыш «экспрессом» (ситуация, когда на последнем ходу у вас остаётся 2, 3 или 4, скажем, шестёрки или валетов). Тогда они выкладываются на стол все махом и вы — молодец. Есть печеньки за победу, когда соперник дает вам «в лист». Ему тоже достаётся утешительная, с черепом и костями.

    О всяком дополнительном функционале


    Приложение бесплатное, но у него есть разные дополнительные «плюшки», оформленные как InApp Purchases:

    • группировка своих карт по масти и подсказки во время игры (когда нужно бить или подбрасывать, подходящие карты выдвигаются вверх)
    • возможность не показывать сопернику, сколько у вас карт в руках. В обычной ситуации он это видит
    • возможность всегда первым начинать игру. Иначе первый ход разыгрывается случайно
    • возможность увидеть отбитые в игре карты
    • возможность один раз за игру подглядеть в колоду и узнать следующую карту
    • возможность «жульничать». Вы можете 3 раза за игру отбиться или подбросить неправильную карту, вообще любую. Но у соперника есть возможность, заметив это, вернуть вам неправильную карту.

    Заключение


    В результате разработки этого приложения я:

    • познакомился с SignalR
    • освежил в памяти SQL (запросы, хранимые процедуры, функции)
    • узнал, как размещать приложения в Azure
    • опупел от игры в «дурака».

    Вопрос


    А как на Хабре обстоит дело с разбрасыванием денег с вертолёта раздачей в личке промо-кодов желающим? Если за это не дают пинка, то обращайтесь.

    Update


    YouTube: запись пары игр
    Поделиться публикацией

    Комментарии 25

      0
      Уже несколько постов было, где выбор был в пользу MVVMLight, а почему не Caliburn? По мне, вместо MVVMLight можно самому сделать реализацию INPC и DI, а в Caliburn есть и навигация и шина сообщений и упрощение привязок.
        0
        Скажем так — просто не смотрел Caliburn. Руки не доходили.
          0
          Caliburn для тех кто уже разобрался в MVVM и вам не хватает MVVMLight.
          +1
          Честно говоря статья так себе.
          1) Кода мало, и он вообще не информативен, разве что сразу видно стиль.
          2) Написано чудным образом, с прыжками во времени между мыслями.
          3) Ну, а как продукт не уникален, да еще и с забавным платным контентом.
          Но ругать все могут, да и почитать было забавно, спасибо.
            0
            > возможность всегда первым начинать игру. Иначе первый ход разыгрывается случайно

            А если у всех соперников это куплено, то что произойдёт?

            Не в ваш огород камень, но напомнило анекдот про двух ковбоев, которые бесплатно наелись известно чего :))
              0
              )) тогда будет в точности, как в том анекдоте (опять первый ход будет выбран случайно), но вероятность этого крайне мала, учитывая нынешнее количество игроков.
                0
                И тут вспоминается АнтиАнтиАОН от опсосов. Даешь больше подписок.
                  +1
                  Платиновый аккаунт:

                  1 — Видеть всю колоду и иметь возможность менять очередность карт.
                  2 — AI подсказчик хода.
                  3 — Видеть все карты соперника.
                  4 — Возможность поменять любую свою карту на карту из битых, соперник может ее отклонить.
                    0
                    Есть пара отличных мыслей! Ждите апдейт! ))
                      0
                      Боюсь, вы отстали от современных донат-технологий. Нельзя сразу столько возможностей кидать на один-единственный «платиновый» аккаунт. Нужно всё раскидать по VIP-левелам (от VIP0 до VIP20), причём с ростом VIP цена должна расти в геометрической прогрессии.
                      А ещё в обязательном порядке добавить кнопку Auto, чтобы не играть самому, когда это вполне может делать компьютер — в любой азиатской доильнице есть такая кнопка.
                      И конечно же нельзя забывать про лутбоксы, из которых с шансом 0.0001% можно вытянуть разлочку дополнительной масти карт (помимо стандартных четырёх).
                        0
                        На картах нарисовать аниме-девочек и можно выходить в топы мобильных сторов.
                          0
                          Для внимательных читателей напомню, что приложение — для «большой» винды, так что в топ «мобильных» сторов — ну никак… (( А вот за мысль насчёт девочек на картах — спасибо… Немцы, вроде бы, особенные выдумщики насчёт девочек на картах… Незадача только — как быть с валетами и королями? Я в отчаянии!
                            0

                            Главное — идея! Было бы желание, сделали бы и на мобильные

                0
                С платным функционалом, мне кажется, перебор. Из-за потенциальной возможности нарваться на игрока со 2 (ну это еще ничего, ладно), 4 и 5 пунктом даже качать бы не стал…
                  +1
                  Ну если не рассматривать это как честную спортивную игру, а как казуалку… Есть ведь шведские шахматы. ;-) Так что это интересный социальный эксперимент, на самом деле. Будут ли играть знаю что возможен шулер, будут ли покупать возможность шулерства и антишулерства.
                    0
                    На самом деле, я пытался полностью имитировать реальную игру с человеком за столом. Ведь он может спрятать руки с картами под столом, чтобы соперник не увидел, сколько у него карт? Может! Может нахально подглядеть верхнюю карту в колоде? Может! Может наудачу, рассчитывая на невнимательность, сыграть неверной картой? Может! Смотрите на карты внимательно, всегда есть возможность вернуть неправильную карту. Может глянуть в «отбой»? Да легко! Единственное, что тут не из реальной игры — это всегда ходить первым. По собственному опыту скажу )) — это очень удобно, но не является прям уж определяющим преимуществом в игре.
                      0
                      Ну опытный игрок и не показывает количество карт, особенно в конце «кидай по одной и не ошибешься». Опытный игрок может запоминать «отбой» в той или иной степени? Да обязательно. Первый ход обычно по меньшему козырю делается. Если тасовать не совсем рандомом, то и это можно объяснить.
                  0
                  Приложение бесплатное, но у него есть разные дополнительные «плюшки», оформленные как InApp Purchases:

                  В оригинальном Microsoft Hearts такой гадости не было! С таким донатом никто играть не будет

                    0
                    В оригинальном Microsoft Hearts

                    Так это и не Hearts.
                    С таким донатом никто играть не будет

                    Вообще-то в публикации приведены скриншоты, которые говорят ровно об обратном ))
                    –1
                    Не смог скачать и установить приложение.

                    Формулировка от MS: «Похоже, у вас нет подходящих устройств, привязанных к учетной записи Майкрософт. Чтобы выполнить установку, войдите в систему на устройстве с помощью своей учетной записи Майкрософт.»

                    Видимо, из-за требований к ОС: "...Windows 10 версии 10049.0 или более поздней, Windows 8.1"

                    Тоска-печаль…
                      0
                      А учётка Microsoft есть? Зашли под ней? Если да, то обновляйте винду.
                      –1
                      Это отсылка к началу статьи: «Давным-давно, в Windows 95 была игра Microsoft Hearts. Игра в карты по сети, с оппонентами по всему миру. Если мне не изменяет память, то в Windows for Workgroups 3.11 (да, я застал все эти артефакты!) „

                      У меня артефакт — MS Windows 7, обновлять не хочу принципиально.

                      Просто поделился печалью.

                      Это же MS Windows Store, какие к вам-то претензии…

                        0
                        Не совсем уловил, в чём печаль? Обновлять же не хотите принципиально, а не потому, что кто-то не даёт. Или это очередной раунд бессмыслицы «вот раньше винда была лучше, не то, что сейчас»? Так публикация не об этом, в инете полно площадок, где можно излить свою грусть-печаль по этому поводу.
                          0
                          Есть старая сисадминская мудрость: «Работает — не трогай». У меня Win7 работает. Мне хватает её производительности. Зачем чинить то, что не ломалось? Новые плюшки? — блин, от старых бы избавиться…
                            0
                            То есть, таки-да: «очередной раунд бессмыслицы». Не пойму, зачем обсуждать «Win7 vs. Win10» или то, насколько лично вам достаточна Win7 в публикации про приложение, которое никак к Win7 не относится?
                            Разве я где-то в публикации «с горящим взором» агитирую переходить на Win10?
                            Нет, я просто не понимаю природу возникновения подобных «каментов».

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

                      Самое читаемое