
Selenium for Games: Automating Tic Tac Toe
For this week’s live session, I was inspired by Sudharsan Selvaraj’s demo of him using Selenium to play a virtual piano. I, too, wanted to use Selenium to do something fun, so this recipe demonstrates how to automate playing tic-tac-toe online!
What’s great about this recipe is it goes beyond just using Selenium to test, but exercises design skills.
Recipe to Play Tic-Tac-Toe
Ingredients
- Selenium WebDriver
- Tic Tac Toe Game
Instructions
- Create construct to represent game spaces
- Create construct to represent game board and play behavior
- Create construct to represent the game itself
- Create class to execute game play
Live Stream Video
Ingredients
1 Selenium WebDriver
Add the Selenium WebDriver dependency to your project. I am using maven so I add this dependency to my pom.xml file.
1 2 3 4 5 |
<dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.0.0-alpha-5</version> </dependency> |
2 Tic Tac Toe Game
We’ll use this online Tic Tac Toe game.
Instructions
1 Create construct to represent game spaces
I created an enum to hold the spaces on the tic tac toe game. The enum will also hold the locators for the each of the spaces. This will allow us to easily references board spaces as we need them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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 Create construct to represent game board and play behavior
Now we need a class that represents the game board. This class will keep track of the current game in play, holding the state of each of the board spaces, and allowing the player to make a move.
Since we’ll need to interact with the website, this class will need the ChromeDriver from Selenium.
1 2 3 4 5 6 7 8 |
public class Board { private ChromeDriver driver; public Board(ChromeDriver driver) { this.driver = driver; } } |
Next, I want a structure to represent the status of each of the spaces on the board, so let’s create a Map which holds each space and a boolean value indicating whether it is occupied.
1 2 3 4 5 6 7 8 9 |
public class Board { private ChromeDriver driver; private Map<Space, Boolean> spaces = new HashMap(); public Board(ChromeDriver driver) { this.driver = driver; } } |
Next, we need to populate the map with each of the spaces from our Space enum and set occupancy to false for all of them since the game has not begun and the board is empty.
1 2 3 4 5 6 7 8 9 10 |
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 for the fun part! We need a method that allows the user to make a play. In this method, we’ll have the player provide the Space they’d like to put their marker on, and we also need to update our internal Map.
Also, because we are playing against a computer, it makes it’s play right after ours, so I’ve added a little wait here to allow for their move. Don’t worry, this isn’t test code that will be running on multiple environments as part of a CI build. A little sleep here is ok 🤪
11 12 13 14 15 |
public void play(Space space){ driver.findElement(space.getLocator()).click(); spaces.put(space, true); try{ Thread.sleep(500); } catch(Exception e){} } |
The play method updates our internal Map with our move, but doesn’t account for the computer’s move. So let’s make a method to check the browser and update our Map to accurately reflect the status of the board.
But first, we need a locator to get all of the empty spaces on the board.
5 |
private By emptySpacesSelector = By.xpath("//div[@class='board']/div/div[@class='']/.."); |
Next, we use Selenium to get all of these empty spaces, store them into a List, then loop through our internal Map to update all of the spaces that do not appear in the empty-space Selenium list to be marked as occupied.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * 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); } }); } |
Ok, one more method for this class. It would be nice to provide a list of the unoccupied spaces so that our player knows what’s available to choose from. This method will call into the one we just made, updateSpaceOccupancy(), then filter through our internal Map of spaces and get all of the unoccupied ones.
33 34 35 36 37 38 39 40 |
public List<Space> getOpenSpaces(){ updateSpaceOccupancy(); return spaces.entrySet() .stream() .filter(occupied -> !occupied.getValue()) .map(space->space.getKey()) .collect(Collectors.toList()); } |
3 Create construct to represent the game itself
Now, let’s make a class to represent the actual game itself. The first thing this class needs to do is setup up the ChromeDriver and launch the game. We can also create an instance of the board.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 want to enable the user to determine when the game is over. This app displays a restart overlay once the game ends, so we can create a method that let’s us know this, and another method to restart the game should the player want to. When restarting, it’s important to reinstatiate the board so that all spaces are marked as empty again.
5 |
private By restartIndicator = By.className("restart"); |
20 21 22 23 24 25 26 27 28 |
public boolean isOver(){ return driver.findElement(restartIndicator).isDisplayed(); } public Board restart() { driver.findElement(restartIndicator).click(); board = new Board(driver); return board; } |
Next, we need to determine if the player has won or lost. So, I’ve created three methods. One to get the scores from the website, another to allow the user to provide a winning score (in case they want to play until one of the players reaches a certain score), and finally one to print the results.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
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 for this class, we’ll add a method to end the game – which closes the browser and kills the thread.
58 59 60 61 62 |
public void end(){ printResults(); driver.quit(); System.exit(0); } |
4 Create class to execute game play
Now to play the game! I’ve created another class to execute the game as the player. First thing we’ll do here is instantiate a new game and get a handle to the game board.
1 2 3 4 5 6 7 |
public class PlayGame { public static void main(String args[]){ var game = new Game(); var board = game.getBoard(); } } |
Next, we determine how many rounds of the game we want to play before determining a winner. In this example, we indicate that whoever gets to 5 wins first is the ultimate winner. We want to continue playing until we get to this point, so we’ll use a while loop to represent each round.
3 4 5 6 7 8 9 10 11 |
public static void main(String args[]){ var game = new Game(); var board = game.getBoard(); while(!game.isThereAWinner(5)) { } } |
Inside of the loop, we need another while loop to represent each play within a single round.
7 8 9 10 11 12 13 14 |
while(!game.isThereAWinner(5)) { while(!game.isOver()) { } } |
Inside of this inner loop, we want to make a play on the board. So we need to get the empty spaces. It would be nice to have an algorithm for choosing the most strategic spot to choose, however, for now, we’re just going to randomly choose one of the open spaces.
7 8 9 10 11 12 13 |
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 make sure to clear the board by clicking the reset. This needs to be inside of the outer loop but outside of the inner loop.
7 8 9 10 11 12 13 14 |
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! This needs to be outside of both loops.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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! Now we have a solution to automate playing tic-tac-toe.
Maha Lakshmi
how to find the best move in this game ?
zemiak
to avoid Enum and Map
get list of empty squares from webpage
(where elements are of RemoteWebElement type)
use random element
and do .click()