[Из песочницы] Игра для самых маленьких — простая идея, которую не стыдно включить в резюме

Предыстория


Мой сын, как, наверное, все дети программистов, получил свою первую клавиатуру ещё когда не умел сидеть. Сейчас ему чуть меньше года, но он уже понимает разницу между «игрушечной» и «настоящей» (папиной) клавиатурой — если колотить по кнопкам настоящей, то на экране меняется картинка, а компьютер иногда издаёт какие-то звуки.

КДПВ

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

Чтобы процесс освоения компьютера стал для детёныша более увлекательным, я решил написать ему простенькую игру. Будучи программистом со стажем, весь процесс решено было построить «правильно».

Требования


Заказчик (мой сын, возраст помочь писать самому.

Функциональные:

  • Приложение работает в режиме полного экрана.
  • Можно нажимать на всё подряд, но самые доступные методы выхода или переключения программ должны быть заблокированы.
  • Визуальная обратная связь — цвет фона меняется при нажатии, в центре экрана отображается нажатый символ.
  • Звуковая обратная связь — приложение издаёт звук при нажатии на клавишу.
  • Предсказуемое поведение — цвет фона, символ и звук должны быть всегда одинаковыми для одной и той же клавиши.


Не функциональные:

  • Мне должно быть не стыдно за написанный код.
  • Код должен быть ценен сам по себе.
  • Архитектура и все решения должны быть «правильными» — как в заказном проекте.


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

В качестве языка программирования и среды разработки были выбраны C# и Visual Studio, так как они обеспечивали исполнителю наибольшую скорость работы.

Реализация


Из одного из старых проектов был извлечен код для создания приложения, развернутого на весь экран:

    FormBorderStyle = FormBorderStyle.None;
    WindowState = FormWindowState.Maximized;
    var screen = Screen.PrimaryScreen;
    Bounds = screen.Bounds;


Далее в дебрях интернета была найдена библиотека MouseKeyHook, с примерами, как заблокировать кнопку Windows. Аналогично примерам были заблокированы Alt-Tab и Ctrl-Esc. Теперь выйти из приложения можно только по Alt-F4.

Далее был написан код, который инициализирует рандомный цвет фона для нажатой клавиши:

  • Использовался new Random (seed), чтобы при каждом запуске рандом выдавал одни и те же значения.
  • Чтобы цвета были более-менее осмысленными, рандом выбирал значение из перечисления KnownColor, которое затем преобразовывалось в Color и присваивалось Form.BackColor.
  • Поддерживались буквенные символы и цифры.
  • Символ выводился «как есть» — клавиша Q могла вывести «Q», «q», «Й», «й», в зависимости от активного языка ввода и состояния CapsLock.


Первые альфа-тесты на себе выявили следующие недостатки реализации:

  • Form.BackColor категорически не согласен принимать цвет Transparent.
  • Чёрный цвет принимается, но символа на нём не видно.
  • Есть ряд клавиш, которые могут быть нажаты, у них есть символ, но они не обрабатываются программой или не отображают символ — Enter, Tab, Space, блок цифр над буквами и блок цифровых клавиш справа на клавиатуре.
  • Очень не нравился код обработки KeyDown/KeyPress — нужно было выделять диапазоны символов 'A-Z' и '0–9', пробел, Enter. Много не очень внятных блоков условий и сложный код расчёта размера массива рандомных цветов и выборки цвета из него.


Во второй итерации были внесены следующие изменения:

  • Написана простенькая WinForm утилита, которая точно так же «слушает» нажатия, сохраняет их в словарь Клавиша-Символ. Это позволило разрешить проблему вывода русских/английских букв.
  • У утилиты есть кнопка сохранения словаря в файл.
  • Поскольку клавиши Space и Enter в этом случае вызывали срабатывание обработчика кнопки, а Tab вызывал переход на кнопку, даже если она не выбрана, пришлось эти случаи отдельно обработать — установить TabStop=false для кнопок и вставить ActiveControl = null везде, где только можно.
  • Утилита помогла выявить все значимые клавиши — она запоминала клавишу при KeyDown, но добавляла её в словарь только по KeyPress, соответственно, всё, что не имеет символьного представления (Alt. Shift, Ctrl, Windows, функциональные клавиши) игнорировалось.
  • Обработку клавиши в самой игре можно будет значительно упростить до поиска по словарю.
  • Формат файла был самый простой — готовые наборы разделяются переводом строки, а поля (Клавиша-Символ-Цвет) в наборе разделяются символом \0 (пробел, табуляцию, и символы вроде запятой использовать не получилось, так как они могли быть элементом набора)
  • После сохранения невидимые символы вручную были заменены на Unicode-символы, отсутствующие на клавиатуре.
  • Цвет подбирался не случайным образом, а брался последовательно из enum KnownColor, начиная со следующего после KnownColor.Black (KnownColor.Transparent идёт немного раньше).


Альфа-тестирование на себе прошло вполне успешно и была проведена демонстрация заказчику.

Заказчик проявил интерес к продукту, выделил целых 2 минуты на тестирование, оценил работу в целом положительно и указал на следующие недостатки:

  • Недостаточная звуковая обратная связь (звук издает только клавиша PrintScreen).
  • Некорректно обрабатывается маленькая светящаяся кнопочка в правом дальнем углу ноутбука (экран гаснет).


Воодушевившись поддержкой заказчика, команда автор провел ретроспективу и сделал следующие выводы:

  • Нужно использовать внешнюю клавиатуру без кнопок управления питанием или маскировать аппаратную кнопку рукой.
  • Пора переходить к звуковой обратной связи.


Для звуковой обратной связи принято решение издавать звуки, соответствующие нотам (клавишам пианино). Быстрый поиск в интернете позволил найти формулу расчета частоты звука для каждой клавиши и данная формула была оперативно реализована в C# коде. Для непосредственного вывода звука на колонки использован Console.Beep (а что, работает же!).
Первый же прогон продемонстрировал недостатки:

  • Автор невнимательно прочитал MSDN, а именно строку «ranging from 37 to 32767 hertz».
  • Низкие звуки примерно до 110 Гц звучат отвратительно и их нельзя показывать заказчику.
  • Длительность звука 300 мс — слишком долго.
  • Звук выводится синхронно и вызывает задержку прорисовки фона.


По результатам были внесены следующие изменения:

  • Формировать частоты от 110Гц (25-я клавиша пианино, A2).
  • Длительность звука сделать 100 мс.
  • Выводить звук в отдельном потоке.
  • Команда выразила подозрение, что нужно делать Lock во втором потоке на время выполнения Console.Beep. В дальнейшем подозрение не подтвердилось, но удалять было лень блокировка осталась для дидактических целей.
  • Использовать двойной буфер при смене цвета, чтобы не было полос на экране при быстром нажатии на клавиши.


Данная версия получила высокую оценку самой команды, а поскольку до демо для заказчика оставалось время, команда решила провести рефакторинг:

  • Реализовать паттерн MVC, выделить логику игры в контроллер, во View оставить только код специфичный для работы с формой (переход в полный экран, обработчики событий).
  • Покрыть контроллер юнит-тестами
  • Вынести файл-словарь с тройками «Клавиша-Символ-Цвет» в ресурсы и реализовать русскую и английскую версии.
  • Поскольку на рабочем ноуте (а на нём мы планировали провести демо) у меня стоит локаль английская, было реализована настройка локали через конфиг. При этом в конфиге добавлена своя секция и реализован простенький файл для доступа к этой секции, возвращающий типизированные значения переменных конфига.


Итог


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

Вот список того, что можно изучить по поду игрушки:

  • Работа с WinForms (полный экран, двойной буффер, обработка событий клавиатуры)
  • Работа с локализованными ресурсами.
  • Применение паттерна MVC для WinForms (да, да вовсе не обязательно для этого переходить на WCF).
  • Применение паттерна Singletone (многопоточного).
  • Работа с Moq при разработке юнит-тестов.
  • Работа с Shouldly при разработке юнит-тестов.
  • Парсинг строк/файлов.
  • Многопоточность и блокировка потоков.
  • Работа с конфиг-файлом и создание своих секций.
  • Правильный кодинг-стайл и использование комментариев и регионов.
  • Работа с отладочной консолью (логгирование событий).
  • Перечисление значений enum при помощи Enum.GetValues.
  • Работа со статическими методами Array (Copy, IndexOf).
  • Работа с unmanaged-объектами (using).
  • «Отзывчивая» работа формы — подтверждение выхода, использование диалога сохранения файла.
  • Работа с NuGet и выкачивание пакетов при сборке.


Готовый код выложен в виде открытого репозитория на GitHub и доступен с лицензией MIT.

p.s. КДПВ © kobyakov

© Habrahabr.ru