Selenium для игр: автоматизируем крестики-нолики

Автор оригинала: Angie Jones
  • Перевод

В преддверии старта курса "Java QA Automation Engineer" делимся с вами традиционным переводом интересного материала.


На тему моего стрима на этой неделе меня вдохновила демонстрация Судхарсана Селвараджа, где он использовал Selenium для игры на виртуальном пианино. Я тоже хотела использовать Selenium, чтобы немного развлечь вас и себя, поэтому составила этот «рецепт», наглядно демонстрирующий, как автоматизировать игру в крестики-нолики (tic-tac-toe) онлайн!

Что особо примечательно в этом рецепте, так это то, что он выходит за рамки привычного использования Selenium для тестирования и способствует развитию навыков проектирования.

Рецепт автоматизации игры в крестики-нолики

Ингредиенты

  • Selenium WebDriver

  • Игра крестики-нолики

Инструкции

  1. Создайте конструкцию для представления игровых пространств

  2. Создайте конструкцию для представления игрового поля и игрового поведения

  3. Создайте конструкцию для представления самой игры

  4. Создайте класс для выполнения игрового процесса

- Энджи Джонс

Запись стрима:

Ингредиенты

1. Selenium WebDriver

Добавьте зависимость Selenium WebDriver в свой проект. Я использую maven, поэтому добавляю эту зависимость в свой файл pom.xml.

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.0.0-alpha-5</version>
</dependency>

2. Игра крестики-нолики

Мы будем использовать эту онлайн-игру в крестики-нолики — Tic Tac Toe.

Инструкция

1. Создаем конструкцию для представления игровых пространств

Я создала enum (перечисление) для хранения пустых клеток в игре в крестики-нолики. Enum также будет содержать локаторы для каждой из этих клеток. Это позволит нам легко ссылаться на клетки на поле по мере их необходимости.

package tictactoe;
import org.openqa.selenium.By;
import static java.lang.String.format;
 
public enum Space {
 
    TOP_LEFT("square top left"),
    TOP_CENTER("square top"),
    TOP_RIGHT("square top right"),
    CENTER_LEFT("square left"),
    CENTER_CENTER("square"),
    CENTER_RIGHT("square right"),
    BOTTOM_LEFT("square bottom left"),
    BOTTOM_CENTER("square bottom"),
    BOTTOM_RIGHT("square bottom right");
 
    private String className;
    private By locator;
 
    Space(String className){
        this.className = className;
        locator = By.xpath(format("//div[@class='%s']", className));
    }
 
    public String getClassName(){
        return className;
    }
 
    public By getLocator(){
        return locator;
    }
}

2. Создаем конструкцию для представления игрового поля и игрового поведения

Теперь нам нужен класс, представляющий игровое поле. Этот класс будет отслеживать текущую игру, сохраняя состояние каждой из клеток поля и позволяя игроку сделать ход.

Поскольку нам нужно взаимодействовать с веб-сайтом, этому классу потребуется ChromeDriver из Selenium.

public class Board {
 
    private ChromeDriver driver;
    
    public Board(ChromeDriver driver) {
        this.driver = driver;
    }
}

Затем мне потребуется структура, которая будет представлять состояние каждой из клеток на поле, поэтому давайте создадим Map (ассоциативный массив), которая содержит каждую клетку и логическое значение, указывающее, занята ли она.

public class Board {
 
    private ChromeDriver driver;
    private Map<Space, Boolean> spaces = new HashMap();
 
    public Board(ChromeDriver driver) {
        this.driver = driver;
    }
}

Затем нам нужно заполнить map клетками из нашего перечисления Space и установить для всех них значение занятости false, поскольку игра еще не началась и поле пустое.

public class Board {
 
    private ChromeDriver driver;
    private Map<Space, Boolean> spaces = new HashMap();
 
    public Board(ChromeDriver driver) {
        this.driver = driver;
        Arrays.stream(Space.values()).forEach(space -> spaces.put(space, false));
    }
}

А теперь самое интересное! Нам нужен метод, который позволит пользователю совершать ходы. В этом методе мы попросим игрока указать нам клетку, на котором он хотел бы поставить свой знак, и нам также необходимо обновить нашу внутреннюю Map.

Кроме того, поскольку мы играем против компьютера, он делает свой ход сразу после нашего, поэтому я добавила некоторое ожидание, чтобы дать компьютеру походить. Не заостряйтесь на этом моменте, это не тестовый код, который будет выполняться в нескольких средах как часть CI конвейера. Здесь можно немножко и подхалтурить.

public void play(Space space){
        driver.findElement(space.getLocator()).click();
        spaces.put(space, true);
        try{ Thread.sleep(500); } catch(Exception e){}
    }

Метод play отражает наш ход в нашей внутренней Map, но не учитывает ход компьютера. Так давайте же создадим метод для проверки браузера и обновления нашей Map, который будет точно отражать статус игрового поля.

Но сначала нам нужен локатор, чтобы получить все пустые клетки на поле.

private By emptySpacesSelector = By.xpath("//div[@class='board']/div/div[@class='']/..");

Затем мы используем Selenium, чтобы получить все эти пустые клетки, сохранить их в List (список), а затем пройтись по нашей внутренней Map, чтобы обновить все клетки, которые не отображаются в списке Selenium с пустыми клетками, чтобы они были помечены как занятые.

/**
     * Updates Spaces map to be in sync with the game on the browser
     */
    private void updateSpaceOccupancy(){
        var emptyElements = driver.findElements(emptySpacesSelector)
                .stream()
                .map(e->e.getAttribute("class"))
                .collect(Collectors.toList());
 
        Arrays.stream(Space.values()).forEach(space -> {
            if(!emptyElements.contains(space.getClassName())){
                spaces.put(space, true);
            }
        });
    }

