[#3] Пишем игру Backjack (Очко). Моделирование структур данных

На предыдущих этапах мы узнали какие аспекты мы моделируем а так же получили картинку конечного результата.

Следующий этап - моделирование объектов и структур данных.

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

В ООП парадигме моделируемые аспекты описываются с помощью классов. В функциональной парадигме с помощью структур данных (массивы-объекты). Мы будем прагматичны и будет использовать объекты для задания “формы” программы, а структуры данных для элементов.

Вся игра будет инкапсулирована в одном объекте который отвечает за поддержание состояния игры (колода, рука игрока итд) а так же за отображение этого состояния (отображение цифр в DOM, смена классов-узлов чтобы отрисовать карты). Объект требует на вход только одного: узел в который он будет отображать состояние игры. Все остальное - создание разметки, навешивание обработчиков событий - будет инкапсулировано в объекте игры. Внутри объекта игры мы моделируем аспекты типа: колода, рука игрока, рука компьютера. Возможно в процессе написания игры понадобится что-то изменить. Кстати, в ООП парадигме нужно было бы создавать класс для колоды и класс для карты. Но мы обойдемся объектами ради компактности конечного решения.

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

Появляются варианты выбора, значит нужно сравнивать последствия выбора одного или другого формата описания карты. Я всегда начинаю с минимально необходимого объекта для описания сущности. Это масть и достоинство. И начинаю представлять как моделируются аспекты задачи через предполагаемый объект. Что будет означать “колода карт”? Массив объектов с мастью и достоинством. Что значит “перемешать колоду карт” значит случайно расположить объекты в массиве. Что значит “взять” карту? Значит переместить объект из массива карт в массив “рука”. А что значит посчитать вес руки? Придется вычислять вес исходя из некоторой таблицы. И этот шаг можно опустить, добавив к карте еще один аттрибут: ее вес. Для случая когда результат зависит от комбинации карт а не от ее веса, такой трюк-оптимизация бы не сработал.

Колоду проще всего взять и описать руками (ну или “помочь” алгоритмом). Я бы вставил колоду в код как массив объектов и “клонировал” бы его каждый раз когда нужна колода карт.

На сегодня все. Продолжение следует завтра. Ниже код с некоторыми комментариями. Когда код будет запускаем я начну делиться им через jsfiddle.

// Основной класс который управляет игрой а так же отображением состояния игры в DOM
class BlackJackGame {
	// Ниже значения для демонстрации какие свойства будут у объекта игры.
	// Значения будут манипулироваться объектом игры в процессе игры
	playersCards
	computerCards
	deck
	
	// Узлы в которые будет происходит отрисовка состояния игры. Удобно когда узлы существуют в течение всей игры. Тогда не придетс заморачиваться с поддержанием состосния (навешивание-убирание обработчиков событий)
	// Иногда для удобства отличия узлов от данных придумывают формат переменных, например, начинающиеся с $ это DOM узлы. Или дать всем префикс DOM. Тогда при чтении кода будет однозначно понятно с чем имеем дело
	DOMComputerScore
	DOMComputerCards
	DOMDeckOfCards
	DOMPlayerScore
	DOMPlayerCards
	DOMPass
	
	// Корневой элемет
	DOMRootNode
	
	constructor(rootNode) {
		this.DOMRootNode = rootNode
		this.init()
	}
	
	// Функция-привязка ко внешнему миру, куда игра будет себя проэцировать.
	// Запускается разово, генерирует всю нужную разметку в переданом узле, создает начальное сотояние игры
	init() {
		this.playersCards = []
		this.computerCards = []
		this.deck = this._createDeck()
		
		// Разметку с которой будет иметь дело игра лучше создавать самой игре
		// На начальных этапах фокусируемся на контейнерах - узлах в которые мы будем отображать аспекты игры.
		// На конечных этапах мы просто перенесем эти узлы в другие узлы или добавим оберток чтобы создать желаемую картинку. Код же самой игры менять не будет потому что мы отделим логику отображения от логики самой игры. 
		// Стремимся называть классы единообразно, сообразно аспекту за который отвечает узел. Классы нужны для того чтобы уникальным образом обратиться к элементу. Например блок карт бота и блок карт человека должны иметь разные классы (можно сделать общий класс чтобы поместить в него общие стили, но логику будем строить на уникальных классах).
		// Не критично, но важно стремиться использовать один и тот же набор категорий. Однообразно называть пользователя игры "игрок" а не "ползователь" в одном месте и "игрок" в другом. Но фишка в том что этого не всегда можно достичь даже по объективным причинам. Ну то отдельная тема, пока стремимся опираться на один словарь терминов.
		this.rootNode.innerHtml = `
			<div class="computer-score"></div>
			<div class="computer-cards"></div>
			<div class="deck-of-cards"></div>
			<div class="player-score"></div>
			<div class="player-cards"></div>
			<button class="pass">Пасс</button>
		`
		
		// У некоторых, чаще начинающих но уже понапрограммировавших, или некритичных последователей принципов DRY могут чесаться руки написать функцию которая сгенерирует те же действия из структуры типа массива имен классов. Не надо. Код ниже куда проще и понятен всем вне зависимости от уровня. Плюс меняется куда проще если формат именования нужно будет сменить
		this.DOMComputerScore = this.DOMRootNode.querySelector('.computer-score')
		this.DOMComputerCards = this.DOMRootNode.querySelector('.computer-cards')
		this.DOMDeckOfCards = this.DOMRootNode.querySelector('.deck-of-cards')
		this.DOMPlayerScore = this.DOMRootNode.querySelector('.player-score')
		this.DOMPlayerCards = this.DOMRootNode.querySelector('.player-cards')
		this.DOMPass = this.DOMRootNode.querySelector('.DOMPass')
	}
	
	// Обычно создание и перемешивание колоды разделяется. Но ради простоты конечного кода я совмещаю эти процессы.
	_createDeck() {
		// клонируем из объекта или генерируем
		// результат перемешиваем случайно
	}
}