[Что нам стоит покер-бот построить] Введение
[Что нам стоит покер-бот построить] Взгляд на проблему с высоты
Рассказываю о фундаменте серверного решения, основных типах, удобстве расширения и SOLID. О рефлексии, дженериках и прочей нечисти. Добро пожаловать в интересный мир enterprise.
Доброго времени суток, codeby.
Вся история программирования – это борьба со сложностью. Разработка софта – это не только решение конкретной задачи, это также вечное противостояния между сложностью программного кода, удобством расширения, оптимизацией и так далее. Это поиск компромиссов и эмпирических правил. Я приглашаю вас посмотреть, как это делал я.
Где начинается хороший софт? Со знания предметной области и хорошего технического задания. Наверное, сделаю статью по его написанию, сегодня же мы разбираем только код. Изучить предметную область вам не составит труда use google.
Но, допустим, вас бесполезно учить и вы не понимаете, зачем вам 20 страниц текста и картинок, когда «Итак всё понятно», поэтому я буду показывать написания кода по мере поступления мысли. Не стоит так делать, я серьёзно, это неадекватный подход. Что бы убедиться в этом отправлю вас к Макконелу, посмотрите, сколько стоит исправление ошибки на этапе составления тз и на этапе написания кода.
Давайте вспомним, что наш сервер должен делать. Он должен:
Я выделил вам главное.
Теперь у нас есть с чем работать, давайте начнём:
Решение. Обратимся к предметной области: что в покере является решением? Решение в покере это одно из игровых действий: сбросить карты, уравнять ставку, поднять ставку, чек. Соответственно нам нужно что-то, что однозначно кодировало бы данные действия, в языке C# есть такая вещь как Enum (перечисление)
Но это ведь ещё не всё. С Fold и Check всё понятно, а что делать с Bet(в данном случае, под Bet подразумевается: поднять ставку(raise) и просто поставить). Ведь размер ставки, так же определяется решением и логично что принимать решение о размере ставки должен сервер. Поэтому, наше перечисление ThisAction должно стать частью чего-то большего:
Думаю, тут всё ясно и без комментария.
Фух… не, дальше я не буду так всё разжёвывать.
Информация. Аналогично подумаем про информацию. Обратимся к предметной области. Проведём текстологический анализ информации из предметной области. (Давайте сами).
Получим что-то такое:
Где Player
Table
TableStage тоже Enum, описывающий стадии раздачи.
Уже на этом небольшом этапе мы плавно подошли к принципам хорошего проектирования.
С теорией пока закончим. Вернёмся к коду. Таким образом, у нас уже есть – входные и выходные данные. Давайте думать дальше. Мы с вами хорошо понимаем следующие: разные решение принимается на разных стадиях игры, более того такие решения вдобавок зависят и от типа игры. Абстрагируемся пока от того как эти решения мы будем принимать, давайте подумаем как сделать так чтоб нам было удобно разрабатывать. Мой бывший учитель матана всегда говорил: «Сделайте сегодня себя плохо, чтобы завтра вам было хорошо» умный был дядька.
Фабрика\фабричный метод.
Разные, разные, разные. Такая разность чаще всего приводит к реализациям фабрик. Что такое фабрика? Это очень популярный паттерн проектирования, знать про который обязан каждый. Реализаций этого подхода уйма. Я покажу интересный, основанный на рефлексии, дженериках и атрибутах.
Фабрикой я решаю следующую проблему. Понятно, что для каждой игры (Cash, MTT, SnG..) существуют разные тактики игры на разных стадиях раздачи (префлоп\флоп\тёрн\ривер). Задача такова: как можно максимально избежать дублирования кода, сохранив при этом расширяемость и сделать её максимально удобной, а также реализовать различные алгоритмы работы с входными данными по-разному.
It’s easy.
Смотрите. Мы знаем, что наша фабрика должна возвращать ResponseAction. Так же мы знаем, что на вход ей подаётся GameInfo. Давайте внимательно взглянем на входные параметры: в интерфейсе ITable есть свойство, которое указывает на перечисление TableStage. Следовательно задав switch по этому полю мы бы могли определить для него стратегию. Но давайте пойдём дальше, напишем стратегию игры на префлопе\флопе\тёрне\ривере, делегируя её отдельным классам.
Начнём с интерфейса: IStrategy
Собственно, мы описали вот это предложение:
Теперь сделаем его реализации для Cash-игр в виде:
Ну и собственно сам метод для получения экземпляра стратегии.
Метод GetStageStratagy возвращает экземпляр CashFlop\CashPreflop\CashTurn\CashRiver. Но как этот метод определяет какой из классов CashFlop\CashPreflop\CashTurn\CashRiver нужно вернуть?
Я уже говорил, что новый функционал должно быть удобно добавлять, так вот я решил использовать для разметки нужных мне классов такую конструкцию как атрибуты:
Теперь покажу как реализован класс CashPreflop
не обращайте внимание на возвращаемое значение – это «заглушка». Уловили логику? Мы помечаем класс, который реализует стратегию для определённой стадии раздачи, атрибутом этой стадии, и в конструкторе указываем: к какому типу игры относится данная стратегия. Удобно, не правда ли?
Ну а теперь нам надо «спарсить» этот атрибут. Делается это с помощью рефлексии.
Не волнуйтесь, она всегда выглядит, как магия, пока не начинаешь с ней работать. Очень хороший инструмент, позволяющий делать интересные вещи, но за это удобство приходится платить производительностью. Дальше я расскажу, как оптимизируют рефлексию, но в других статьях.
Итак, вроде со стадиями разобрались. Я упоминал вот это
Давайте разбираться! Я уже заявил о том, что хочу обеспечить себе возможность маштабировать своё решение. Поэтому очевидно, что я хочу добавлять разные типы игры. Я хочу сделать это максимально удобно. Давайте пока я вам скажу, что от типа игры меняется стратегия, полностью.
Следовательно, мы имеем следующий интерфейс, описывающий тип игры
Тогда для его реализаций мы получим:
Свойство Strategy имеет тип IStrategy и отвечает за стратегию. Получаем мы его как раз с помощью методов SayMeStrategy и GetStageStrategy реализованных в классе, полный код которого выглядит
Ну и собственно сама реализация класса Cash
Так как на верхнем (контроллеров и пользовательских интерфейсов) уровне мы должны указать для какого типа игры мы хотим реализовать контролер, мы реализуем слудующий дженерик класс. Выглядит он просто:
И опять немного маги
Тут мы вызываем конструктор одной из реализации IGameTypeStratage и передаём в качестве параметра информацию о столе.
Вообще-то мы с вами создали хорошую инфраструктуру, поэтому в дальнейшем мы всё это закроем (модификаторами доступа) для других сборок – предоставим чёрный ящик. А для пользователя библиотекой оставим следующий интерфейс:
Теперь где-нибудь на пользовательском уровне нам достаточно сделать следующее:
Всё. Больше пользовательскому уровню ничего знать не требуется. Это называется Инкапсуляцией, о ней позже.
Благодаря, нашим манипуляциям мы теперь можем
Легко расширять и добавлять новый функционал в бота: для этого нам достаточно реализовать интерфейс IGameTypeStratage и интерфейс IStrategy, реализации последнего пометить аттрибутами. Всё остальное сделает наша экосистема. На сегодня, думаю достаточно, переварите информацию её получилось много. Про другие принципы расскажу в статье про тз (опять же если наберётся 20 лайков, я не хочу просто так всё это разжевывать, поймите меня).
Напоследок хочу вот что сказать: научиться пользоваться готовыми решениями не сложно вообще-то. В хакерстве должна привлекать исследовательская часть: поиск новых уязвимостей, разработка эксплойтов, методов анонимизации и прочее. Мир enterpris’a жесток и изменчив, если вам достаточно пользоваться всем готовым, искать маны по дырам, пусть. Я вас не осуждаю, но мир разработки призывает постоянно думать, совершенствоваться, изобретать, в конце концов, поэтому он не для вас, дорогие скрипт-кидди. Я при всём желании не смог бы разжевать в этой статье все аспекты такого ремесла как программирования. Читайте, господа, гуглите, расширяйте кругозор. В статье есть много новых для вас слов, изучите их, вникните и возвращайтесь ко мне. Дальше мы займёмся не такими тривиальными вещами.
[Что нам стоит покер-бот построить] Взгляд на проблему с высоты
Рассказываю о фундаменте серверного решения, основных типах, удобстве расширения и SOLID. О рефлексии, дженериках и прочей нечисти. Добро пожаловать в интересный мир enterprise.
Доброго времени суток, codeby.
Вся история программирования – это борьба со сложностью. Разработка софта – это не только решение конкретной задачи, это также вечное противостояния между сложностью программного кода, удобством расширения, оптимизацией и так далее. Это поиск компромиссов и эмпирических правил. Я приглашаю вас посмотреть, как это делал я.
Где начинается хороший софт? Со знания предметной области и хорошего технического задания. Наверное, сделаю статью по его написанию, сегодня же мы разбираем только код. Изучить предметную область вам не составит труда use google.
Но, допустим, вас бесполезно учить и вы не понимаете, зачем вам 20 страниц текста и картинок, когда «Итак всё понятно», поэтому я буду показывать написания кода по мере поступления мысли. Не стоит так делать, я серьёзно, это неадекватный подход. Что бы убедиться в этом отправлю вас к Макконелу, посмотрите, сколько стоит исправление ошибки на этапе составления тз и на этапе написания кода.
Давайте вспомним, что наш сервер должен делать. Он должен:
А вот сервер, должен-таки, думать и принимать решения, основываясь на предоставленной информации. Кроме того, он должен сообщать о них клиенту.
Я выделил вам главное.
· Сущность – 2 штуки (Решение, информация)
· Действия – 3 штуки. (Принимать решение, думать (обрабатывать информацию), сообщать клиенту)
Только что мы вместе с вами воспользовались текстологическим анализом, это очевидное действие, весьма полезно в работе, особенно в тех случаях, когда надо с чего-то начать, а в голове ноль идей (относится к задачам любой сложности). Вот только анализировали мы мысль, а в нормальном подходе так анализируют тз.
· Действия – 3 штуки. (Принимать решение, думать (обрабатывать информацию), сообщать клиенту)
Только что мы вместе с вами воспользовались текстологическим анализом, это очевидное действие, весьма полезно в работе, особенно в тех случаях, когда надо с чего-то начать, а в голове ноль идей (относится к задачам любой сложности). Вот только анализировали мы мысль, а в нормальном подходе так анализируют тз.
Теперь у нас есть с чем работать, давайте начнём:
Решение. Обратимся к предметной области: что в покере является решением? Решение в покере это одно из игровых действий: сбросить карты, уравнять ставку, поднять ставку, чек. Соответственно нам нужно что-то, что однозначно кодировало бы данные действия, в языке C# есть такая вещь как Enum (перечисление)
Но это ведь ещё не всё. С Fold и Check всё понятно, а что делать с Bet(в данном случае, под Bet подразумевается: поднять ставку(raise) и просто поставить). Ведь размер ставки, так же определяется решением и логично что принимать решение о размере ставки должен сервер. Поэтому, наше перечисление ThisAction должно стать частью чего-то большего:
Думаю, тут всё ясно и без комментария.
Фух… не, дальше я не буду так всё разжёвывать.
Информация. Аналогично подумаем про информацию. Обратимся к предметной области. Проведём текстологический анализ информации из предметной области. (Давайте сами).
Получим что-то такое:
Где Player
Table
TableStage тоже Enum, описывающий стадии раздачи.
Уже на этом небольшом этапе мы плавно подошли к принципам хорошего проектирования.
Не используйте строки. Нигде. Серьёзно - по минимуму. Особенно в больших проектах (а наш бот будет большим проектом). Почему?
На самом деле за значениями в Enum скрываются цифры 0…n. Допустим, мы определили бы стадии игры через строку. В таком случае при разработки клиента мы не застрахованы от опечаток и в случае опечатки мы бы потратили очень много времени на поиск и устранении ошибки. Enum же мало того что исключает опечатки, благодаря IntelliSense показывает все возможные варианты, если мы вдруг забудем.
Благодаря Enum мы предотвращаем большинство ошибок на этапе компиляции, а это очень хорошо.
Короче, Enum – бро, String – не бро.
На самом деле за значениями в Enum скрываются цифры 0…n. Допустим, мы определили бы стадии игры через строку. В таком случае при разработки клиента мы не застрахованы от опечаток и в случае опечатки мы бы потратили очень много времени на поиск и устранении ошибки. Enum же мало того что исключает опечатки, благодаря IntelliSense показывает все возможные варианты, если мы вдруг забудем.
Благодаря Enum мы предотвращаем большинство ошибок на этапе компиляции, а это очень хорошо.
Короче, Enum – бро, String – не бро.
Думаю, многие из вас знакомы с понятием полиморфизм. Однако немногие умеют им пользоваться и плохо понимают его. Давайте обратимся к простому определению:
Полиморфизм – один интерфейс множество реализаций.
В прошлых статьях я много раз делал акцент на масштабируемости решения и для данной предметной области это очень важно. У нас есть множество вариантов реализации игрока и стола. Допустим, мы решили расширить функционал нашего бота, добавить ему возможность думать в MTT(многостоловые турниры). Ну и понятно, что стратегия игры для мтт будет отличаться от стратегии игры в Cash не только на стадиях флоп\тёрн\ривер, но и на этапах самого турнира (эти этапы характеризуются оставшимся количеством игроков) : бабл, призы(финал), начало турнира. Тогда для принятия решения нам нужно будет больше информации о столе, например о том какой это этап. В таком случае нам нужно будет реализовать отдельный стол. Так как у них будет общее поведение, мы просто реализуем интерфейс ITable и всё. Во всех наших старых классах мы можем работать с новым столом не напрягаясь (почти). Главное не забыть про один из принципов SOLID – Принцип подстановки Барбары Лисков (LSP) (хорошее объяснении которого можно прочесть на хабре (не реклама)), который по простому звучит так: Наследующий класс должен дополнять, а не замещать поведение базового класса Или Замена базового типа субтипом не должна отражаться на работе программы
Соответственно:
· Программирование для интерфейса, а не для реализации упрощает расширение ваших программ.
· При программировании для интерфейса ваш код будет работать со всеми субклассами этого интерфейса, даже с теми, которые ещё не созданы.
Интерфейсы и абстрактные классы – бро.
Полиморфизм – один интерфейс множество реализаций.
В прошлых статьях я много раз делал акцент на масштабируемости решения и для данной предметной области это очень важно. У нас есть множество вариантов реализации игрока и стола. Допустим, мы решили расширить функционал нашего бота, добавить ему возможность думать в MTT(многостоловые турниры). Ну и понятно, что стратегия игры для мтт будет отличаться от стратегии игры в Cash не только на стадиях флоп\тёрн\ривер, но и на этапах самого турнира (эти этапы характеризуются оставшимся количеством игроков) : бабл, призы(финал), начало турнира. Тогда для принятия решения нам нужно будет больше информации о столе, например о том какой это этап. В таком случае нам нужно будет реализовать отдельный стол. Так как у них будет общее поведение, мы просто реализуем интерфейс ITable и всё. Во всех наших старых классах мы можем работать с новым столом не напрягаясь (почти). Главное не забыть про один из принципов SOLID – Принцип подстановки Барбары Лисков (LSP) (хорошее объяснении которого можно прочесть на хабре (не реклама)), который по простому звучит так: Наследующий класс должен дополнять, а не замещать поведение базового класса Или Замена базового типа субтипом не должна отражаться на работе программы
Соответственно:
· Программирование для интерфейса, а не для реализации упрощает расширение ваших программ.
· При программировании для интерфейса ваш код будет работать со всеми субклассами этого интерфейса, даже с теми, которые ещё не созданы.
Интерфейсы и абстрактные классы – бро.
С теорией пока закончим. Вернёмся к коду. Таким образом, у нас уже есть – входные и выходные данные. Давайте думать дальше. Мы с вами хорошо понимаем следующие: разные решение принимается на разных стадиях игры, более того такие решения вдобавок зависят и от типа игры. Абстрагируемся пока от того как эти решения мы будем принимать, давайте подумаем как сделать так чтоб нам было удобно разрабатывать. Мой бывший учитель матана всегда говорил: «Сделайте сегодня себя плохо, чтобы завтра вам было хорошо» умный был дядька.
Фабрика\фабричный метод.
Разные, разные, разные. Такая разность чаще всего приводит к реализациям фабрик. Что такое фабрика? Это очень популярный паттерн проектирования, знать про который обязан каждый. Реализаций этого подхода уйма. Я покажу интересный, основанный на рефлексии, дженериках и атрибутах.
Фабрикой я решаю следующую проблему. Понятно, что для каждой игры (Cash, MTT, SnG..) существуют разные тактики игры на разных стадиях раздачи (префлоп\флоп\тёрн\ривер). Задача такова: как можно максимально избежать дублирования кода, сохранив при этом расширяемость и сделать её максимально удобной, а также реализовать различные алгоритмы работы с входными данными по-разному.
It’s easy.
Смотрите. Мы знаем, что наша фабрика должна возвращать ResponseAction. Так же мы знаем, что на вход ей подаётся GameInfo. Давайте внимательно взглянем на входные параметры: в интерфейсе ITable есть свойство, которое указывает на перечисление TableStage. Следовательно задав switch по этому полю мы бы могли определить для него стратегию. Но давайте пойдём дальше, напишем стратегию игры на префлопе\флопе\тёрне\ривере, делегируя её отдельным классам.
Начнём с интерфейса: IStrategy
Собственно, мы описали вот это предложение:
Мы знаем, что наша фабрика должна возвращать ResponseAction. Так же мы знаем, что на вход ей подаётся GameInfo.
Теперь сделаем его реализации для Cash-игр в виде:
Ну и собственно сам метод для получения экземпляра стратегии.
Метод GetStageStratagy возвращает экземпляр CashFlop\CashPreflop\CashTurn\CashRiver. Но как этот метод определяет какой из классов CashFlop\CashPreflop\CashTurn\CashRiver нужно вернуть?
Я уже говорил, что новый функционал должно быть удобно добавлять, так вот я решил использовать для разметки нужных мне классов такую конструкцию как атрибуты:
Теперь покажу как реализован класс CashPreflop
не обращайте внимание на возвращаемое значение – это «заглушка». Уловили логику? Мы помечаем класс, который реализует стратегию для определённой стадии раздачи, атрибутом этой стадии, и в конструкторе указываем: к какому типу игры относится данная стратегия. Удобно, не правда ли?
Ну а теперь нам надо «спарсить» этот атрибут. Делается это с помощью рефлексии.
Не волнуйтесь, она всегда выглядит, как магия, пока не начинаешь с ней работать. Очень хороший инструмент, позволяющий делать интересные вещи, но за это удобство приходится платить производительностью. Дальше я расскажу, как оптимизируют рефлексию, но в других статьях.
Итак, вроде со стадиями разобрались. Я упоминал вот это
в конструкторе указываем, к какому типу игры относится данная стратегия.
Давайте разбираться! Я уже заявил о том, что хочу обеспечить себе возможность маштабировать своё решение. Поэтому очевидно, что я хочу добавлять разные типы игры. Я хочу сделать это максимально удобно. Давайте пока я вам скажу, что от типа игры меняется стратегия, полностью.
Следовательно, мы имеем следующий интерфейс, описывающий тип игры
Тогда для его реализаций мы получим:
Свойство Strategy имеет тип IStrategy и отвечает за стратегию. Получаем мы его как раз с помощью методов SayMeStrategy и GetStageStrategy реализованных в классе, полный код которого выглядит
Ну и собственно сама реализация класса Cash
Так как на верхнем (контроллеров и пользовательских интерфейсов) уровне мы должны указать для какого типа игры мы хотим реализовать контролер, мы реализуем слудующий дженерик класс. Выглядит он просто:
И опять немного маги
Тут мы вызываем конструктор одной из реализации IGameTypeStratage и передаём в качестве параметра информацию о столе.
Вообще-то мы с вами создали хорошую инфраструктуру, поэтому в дальнейшем мы всё это закроем (модификаторами доступа) для других сборок – предоставим чёрный ящик. А для пользователя библиотекой оставим следующий интерфейс:
Теперь где-нибудь на пользовательском уровне нам достаточно сделать следующее:
Всё. Больше пользовательскому уровню ничего знать не требуется. Это называется Инкапсуляцией, о ней позже.
Благодаря, нашим манипуляциям мы теперь можем
Легко расширять и добавлять новый функционал в бота: для этого нам достаточно реализовать интерфейс IGameTypeStratage и интерфейс IStrategy, реализации последнего пометить аттрибутами. Всё остальное сделает наша экосистема. На сегодня, думаю достаточно, переварите информацию её получилось много. Про другие принципы расскажу в статье про тз (опять же если наберётся 20 лайков, я не хочу просто так всё это разжевывать, поймите меня).
Напоследок хочу вот что сказать: научиться пользоваться готовыми решениями не сложно вообще-то. В хакерстве должна привлекать исследовательская часть: поиск новых уязвимостей, разработка эксплойтов, методов анонимизации и прочее. Мир enterpris’a жесток и изменчив, если вам достаточно пользоваться всем готовым, искать маны по дырам, пусть. Я вас не осуждаю, но мир разработки призывает постоянно думать, совершенствоваться, изобретать, в конце концов, поэтому он не для вас, дорогие скрипт-кидди. Я при всём желании не смог бы разжевать в этой статье все аспекты такого ремесла как программирования. Читайте, господа, гуглите, расширяйте кругозор. В статье есть много новых для вас слов, изучите их, вникните и возвращайтесь ко мне. Дальше мы займёмся не такими тривиальными вещами.