Окей, нам нужен еще один метод для этого класса. Было бы неплохо иметь возможность предоставлять список свободных клеток, чтобы наш игрок знал, из чего можно выбирать. Этот метод в свою очередь вызовет метод, который мы только что создали, updateSpaceOccupancy(), затем отфильтрует нашу внутреннюю Map клеток и получит все незанятые.

public List<Space> getOpenSpaces(){
        updateSpaceOccupancy();
        return spaces.entrySet()
                .stream()
                .filter(occupied -> !occupied.getValue())
                .map(space->space.getKey())
                .collect(Collectors.toList());
    }

3. Создаем конструкцию, представляющую саму игру

Теперь давайте создадим класс, представляющий саму игру. Первое, что нужно сделать этому классу, — это настроить ChromeDriver и запустить игру. Мы также можем создать инстанс игрового поля.

public class Game {
 
    private ChromeDriver driver;
    private Board board;
    
    public Game() {
        System.setProperty("webdriver.chrome.driver", "resources/chromedriver");
        driver = new ChromeDriver();
        driver.get("https://playtictactoe.org/");
 
        board = new Board(driver);
    }
 
    public Board getBoard(){
        return board;
    }
}

Мы хотели бы дать пользователю возможность определять, когда игра окончена. Это приложение отображает окно перезапуска игры по завершению партии, поэтому мы можем создать метод, который позволит нам это узнать, и наш собственный метод для перезапуска игры по желанию игрока. При перезапуске нужно не забыть сбросить игровое поле, чтобы все клетки снова были помечены как пустые.

private By restartIndicator = By.className("restart");
public boolean isOver(){
        return driver.findElement(restartIndicator).isDisplayed();
    }
 
    public Board restart() {
        driver.findElement(restartIndicator).click();
        board = new Board(driver);
        return board;
    }

Затем нам нужно определить, выиграл игрок или проиграл. Т.е. я создала три метода. Один для получения результатов с веб-сайта, другой для предоставления пользователю возможности указать выигрышный счет (в случае, если они хотят играть, пока один из игроков не наберет определенное количество очков), и, наконец, третий для вывода результатов.

public boolean isThereAWinner(int winningScore){
        updateScores();
        return playerScore >= winningScore || computerScore >= winningScore;
    }
 
   /**
    * Gets scores from the browser
    */
    public void updateScores(){
 
        var scores = driver.findElementsByClassName("score")
                .stream()
                .map(WebElement::getText)
                .map(Integer::parseInt)
                .collect(Collectors.toList());
 
        playerScore = scores.get(0);
        tieScore = scores.get(1);
        computerScore = scores.get(2);
    }
 
    public void printResults(){
        if(playerScore > computerScore){
            System.out.println(format("Yayyy, you won! ? Score: %d:%d", playerScore, computerScore));
        }
        else if(computerScore > playerScore){
            System.out.println(format("Awww, you lose. ? Score: %d:%d", computerScore, playerScore));
        }
    }

Наконец, мы добавим в этот класс метод завершения игры, который закрывает браузер и убивает поток.

public void end(){
        printResults();
        driver.quit();
        System.exit(0);
    }

4. Создаем класс для выполнения игрового процесса

Теперь приступим к игре! Я создала еще один класс для выполнения игры от лица игрока. Первое, что мы здесь сделаем, это создадим инстанс новой игры и получим дескриптор игрового поля.

public class PlayGame {
 
    public static void main(String args[]){
        var game = new Game();
        var board = game.getBoard();
    }
}

Затем мы определяем, сколько партий мы хотим сыграть до определения победителя. В этом примере мы указываем, что тот, кто первым наберет 5 очков, является окончательным победителем. Мы хотим продолжать игру, пока не дойдем до этого момента, поэтому мы будем использовать цикл while для представления каждого раунда.

public static void main(String args[]){
        var game = new Game();
        var board = game.getBoard();
 
        while(!game.isThereAWinner(5))
        {
 
        }
    }

Внутри цикла нам нужен еще один цикл while, чтобы представить каждую игру в пределах одной партии.

while(!game.isThereAWinner(5))
        {
            while(!game.isOver()) 
            {
 
            }
 
        }

Внутри этого внутреннего цикла мы будем играть на игровом поле. Значит нам нужно получить пустые клетки. Было бы неплохо написать алгоритм со стратегией выбора клетки, однако пока мы просто случайным образом выбираем одну из списка свободных клеток.

while(!game.isThereAWinner(5))
        {
            while(!game.isOver()) {
                var spaces = board.getOpenSpaces();
                board.play(spaces.get(new Random().nextInt(spaces.size())));
            }
        }

Затем после каждого раунда мы должны очищать поле, нажимая кнопку сброса. Это должно быть внутри внешнего цикла, вне области видимости внутреннего.

while(!game.isThereAWinner(5))
        {
            while(!game.isOver()) {
                var spaces = board.getOpenSpaces();
                board.play(spaces.get(new Random().nextInt(spaces.size())));
            }
            board = game.restart();
        }

И наконец, мы заканчиваем игру! Это должно быть за пределами обоих циклов.

public static void main(String args[]){
        var game = new Game();
        var board = game.getBoard();
 
        while(!game.isThereAWinner(5))
        {
            while(!game.isOver()) {
                var spaces = board.getOpenSpaces();
                board.play(spaces.get(new Random().nextInt(spaces.size())));
            }
            board = game.restart();
        }
        game.end();
    }

Вуаля! Теперь у нас есть решение для автоматизации игры в крестики-нолики.


Узнать подробнее о курсе "Java QA Automation Engineer".

Предлагаем также посмотреть запись demo-урока на тему тестирования API:
«HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI».

OTUS
Цифровые навыки от ведущих экспертов

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое