[#6] Пишем игру Backjack (Очко). Ограничиваем пользователя и заканчиваем раунд итоги подводим

Выполняемый код на jsfiddle. Код в конце статьи. Удобно иметь его перед глазами, читая статью. Поехали.

В прошлом эпизоде мы реализовал конец хода игрока и ход компьютера. А вот пользователю не запретили ходить во время хода компьютера. Пора исправлять.

Пользователю доступно 2 действия: взятие карты и завершение своего хода. И игра будет некорректной если разрешить пользователю производить эти действия в момент хода компьютера. Что значит запретить пользователю выполнять действия? Можно, как предположил @NONLUCIFER убирать обработчики и ставить их заново. Идея рабочая и реализуемая, хоть нам и понадобится решить часть дополнительных задач. Нужно будет где-то хранить ссылки на обработчики а так же убедиться что в обработчиках корректный this.

В js как и в реальном мире, в отличии от школьной комнаты, бывает несколько ответов на один и тот же вопрос (а бывает ни одного, лол). Того же эффекта отмены можно добиться иным подходом. Мы просто заведем новое свойство this.playersTurn которое будет кодировать чей ход в данный момент: если значение true то ход игрока иначе ход компьютера. В обработчике же кликов по колоде игнорируем логику обработки если ход не пользователя.

Эту технику называет “early return” или ранний выход. При ряде обстоятельств функция не должна выполнять код, эти обстоятельства прописывают вверху определения функции. Например обстоятельствами может быть валидация входных данных или как в нашем случе сверка с состоянием, хранимым где-то в другом месте.

Выведем состояние свойства this.playersTurn в DOM (тут можно разойтись и развесить красивые классы на блоки с картами игрока и компьютера итд), например, дав кнопке пасса состояние disabled. Обрати внимание что функция просто “проецирует” состояние каким бы оно ни было.

Осталось подвести итоги раунда. Метод endRound существует и просит быть наполненным содержанием. Это подведение и отображение итогов и подготовка состояний для нового раунда (колода, руки игроков. передача хода итд).

Подведение итогов. Проходим уже давно знакомой тропой. Что значит счет раундов? Это просто чисто сколько раз на конец раунда выигрывает одна или вторая сущность. Заводим свойства которые моделирует счет человека и счет компьютера, описываем как они отображаются в DOM и обновляем-рисуем значения при завершении раунда.

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

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

Добавляем раннее завершение хода если игрок набрал 21.

На данном этапе игра становится играбельной. Не хватает некоторого промежуточного состояния когда раунд закончился и можно сверить свою руку с рукой компьютера. Думаю нужно ввести нечто типа кнопки “начать новый раунд” и делать ее доступной по завершению раунда. Или можно обойтись блокирующими интерфейс диалогами типа confirm и alert. Что скажешь, @NONLUCIFER , как решим задачу просмотра результатов когда раунд закончился?

// Основной класс который управляет игрой а так же отображением состояния игры в DOM
class BlackJackGame {
	get playerHandWeight() {
		return this._getWeight(this.playersCards)
	}

	get computerHandWeigh() {
		return this._getWeight(this.computerCards)
	}

	constructor(rootNode) {

		// Ниже значения для демонстрации какие свойства будут у объекта игры.
		// Значения будут манипулироваться объектом игры в процессе игры
		this.playersCards = null
		this.computerCards = null
		this.deck = null
		this.playersTurn = null
		this.playerWings = null
		this.computerWings = 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.playersTurn = true
		this.playerWings = 0
		this.computerWings = 0

		// Разметку с которой будет иметь дело игра лучше создавать самой игре
		// На начальных этапах фокусируемся на контейнерах - узлах в которые мы будем отображать аспекты игры.
		// На конечных этапах мы просто перенесем эти узлы в другие узлы или добавим оберток чтобы создать желаемую картинку. Код же самой игры менять не будет потому что мы отделим логику отображения от логики самой игры. 
		// Стремимся называть классы единообразно, сообразно аспекту за который отвечает узел. Классы нужны для того чтобы уникальным образом обратиться к элементу. Например блок карт бота и блок карт человека должны иметь разные классы (можно сделать общий класс чтобы поместить в него общие стили, но логику будем строить на уникальных классах).
		// Не критично, но важно стремиться использовать один и тот же набор категорий. Однообразно называть пользователя игры "игрок" а не "ползователь" в одном месте и "игрок" в другом. Но фишка в том что этого не всегда можно достичь даже по объективным причинам. Ну то отдельная тема, пока стремимся опираться на один словарь терминов.
		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('.pass')

		this.DOMDeckOfCards.addEventListener('click', () => {
			if (!this.playersTurn) {
				return
			}
			this.pickCard()
		})

		this.DOMPass.addEventListener('click', () => {
			if (!this.playersTurn) {
				return
			}
			this.pass()
		})

		this.renderScore()
	}

	pickCard() {
		const card = this.deck.shift()
		this.playersCards.push(card)
		this.renderPlayerHand()
		if (this.playerHandWeight === 21) {
			this.endRound()
		}
	}

	renderPlayerHand() {
		this.DOMPlayerCards.innerHTML = `
			${ JSON.stringify(this.playersCards)}
		`
	}

	computerTurn() {
		const card = this.deck.shift()
		this.computerCards.push(card)
		this.renderComputerHand()
		if (this.computerHandWeigh < 18) {
			setTimeout(() => {
				this.computerTurn()
			}, 500)
		} else {
			this.endRound()
		}
	}

	renderComputerHand() {
		this.DOMComputerCards.innerHTML = `
			${ JSON.stringify(this.computerCards) }
		`
	}

	pass() {
		this.playersTurn = true
		this.renderTurn()
		this.computerTurn()
	}
	
	renderTurn() {
		this.DOMPass.disabled = !this.playersTurn
	}

	_getWeight(hand) {
		return hand.reduce((sum, card) => {
			return sum + card.weight
		}, 0)
	}

	endRound() {
		console.log(this.playerHandWeight, this.computerHandWeigh)
		// перебора нет
		if (this.playerHandWeight <= 21 && this.computerHandWeigh <= 21) {
			if (this.playerHandWeight > this.computerHandWeigh) {
				this.playerWings += 1
			} else if (this.computerHandWeigh > this.playerHandWeight) {
				this.computerWings += 1
			}
			// Иначе ничья. Очков никому
		// Eсть перебор
		// Перебор человека
		} else if (this.playerHandWeight > 21 && this.computerHandWeigh <= 21) {
			this.computerWings += 1
				// Перебор компьютера
		} else if (this.computerHandWeigh > 21 && this.playerHandWeight <= 21) {
			this.playerWings += 1
		} else {
			// Перебор обоих. Очков никому
		}
		this.renderScore()
		this.computerCards = []
		this.playersCards = []
		this.deck = this._createDeck()
		this.playersTurn = true
		this.renderComputerHand()
		this.renderPlayerHand()
		this.renderTurn()
	}

	renderScore() {
		this.DOMComputerScore.innerHTML = this.computerWings
		this.DOMPlayerScore.innerHTML = this.playerWings
	}

	// Обычно создание и перемешивание колоды разделяется. Но ради простоты конечного кода я совмещаю эти процессы.
	_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'))

Можно ли сделать окно у которого будет свойство display:none; и когда функция endround сработает то выведется окно в котором будет показан счёт за раунд игрока и компьютера и пока пользователь не нажмёт продолжить то функция endround не закончится.

Именно “остановить” функцию не получится. Нужно моделировать игру так чтобы во время показа окна игра не продолжалась. Вот такой получился результат и процесс: [#7] Пишем игру Backjack (Очко). Даем игроку опомнится в конце раунда.

Осталось только навести красоту и цель написания игры можно считать достигнутой.