Selenium for games: automating tic-tac-toe

On the eve of the start of the course Java QA Automation Engineer we share with you the traditional translation of interesting material.


My stream this week was inspired by the demonstration by Sudharsan Selvaraj, where he used selenium to play virtual piano… I also wanted to use Selenium to entertain you and myself a little, so I put together this “recipe” to show you how to automate tic-tac-toe online!

What’s particularly remarkable about this recipe is that it goes beyond the usual use of Selenium for testing and promotes development of design skills.

A recipe for automating tic-tac-toe

Ingredients

  • Selenium WebDriver

  • Tic-tac-toe game

Instructions

  1. Create a structure to represent play spaces

  2. Create a structure to represent the playing field and play behavior

  3. Create a structure to represent the game itself

  4. Create a class to perform gameplay

– Angie Jones

Stream recording:

Ingredients

1. Selenium WebDriver

Add Selenium WebDriver dependency to your project. I use mavenso I add this dependency to my pom.xml file.

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

2. The game of tic-tac-toe

We will be using this online tic-tac-toe game – Tic tac toe

Instructions

1. We create a structure to represent play spaces

I created enum (enumeration) for storing empty cells in a tic-tac-toe game. The Enum will also contain locators for each of these cells. This will allow us to easily reference cells on the field as needed.

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. We create a structure to represent the playing field and playing behavior

Now we need a class to represent the playing field. This class will keep track of the current game, saving the state of each of the squares of the field and allowing the player to make a move.

Since we need to interact with the website, this class will need the ChromeDriver from Selenium.

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

Then I need a structure that will represent the state of each of the cells in the field, so let’s create a Map (associative array) that contains each cell and a boolean value indicating whether or not it is busy.

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

Next, we need to fill the map with cells from our Space enumeration and set all of them busy to false, since the game has not started yet and the field is empty.

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));
    }
}

Now comes the fun part! We need a method that allows the user to make moves. In this method, we will ask the player to tell us the square on which he would like to put his sign, and we also need to update our internal Map.

Also, since we are playing against the computer, he makes his move right after ours, so I added some waiting to give the computer a run. Don’t dwell on this point, this is not test code that will run in multiple environments as part of the CI pipeline. Here you can cheat a little.

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

Method play reflects our move in our internal Map, but does not account for the computer’s move. So let’s create a method to test the browser and update our Map that accurately reflects the status of the game board.

But first, we need a locator to get all the empty cells on the field.

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

We then use Selenium to get all those empty cells, store them in a List, and then walk through our internal Map to update any cells that are not shown in the Selenium empty cell list to be marked as occupied.

/**
     * 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);
            }
        });
    }

Okay, we need another method for this class. It would be nice to be able to provide a list of free cells so that our player knows what to choose from. This method in turn will call the method we just created, updateSpaceOccupancy(), then it will filter out our inner Map of cells and get all the unoccupied ones.

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

3. Create a structure that represents the game itself

Now let’s create a class to represent the game itself. The first thing this class needs to do is configure the ChromeDriver and launch the game. We can also create a playfield instance.

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;
    }
}

We would like to give the user the ability to determine when the game is over. This application displays a window for restarting the game at the end of the game, so we can create a method that allows us to find out, and our own method for restarting the game at the player’s request. When restarting, you must remember to reset the playing field so that all cells are marked as empty again.

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;
    }

Then we need to determine if the player won or lost. Those. I have created three methods. One for getting results from the website, another for allowing the user to specify a winning score (in case they want to play until one of the players has a certain number of points), and finally the third for displaying the results.

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));
        }
    }

Finally, we’ll add a game termination method to this class that closes the browser and kills the thread.

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

4. We create a class for performing the gameplay

Now let’s start playing! I created another class to execute the game from the player’s perspective. The first thing we’ll do here is create an instance of a new game and get a handle to the game board.

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

We then determine how many games we want to play before determining the winner. In this example, we indicate that whoever scores 5 points first is the final winner. We want to keep playing until we get to this point, so we’ll use a while loop to represent each round.

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

Inside the loop, we need another loop whileto represent each game within the same game.

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

Within this inner loop, we will be playing on the playing field. So we need to get empty cells. It would be nice to write an algorithm with a cell selection strategy, but for now we just randomly choose one from the list of free cells.

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

Then after each round we have to clear the field by pressing the reset button. It should be inside the outer loop, outside the scope of the inner one.

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

Finally, we end the game! It should be outside of both loops.

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();
    }

Voila! We now have a solution to automate the tic-tac-toe game.


Learn more about the course Java QA Automation Engineer

We also suggest watching a demo lesson on the topic of API testing: “HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI “.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *