Сейчас нам осталось написать методы, которые будут проверять, есть ли в игре победитель, и кто выиграл.
Будем перебирать выигрышные комбинации, делать пользователя победителем и тестировать проверку на победителей.
Начнём с выигрыша по горизонтали и постепенно будем увеличивать количество выигрышных комбинаций. Подробно описывать каждую не будем, рассмотрим только горизонталь. Остальные комбинации можно будет подсмотреть в исходном коде на гитхабе.
test/game.jstest('Checks if user won by horizontal', () => { game.acceptUserMove(0, 0) game.acceptUserMove(0, 1) game.acceptUserMove(0, 2) const userWon = game.isWinner(userName) expect(userWon).toEqual(true) })
src/Game.js+ isWinner(player) { + return false + }
Аргументом передаём имя игрока, которого хотим проверить. В зависимости от имени будем выбирать искомый символ. Затем проверим, выполняется ли условие победы по горизонтали и вернём результат.
src/Game.jsisWinner(player) { const symbol = player === this._userName ? this._userMoveSymbol : this._computerMoveSymbol const win = [...Array(this._fieldSize).keys()].reduce((res, i) => { return this._board[i][0] === symbol && this._board[i][1] === symbol && this._board[i][2] === symbol || res }, false) return win }
Определение искомого символа по имени вынесем в метод _getSymbolForPlayer.
Массив заданной размерности будем создавать в константе range.
Метод _checkCellEqual будет запоминать символ, с которым мы собираемся сравнивать клетку на доске и возвращать функцию для сравнения. Эта функция будет принимать координаты клетки и сравнивать её содержимое с указанным символом.
Так как у нас будет несколько условий победы, назовём условие победы по горизонтали horizontal.
src/Game.jsisWinner(player) { const symbol = this._getSymbolForPlayer(player) const range = [...Array(this._fieldSize).keys()] const isEqual = this._checkCellEqual(symbol) const horizontal = range.reduce((res, i) => isEqual(i, 0) && isEqual(i, 1) && isEqual(i, 2) || res, false) return horizontal } _getSymbolForPlayer(player) { return player === this._userName ? this._userMoveSymbol : this._computerMoveSymbol } _checkCellEqual(symbol) { return (i, j) => this._board[i][j] === symbol }
В коде теста на первый взгляд всё хорошо. Мы объявляем, в какие клетки проставить крестик, чтобы потом проверить, победил ли пользователь.
Но попробуем посмотреть «взглядом новичка» на game.acceptUserMove(0, 0). Если ничего не знать о внутреннем устройстве класса, то сообразить, что делает эта строка, трудно.
Это становится проблемой, когда мы переводим требования с языка разработки на язык бизнеса и обратно. Работать с тестами могут не только разработчики, но и аналитики, тестировщики и кто угодно ещё.
В этом тесте мы проверяем, что если на доске строчка заполнена крестиками, то пользователь победил. Было бы круто прямо так и написать.
На самом деле мы можем так написать свой тест. Это называется DSL. Для этого нам потребуется что‑то, что будет переводить метод setBoardState в понятные методы для класса Game.
Для таких целей можно использовать паттерн строитель (builder). Класс GameBuilder возьмёт на себя логику создания игры с нужной нам доской.
Ключевая особенность классов‑строителей в том, что в каждом методе with- они возвращают this. Таким образом, при их использовании мы можем совмещать методы в цепочки, чтобы настраивать нужный нам объект.
После настройки останется вызвать метод build, чтобы строитель вернул созданный объект.
Теперь писать тесты на проверку победы удобнее и нагляднее.
// pseudocodesomegame.setBoardState(` x x x . . . . . . `)
test/GameBuilder.jsimport Game from '../src/Game' class GameBuilder { constructor() { this.game = new Game() } withBoardState(state) { state = state .split('\n') .filter(item => !!item.trim()) .map(item => item.trim().split(' ')) state.forEach((item, i) => { item.forEach((symbol, j) => { if (symbol === 'x') this.game.acceptUserMove(i, j) }) }) // will allow us to chain methods return this } build() { return this.game } } export default GameBuilder
test/game.jsimport GameBuilder from '../src/GameBuilder' // ... test('Checks if user won by horizontal', () => { const game = new GameBuilder() .withBoardState(` x x x . . . . . .`) .build() const userWon = game.isWinner(userName) expect(userWon).to.equal(true) })