[#4] Пишем игру Backjack (Очко). Колода и ход игрока

Cсылка на запускаемый результат, результирующий код в конце статьи. Думаю что будет лучше если вы будете читать текст и параллельно смотреть какой код ему соответствует.

При моделировании структур данных мы решили что карта описывается объектом трех свойств: масть, достоинство, вес.

{
	suit: 'hearts'
	name: 'tuz',
	weight: 11
}

Интересен аспект именования. Можно переводить названия карт на английский, но я напишу транслитерацией, оставив названия мастей по английски просто так. При этом для десяток-шестерок название должно оставаться строкой для однородности типов данных.

Перемешивание карт моделируется перемешиванием объектов в массиве. Существует простейший способ перемешать объекты случайным образом в массиве.

arrayToShuffle.sort(() => {
	return Math.random() > 0.5 ? 1 : -1
})

Но у этого алгоритма есть нюанс: рандомность перемешивания нерандомна и зависит от начального расположения объектов. Не будем заморачиваться пока что с математически корректным перемешиванием колоды и соберем наш первый метод создания колоды.

Колоду оказалось проще сгенерировать из пары массивов чем описывать ее вручную.

Генерация колоды:

const suits = ['hearts', 'clubs', 'diamonds', 'spades']
const cards = [{name: 'tuz', weight: }, {name: 'korol', weight: }, {name: 'dama', weight: }, {name: 'valet', weight: }, {name: '10', weight: }, {name: '9', weight: }, {name: '8', weight: }, {name: '7', weight: }, {name: '6', weight: 6}]
const deck = []
for (let suit of suits) {
	for (let cardProto of cards) {
		deck.push({
			suit: suit,
			name: cardProto.name,
			weight: cardProto.weight
		})
	}
}
deck.sort(() => {
	return Math.random() > 0.5 ? 1 : -1
})

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

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

Не все аспекты изолируемы кодом. А так же не все что можно изолировать нужно изолировать. Но попробовать стоит чтобы понять последствия своих решений. Именно так мы и учимся. Через предположение что будет при решении X на основании Y (известных данных) + Z (субъективный опыт). Реализации решения, сравнение ожиданий с действительностью, и изменение или Z (лучшее понимание как устроены аспекты) или через изменение Z (понятно что так повторять не надо).

Итак ход игрока. Игроку доступны 2 действия - взять карту и окончание хода. Окончание хода пока опустим. Что значит взять карту? В терминах выбранных нами структур данных это перенос первого объекта карты из массива колоды в массив руки игрока.

А что же с обработчиками событий. Когда у нас есть функция “взять карту”, то все равно когда и по какому обработчику она будет вызвана. А что же делать с отображением? Эти вопросы, возможно для некоторых, неожиданно вторичны по отношению к моделируемому аспекту, а главное они могут быть реализованы независимо от моделируемого аспекта. Одна из крутейший вещей которые вы можете для себя сделать это понять что данные первичны а их отображение вторично.

Отображение хода игрока. Все же что значит отобразить карты игрока? Поместить нужную разметку в DOM. Конкретная разметка нас не интересует, потому что вопрос “изолирован” от программы. Просто отобразим данные, описывающие руку игрока, превратив в строку объекты.

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

На сегодня все. Продолжение следует.

На этом этапе имеет смысл размещать код на jsfiddle так как есть на что посмотреть и что потрогать. Edit fiddle - JSFiddle - Code Playground. Клик по колоде будет отображать данные руки пользователя.

// Основной класс который управляет игрой а так же отображением состояния игры в DOM
class BlackJackGame {
	constructor(rootNode) {
		// Ниже значения для демонстрации какие свойства будут у объекта игры.
		// Значения будут манипулироваться объектом игры в процессе игры
		this.playersCards = null
		this.computerCards = null
		this.deck = null

		// Узлы в которые будет происходит отрисовка состояния игры. Удобно когда узлы существуют в течение всей игры. Тогда не придетс заморачиваться с поддержанием состосния (навешивание-убирание обработчиков событий)
		// Иногда для удобства отличия узлов от данных придумывают формат переменных, например, начинающиеся с $ это DOM узлы. Или дать всем префикс DOM. Тогда при чтении кода будет однозначно понятно с чем имеем дело
		this.DOMComputerScore = null
		this.DOMComputerCards = null
		this.DOMDeckOfCards = null
		this.DOMPlayerScore = null
		this.DOMPlayerCards = null
		this.DOMPass = null

		// Корневой элемет
		this.DOMRootNode = rootNode
		this.init()
	}
	
	// Функция-привязка ко внешнему миру, куда игра будет себя проэцировать.
	// Запускается разово, генерирует всю нужную разметку в переданом узле, создает начальное сотояние игры
	init() {
		this.playersCards = []
		this.computerCards = []
		this.deck = this._createDeck()
		
		// Разметку с которой будет иметь дело игра лучше создавать самой игре
		// На начальных этапах фокусируемся на контейнерах - узлах в которые мы будем отображать аспекты игры.
		// На конечных этапах мы просто перенесем эти узлы в другие узлы или добавим оберток чтобы создать желаемую картинку. Код же самой игры менять не будет потому что мы отделим логику отображения от логики самой игры. 
		// Стремимся называть классы единообразно, сообразно аспекту за который отвечает узел. Классы нужны для того чтобы уникальным образом обратиться к элементу. Например блок карт бота и блок карт человека должны иметь разные классы (можно сделать общий класс чтобы поместить в него общие стили, но логику будем строить на уникальных классах).
		// Не критично, но важно стремиться использовать один и тот же набор категорий. Однообразно называть пользователя игры "игрок" а не "ползователь" в одном месте и "игрок" в другом. Но фишка в том что этого не всегда можно достичь даже по объективным причинам. Ну то отдельная тема, пока стремимся опираться на один словарь терминов.
		this.DOMRootNode.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')
		
		this.DOMDeckOfCards.addEventListener('click', () => {
			this.pickCard()
		})
	}
	
	pickCard() {
		const card = this.deck.shift()
		this.playersCards.push(card)
		this.renderPlayerHand()
	}

	renderPlayerHand() {
		this.DOMPlayerCards.innerHTML = `
			${ JSON.stringify(this.playersCards)}
		`
	}
	
	// Обычно создание и перемешивание колоды разделяется. Но ради простоты конечного кода я совмещаю эти процессы.
	_createDeck() {
		const suits = ['hearts', 'clubs', 'diamonds', 'spades']
		const cards = [{
			name: 'tuz',
			weight: 11
		}, {
			name: 'korol',
			weight: 4
		}, {
			name: 'dama',
			weight: 3
		}, {
			name: 'valet',
			weight: 2
		}, {
			name: '10',
			weight: 10
		}, {
			name: '9',
			weight: 9
		}, {
			name: '8',
			weight: 8
		}, {
			name: '7',
			weight: 7
		}, {
			name: '6',
			weight: 6
		}]
		const deck = []
		for (let suit of suits) {
			for (let cardProto of cards) {
				deck.push({
					suit: suit,
					name: cardProto.name,
					weight: cardProto.weight
				})
			}
		}
		deck.sort(() => {
			return Math.random() > 0.5 ? 1 : -1
		})

		return deck
	}
}

new BlackJackGame(document.querySelector('.game'))