I'm writing a bot in Python that uses Selenium to play a web-based of Tic Tac Toe. I want to loop through an array of XPATHs that represent the game grid and check each square for the presence of the 'O' character. If the square is marked by an O, the number of that square should be appended to another list of marked squares.
This is the section of code that I'm trying to fix:
for i in Board.squares:
text = driver.find_element(By.XPATH, Board.squares[i]).text
if text == 'O':
LOGGER.info("Square " str(i) " marked by O.")
Board.markedSquares.append(i)
LOGGER.info("Appending marked squares list: " str(Board.markedSquares))
But I get the following error in traceback:
11/04/2022 10:32:49 AM–root– INFO:First move: clicking square 3
Traceback (most recent call last):
File "C:\Users\source\repos\React project_2\ttt_user_bot\RandomBot.py", line 97, in <module>
playTTT()
File "C:\Users\source\repos\React project_2\ttt_user_bot\RandomBot.py", line 72, in playTTT
text = driver.find_element(By.XPATH, Board.squares[i]).text
TypeError: list indices must be integers or slices, not str
Below is the full code for my bot. Everything works except for the for loop above, and I'm not sure how to fix it. Without this check, the bot can click on a square marked by an O, and the script will time out.
class Tags():
square1 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[1]"
square2 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[2]"
square3 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[3]"
square4 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[4]"
square5 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[5]"
square6 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[6]"
square7 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[7]"
square8 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[8]"
square9 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[9]"
ohSquare = "//div[contains(@class, 'board-row')]//button[contains(text(), 'O')]"
winner = "//div[contains(@class, 'game-info')]//div[contains(text(), 'Winner:')]"
tie = "//div[contains(@class, 'game-info')]//div[contains(text(), 'tie')]"
class Board():
squares = [Tags.square1,Tags.square2,Tags.square3,
Tags.square4,Tags.square5,Tags.square6,
Tags.square7,Tags.square8,Tags.square9]
markedSquares = []
def firstMove():
random_square = randint(0,8)
time.sleep(5)
element = driver.find_element(By.XPATH, Board.squares[random_square])
element.click()
Board.markedSquares.append(random_square)
LOGGER.info("First move: clicking square " str(random_square))
def playTTT():
random_square = randint(0,8)
time.sleep(5)
try:
driver.find_element(By.XPATH, Tags.winner).is_displayed()
text = driver.find_element(By.XPATH, Tags.winner).text
LOGGER.info(str(text))
driver.save_screenshot("screenshot.png")
driver.close()
except NoSuchElementException:
pass
try:
driver.find_element(By.XPATH, Tags.tie).is_displayed()
LOGGER.info("Tie")
driver.save_screenshot("screenshot.png")
driver.close()
except NoSuchElementException:
pass
for i in Board.squares:
text = driver.find_element(By.XPATH, Board.squares[i]).text
if text == 'O':
LOGGER.info("Square " str(i) " marked by O.")
Board.markedSquares.append(i)
LOGGER.info("Appending marked squares list: " str(Board.markedSquares))
try:
for i in Board.markedSquares:
if i == random_square:
LOGGER.info("Square number " str(i) " already marked. Recomputing...")
break
else:
element = driver.find_element(By.XPATH, Board.squares[random_square])
element.click()
Board.markedSquares.append(random_square)
LOGGER.info("Clicking square:" str(random_square))
break
LOGGER.info("Contents of markedSquares: " str(Board.markedSquares))
playTTT()
except InvalidSessionIdException:
pass
if __name__=='__main__':
firstMove()
playTTT()
CodePudding user response:
You have to loop through the length of the 'Board.squares
' list, like this:
for i in range(len(Board.squares)):
text = driver.find_element(By.XPATH, Board.squares[i]).text
Output:
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[1]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[2]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[3]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[4]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[5]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[6]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[7]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[8]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[9]
CodePudding user response:
I have a suggestion to improve the readability of your code and simplify it somewhat. You can take that for loop and put it in your last try statement like so:
try:
for i in range(len(squares)):
text = driver.find_element(By.XPATH, squares[random_square]).text
if text == 'O':
LOGGER.info("Square " str(random_square) " marked by O.")
break
elif text == 'X':
LOGGER.info("Square " str(random_square) " already marked by X.")
break
else:
element = driver.find_element(By.XPATH, squares[random_square])
element.click()
LOGGER.info("Clicking square:" str(random_square))
break
playTTT()
Instead of appending to a list of "markedSquares" and checking that list every time before you click on a square, check the random_square for the presence of an X or an O using .text and then use break statements to bust out of the loop and get a new random integer. This way, you won't click on a square that's already marked and you get rid of your second list.
This way you can also get rid of your firstMove method, get rid of the Board class and move the squares list into your main method, playTTT().