Ссылка на запускаемый код. Код результата в конце статьи.
Что значит окончить ход игрока? Значит что запустить ход компьютера. Что значит ход копьютера? Подумаем.
Ход компьютера означает примерно следующее. Нужно взять карту в руку (это мы знаем - перемешение объекта из массива колоды в массив руки компьютера), посмотреть на руку (считай посчитать очки на руках), остаток колоды (в зависимости от “разумности” бота посчитать вероятность вытягивания нужной ему карты) принять решение нужно ли повторять ход, если нужно то повторить то снова взять карту. Мы хотим чтобы ход компьютера был виден человеку поэтому между взятиям карт должно проходить время. Время моделируется в js с помощью setTimeout
и setInterval
. С отрисовкой также не заморачиваемся, просто показываем json в узле руки компьютера.
Тут мы с @NONLUCIFER обсудили как подходить к подсчету значения руки. Я предложил getter. При этом нам нужно два getter-а: один для руки игрока один для руки компьютера. Так как логика у обоих геттеров одинаковая, вынесу тело геттера в метод. И чтобы было понятно что метот внутренний, назову его начиная с подчеркивания. Так обычно помечают непубличные, методы которые вторичны по отношению к методам без подчеркивания.
Пишем метод подсчета суммы элементов массива.
_getWeight(hand) {
return hand.reduce((sum, card) => {
return sum + card.weight
}, 0)
}
И геттеры для игрока и компьютера. Описываем их в самом верху чтобы было очевидно их существование.
get playerHandWeight() {
return this._getWeight(this.playersCards)
}
get computerHandWeigh() {
return this._getWeight(this.computerCards)
}
Элементы для моделирования хода компьютера в принципе есть. Осталось расположить их в правильном порядке. Я решаю такие задачи итерациями которые частично проходят в воображении а частично пишутся. Получилась вот такая таймаутно-рекурсивная функция
computerTurn() {
const card = this.deck.shift()
this.computerCards.push(card)
this.renderComputerHand()
if (this.computerHandWeigh < 18) {
setTimeout(() => {
this.computerTurn()
}, 500)
} else {
this.endRound()
}
}
Функция или запускает сама себя или останавливает раунд. Функция остановки раунда пока пуста, но понятно что там мы в будущем подобъем счет и подготовим часть состояние для нового раунда.
На данном этапе код программы далеко не корректный (например можно успешно нажимать на колоду в момент хода компьютера). Но из-за модульности реализации нас не беспокоит эта “сырость” решения. Ограничение взаимодействия игрока с колодой и кнопками реализуем в другой день. Код на jsfiddle: Edit fiddle - JSFiddle - Code Playground и собственно текст программы:
// Основной класс который управляет игрой а так же отображением состояния игры в 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
// Узлы в которые будет происходит отрисовка состояния игры. Удобно когда узлы существуют в течение всей игры. Тогда не придетс заморачиваться с поддержанием состосния (навешивание-убирание обработчиков событий)
// Иногда для удобства отличия узлов от данных придумывают формат переменных, например, начинающиеся с $ это 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('.pass')
this.DOMDeckOfCards.addEventListener('click', () => {
this.pickCard()
})
this.DOMPass.addEventListener('click', () => {
this.pass()
})
}
pickCard() {
const card = this.deck.shift()
this.playersCards.push(card)
this.renderPlayerHand()
}
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.computerTurn()
}
_getWeight(hand) {
return hand.reduce((sum, card) => {
return sum + card.weight
}, 0)
}
endRound() {
}
// Обычно создание и перемешивание колоды разделяется. Но ради простоты конечного кода я совмещаю эти процессы.
_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'))