[#5] Пишем игру Backjack (Очко). Конец хода игрока и ход компьютера

Ссылка на запускаемый код. Код результата в конце статьи.

Что значит окончить ход игрока? Значит что запустить ход компьютера. Что значит ход копьютера? Подумаем.

Ход компьютера означает примерно следующее. Нужно взять карту в руку (это мы знаем - перемешение объекта из массива колоды в массив руки компьютера), посмотреть на руку (считай посчитать очки на руках), остаток колоды (в зависимости от “разумности” бота посчитать вероятность вытягивания нужной ему карты) принять решение нужно ли повторять ход, если нужно то повторить то снова взять карту. Мы хотим чтобы ход компьютера был виден человеку поэтому между взятиям карт должно проходить время. Время моделируется в 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'))

Добавил обработчик для кнопки пас

this.DOMPassButton.addEventListener('click', () => {
            this.pass();
        });, 
теперь компьютер каждые 1.5 секунды выдает новую карту
pass() {
        this.timeClock =  setInterval(() => { this.pickComputerCard() }, 1500);
}
 и проверяет счёт
pickComputerCard() {
        const card = this.deck.pop();
        this.computerScore.push(card.weight);
        this.computercards.push(card);
        this.scoreComputerHand = this.sumOfComputerScore();
        this.renderComputerHand(card);
        this.checkComputerScore();
    }

renderComputerHand(card) {
        this.DOMComputerCards.innerHTML = this.DOMComputerCards.innerHTML + `
            <div class=computer-card style=background-image:url(./img/`+ card.name + `-` + card.suit + `.jpg)></div>
        `
        this.DOMComputerScore.innerHTML = `
            ${JSON.stringify(this.scoreComputerHand)}
        `
    }


checkComputerScore() {
        if (this.scoreComputerHand === 21) {
            this.removeComputerEventListener();
            clearInterval(this.timeClock);
            alert('Computer win'); }
        else if (this.scoreComputerHand > 21) {
            this.removeComputerEventListener();
            clearInterval(this.timeClock);
            alert('Computer lose') }
    }
, но не могу удалить обработчик событий который вызывает эту функцию.

removeComputerEventListener(){
        this.DOMPassButton.removeEventListener('click',() => { 
            this.pass();
        });
    }

Теперь мой код выглядит так: 
class BlackJackGame {
    constructor(rootNode) {
        this.playercards = null;
        this.computercards = null;
        this.playerScore = null;
        this.computerScore = null;
        this.timeClock = null;
        this.deck = null;
        this.scorePlayerHand = null;
        this.scoreComputerHand = null;
        this.DOMComputerScore = null;
        this.DOMPlayerScore = null;
        this.DOMComputerCards = null;
        this.DOMPlayersCards = null;
        this.DOMDeckofCards = null;
        this.DOMPassButton = null;
        this.DOMRootNode = null;

        this.DOMRootNode = rootNode;
        this.init();
    }

    init() {
        this.playercards = [];
        this.computercards = [];
        this.playerScore = [];
        this.computerScore = [];
        this.deck = this._createDeck();

        this.DOMRootNode.innerHTML = `
            <div class='computer-score'></div>
            <div class='player-score'></div>
            <div class='deck-of-cards'></div>
            <div class='computer-cards'></div>
            <div class='player-cards'></div>
            <button class='pass'>Pass</button>
        `
        this.DOMComputerScore = this.DOMRootNode.querySelector('.computer-score');
        this.DOMPlayerScore = this.DOMRootNode.querySelector('.player-score');
        this.DOMComputerCards = this.DOMRootNode.querySelector('.computer-cards');
        this.DOMPlayersCards = this.DOMRootNode.querySelector('.player-cards');
        this.DOMDeckofCards = this.DOMRootNode.querySelector('.deck-of-cards ');
        this.DOMPassButton = this.DOMRootNode.querySelector('.pass');

        this.DOMDeckofCards.addEventListener('click', () => {
            this.pickPlayerCard();
        });

        this.DOMPassButton.addEventListener('click', () => {
            this.pass();
        });
    }

    sumOfPlayerScore() {
        var s = 0;
        for (var i = 0; i < this.playerScore.length; i++) {
            s += this.playerScore[i];
        }
        return s;
    }

    sumOfComputerScore() {
        var s = 0;
        for (var i = 0; i < this.computerScore.length; i++) {
            s += this.computerScore[i];
        }
        return s;
    }

    pickPlayerCard() {
        const card = this.deck.pop();
        this.playerScore.push(card.weight);
        this.scorePlayerHand = this.sumOfPlayerScore();
        this.playercards.push(card);
        this.renderPlayerHand(card);
        this.checkPlayerScore();
    }

    pickComputerCard() {
        const card = this.deck.pop();
        this.computerScore.push(card.weight);
        this.computercards.push(card);
        this.scoreComputerHand = this.sumOfComputerScore();
        this.renderComputerHand(card);
        this.checkComputerScore();
    }

    renderPlayerHand(card) {
        this.DOMPlayersCards.innerHTML = this.DOMPlayersCards.innerHTML + `
            <div class=player-card style=background-image:url(./img/`+ card.name + `-` + card.suit + `.jpg)></div>
        `
        this.DOMPlayerScore.innerHTML = `
            ${JSON.stringify(this.scorePlayerHand)}
        `
    }

    renderComputerHand(card) {
        this.DOMComputerCards.innerHTML = this.DOMComputerCards.innerHTML + `
            <div class=computer-card style=background-image:url(./img/`+ card.name + `-` + card.suit + `.jpg)></div>
        `
        this.DOMComputerScore.innerHTML = `
            ${JSON.stringify(this.scoreComputerHand)}
        `
    }

    checkPlayerScore() {
        if (this.scorePlayerHand === 21){
            this.removePlayerEventListener();
            alert('You win');
        }
        else if (this.scorePlayerHand > 21){
            this.removePlayerEventListener();
            alert('You lose');
        } 
    }

    checkComputerScore() {
        if (this.scoreComputerHand === 21) {
            this.removeComputerEventListener();
            clearInterval(this.timeClock);
            alert('Computer win'); }
        else if (this.scoreComputerHand > 21) {
            this.removeComputerEventListener();
            clearInterval(this.timeClock);
            alert('Computer lose') }
    }

    removePlayerEventListener(){
        this.DOMDeckofCards.removeEventListener('click', () => {
            this.pickPlayerCard();
        });
    }

    removeComputerEventListener(){
        this.DOMPassButton.removeEventListener('click',() => { 
            this.pass();
        });
    }

    pass() {
        this.timeClock =  setInterval(() => { this.pickComputerCard() }, 1500);
    }

    _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 cardInfo of cards) {
                deck.push({
                    suit: suit,
                    name: cardInfo.name,
                    weight: cardInfo.weight
                })
            }
        }
        deck.sort(() => {
            return Math.random() > 0.5 ? 1 : -1;
        })

        return deck;
    }
}

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

