Запускаемый код на JSFiddle. Полный код в конце статьи.
В предыдущей итерации над решением стало понятно что для удобства игры необходим промежуток времени когда игрок сравнит результат и осознает его. Необходимость паузы можно понять самому, поиграв несколько раундов в предыдущую версию. После завершения раунда у игрока нет возможности посчитать свои карты и карты компьютера.
Нам нужно дать время пользователю чтобы понять чем закончился раунд. Для этого нужна ситуация когда отображены счет, руки компьютера и игрока и видно сообщение. По действию пользователя запускается новый раунд. Если присмотреться к существующему методу endRound
, то видно что в нем смешаны аспекты завершения раунда и начала нового. Разнесем эти аспекты в отдельные методы.
endRound() {
// перебора нет
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.showMessage(this.getMessage())
this.renderScore()
this.renderComputerHand()
this.renderPlayerHand()
this.renderTurn()
}
startRound() {
this.computerCards = []
this.playersCards = []
this.deck = this._createDeck()
this.playersTurn = true
this.renderScore()
this.renderComputerHand()
this.renderPlayerHand()
this.renderTurn()
this.clearMessage()
}
На этом этапе я запутался какие из renderX
нужно вызывать и хочется просто вызывать одну функицю которая отрендерит все что нужно. Пока просто дублирую вызовы рендеров.
Начало раунда означает сброс рук, затусовывание колоды и сброс сообщения. Кстати, сброс сообщения означает убрать содержимое узла с текстом и скрытие кнопки. Отобразить сообщение означает вставить текст в DOM и сделать кнопку видимой.
clearMessage() {
this.DOMMessage.innerHTML = ''
this.DOMMessageAction.style.display = 'none'
}
showMessage(text) {
this.DOMMessage.innerHTML = text
this.DOMMessageAction.style.display = 'block'
}
Так как мы будем показывать сообщение только в конце раунда, то достаточно иметь только один обработчик события на кнопке. Нет необходимости жонглировать жонглировать тем какое действие нужно выполнить при нажатии на кнопку. У некоторых могут чесаться руки написать абстракцию “на всякий случай” чтобы можно было задавать любой обработчик при нажатии на кнопке в диалоге. Но мы не будем. Малый и прямолинейный код проще понять и проще изменить когда появится необходимость.
Интересный вопрос как смоделировать вывод сообщения об окончании раунда. Руки чешутся вставить логику выбора строки для сообщения в места где идет подсчет счета. Но чаще удобно “разносить” аспекты. Счет считаем в одном месте, а текст в другом. Позже такой “разнос” дает удобность понимания аспектов в отдельности. Нет большой проблемы “смешать” вывод сообщения с подсчетом выигрыша раунда. Но мы ради тренировки вынесем вычисление сообщения в отдельный метод getMessage
.
Я постарался расписать получени сообщения так чтобы покрыть все возможные случаи. Но не уверен что действиельно покрыл все случаи. Для индикации того что последняя ветка условия никогда не должна выполнятся я кидаю ошибку, а для упрощения отладки, вывожу состояния рук в консоль.
getMessage() {
// специальный случай когда копьютер не походил а пользователь умудрился набрать 21
if (this.playerHandWeight === 21 && this.computerHandWeigh === 0) {
return "Удача! Раунд за тобой, игрок."
} else if (this.playerHandWeight > 21 && this.computerHandWeigh > 21) {
return "Перебор у всех. Ничья."
} else if (
(this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.playerHandWeight > this.computerHandWeigh) ||
(this.playerHandWeight <= 21 && this.computerHandWeigh > 21)) {
return "Раунд за игроком."
} else if (
(this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.computerHandWeigh > this.playerHandWeight) ||
(this.playerHandWeight > 21 && this.computerHandWeigh <= 21)) {
return "Раунд за компьютером."
} else if (this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.playerHandWeight === this.computerHandWeigh) {
return "Ничья."
} else {
console.log('this.playerHandWeight', this.playerHandWeight)
console.log('this.computerHandWeigh', this.computerHandWeigh)
throw new Error('Не должно выполняться')
}
}
Бонус. В прошлой версии кода была логическая ошибка:
pass() {
this.playersTurn = true
this.renderTurn()
this.computerTurn()
}
Строка должна быть this.playersTurn = false
. Ошибки такого рода неизбежны. Отлавливать их надо методически: или при код ревью или при тестировании всех возможных состояний в своем коде.
Весь код:
// Основной класс который управляет игрой а так же отображением состояния игры в 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>
<div class="message"></div>
<button class="message-action">Далее</button>
<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.DOMMessage = this.DOMRootNode.querySelector('.message')
this.DOMMessageAction = this.DOMRootNode.querySelector('.message-action')
this.DOMDeckOfCards.addEventListener('click', () => {
if (!this.playersTurn) {
return
}
this.pickCard()
})
this.DOMPass.addEventListener('click', () => {
if (!this.playersTurn) {
return
}
this.pass()
})
this.DOMMessageAction.addEventListener('click', () => {
this.startRound()
})
this.renderScore()
this.clearMessage()
}
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 = false
this.renderTurn()
this.computerTurn()
}
renderTurn() {
this.DOMPass.disabled = !this.playersTurn
}
_getWeight(hand) {
return hand.reduce((sum, card) => {
return sum + card.weight
}, 0)
}
endRound() {
// перебора нет
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.showMessage(this.getMessage())
this.renderScore()
this.renderComputerHand()
this.renderPlayerHand()
this.renderTurn()
}
startRound() {
this.computerCards = []
this.playersCards = []
this.deck = this._createDeck()
this.playersTurn = true
this.renderScore()
this.renderComputerHand()
this.renderPlayerHand()
this.renderTurn()
this.clearMessage()
}
getMessage() {
// специальный случай когда копьютер не походил а пользователь умудрился набрать 21
if (this.playerHandWeight === 21 && this.computerHandWeigh === 0) {
return "Удача! Раунд за тобой, игрок."
} else if (this.playerHandWeight > 21 && this.computerHandWeigh > 21) {
return "Перебор у всех. Ничья."
} else if (
(this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.playerHandWeight > this.computerHandWeigh) ||
(this.playerHandWeight <= 21 && this.computerHandWeigh > 21)) {
return "Раунд за игроком."
} else if (
(this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.computerHandWeigh > this.playerHandWeight) ||
(this.playerHandWeight > 21 && this.computerHandWeigh <= 21)) {
return "Раунд за компьютером."
} else if (this.playerHandWeight <= 21 && this.computerHandWeigh <= 21 && this.playerHandWeight === this.computerHandWeigh) {
return "Ничья."
} else {
console.log('this.playerHandWeight', this.playerHandWeight)
console.log('this.computerHandWeigh', this.computerHandWeigh)
throw new Error('Не должно выполняться')
}
}
clearMessage() {
this.DOMMessage.innerHTML = ''
this.DOMMessageAction.style.display = 'none'
}
showMessage(text) {
this.DOMMessage.innerHTML = text
this.DOMMessageAction.style.display = 'block'
}
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'))