Такой вопрос может сделать универсальные функции, чтобы не дублировать те же функции с другими названиями а передавать всё таки параметры. Так и уменьшим занимаемое место )

Дай пример как выглядит повторяющаяся функция, обсудим.

В принципе такое возможно. Но не всегда нужно делать все что возможно. Иногда дупликация кода проще для чтения и понимания чем абстракция которая убирает дупликацию.

Вот функции которые берут последнюю карту в массиве так как колода переворачивается, введут счёт

pickPlayerCard() {
const card = this.deck.pop();
this.playerScore.push(card.weight);
this.scorePlayerHand = this.sumOfPlayerScore();
this.playercards.push(card);
this.renderPlayerHand(card);
this.checkPlayerScore();
}

pickComputerCard() {
    const card = this.deck.pop();
    this.computerScore.push(card.weight);
    this.computercards.push(card);
    this.scoreComputerHand = this.sumOfComputerScore();
    this.renderComputerHand(card);
    this.checkComputerScore();
}

Также функции суммы элементов для счёта

sumOfPlayerScore() {
var s = 0;
for (var i = 0; i < this.playerScore.length; i++) {
s += this.playerScore[i];
}
return s;
}

sumOfComputerScore() {
    var s = 0;
    for (var i = 0; i < this.computerScore.length; i++) {
        s += this.computerScore[i];
    }
    return s;
}

Рендерят руки игрока и компьютера:

renderPlayerHand(card) {
this.DOMPlayersCards.innerHTML = this.DOMPlayersCards.innerHTML + <div class=player-card style=background-image:url(./img/+ card.name + - + card.suit + .jpg)></div>
this.DOMPlayerScore.innerHTML = ${JSON.stringify(this.scorePlayerHand)}
}

renderComputerHand(card) {
    this.DOMComputerCards.innerHTML = this.DOMComputerCards.innerHTML + `
        <div class=computer-card style=background-image:url(./img/`+ card.name + `-` + card.suit + `.jpg)></div>
    `
    this.DOMComputerScore.innerHTML = `
        ${JSON.stringify(this.scoreComputerHand)}
    `
}

Я думаю что в этом варианте можно сократить и передавать данные параметром в функции

Понял идею. Действительно можно и даже нужно абстрагировать эти моменты. Но предлагаю сейчас оставить как есть, но взять на карандаш эту возможность и в конце когда у нас высветится готовое приложение, “причесать” его в плане абстракций. Нюанс будет в том что при создании абстракций нужно будет понимать чуть больше про javascript (например как работает работа с объектами по ссылке в случае когда мы будем двигать карту между массивами).

Эти функции не будут работать так как ты ожидаешь. Чтобы убрать обработчик событий нужно вторым аргументом в .removeEventListener передавать ссылку на оригинальный обработчик события. Типо так (обработчик клика существует 10 секунд, потом убирается):

function handler() {
	alert('works')
}

document.body.addEventListener('click', handler)

setTimeout(() => {
	document.body.removeEventListener('click', handler)	
}, 10000)

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