patternpythonMinor
Recursive Tic-Tac-Toe Solver in Python
Viewed 0 times
toetictacrecursivepythonsolver
Problem
I'm implementing a Tic-Tac-Toe solver in Python. It contains two functions:
The end goal is to use this program to construct a lookup table that will be used by a JavaScript Tic-Tac-Toe frontend. But I want this to be a well-documented, freestanding piece of code.
```
import unittest
import enum
import random
class Result(enum.Enum):
lose = -1
draw = 0
win = 1
def assess(board):
"""
Takes: a board string.
Returns: 'x' or 'o' if either of them win, '?' for draw, else None.
"""
# Draw? (no spaces left?)
if '-' not in board:
return '?'
for sym in 'x', 'o':
winning_three = [sym] * 3
# Vertical win?
if winning_three in (
[board[0], board[3], board[6]],
[board[1], board[4], board[7]],
[board[2], board[5], board[8]],
): return sym
# Vertical win?
if winning_three in (
[board[0], board[1], board[2]],
[board[3], board[4], board[5]],
[board[6]
assess to determine whether a given board position is a win for either side, and is_winner to traverse the game tree.The end goal is to use this program to construct a lookup table that will be used by a JavaScript Tic-Tac-Toe frontend. But I want this to be a well-documented, freestanding piece of code.
- Are the tests comprehensive and self-documenting?
- Is the use of
Enumwarranted and well implemented?
- Do the docstrings make the functions' API clear? Is it a good idea to use
Nonein a non-exceptional case, as I do inassess?
- I agonized for a long time about this, but in the first part of
is_winnerI callassessthree times instead of saving the result. I thought it would be more clear -- is it?
- When making a move I need to convert the board string to a list, mutate it, and reconvert to a string to recurse on. I do this because most of the time it seems to me convenient to have
boardas a string, but now I'm questioning that decision.
- (Least importantly) Any optimizations that could be made? Right now the code takes a noticeable amount of time to determine that the game is a draw (starting from an empty board).
```
import unittest
import enum
import random
class Result(enum.Enum):
lose = -1
draw = 0
win = 1
def assess(board):
"""
Takes: a board string.
Returns: 'x' or 'o' if either of them win, '?' for draw, else None.
"""
# Draw? (no spaces left?)
if '-' not in board:
return '?'
for sym in 'x', 'o':
winning_three = [sym] * 3
# Vertical win?
if winning_three in (
[board[0], board[3], board[6]],
[board[1], board[4], board[7]],
[board[2], board[5], board[8]],
): return sym
# Vertical win?
if winning_three in (
[board[0], board[1], board[2]],
[board[3], board[4], board[5]],
[board[6]
Solution
Overall I think this is good.
Some issues:
And some suggestions:
Some issues:
assesswill return?even if someone wins on the last move. You should do the empty check after you check for any wins.
- Your comments in
asseshas# Vertical win?twice. The second should be# Horizontal win?
And some suggestions:
- There is no reason to keep track of all moves that result in a draw, you only ever use one so it is better to just keep the first.
- It is considered bad style to have one-line code blocks, such as one-line
iftests orforloops (ignoring ternary expressions and list comprehensions, of course). For things likeiftests andforloops, put thefororifon one line and the stuff after the:on the next line.
- You convert back and forth between a
strandlista lot, but you never use any string features. I think it would be better to just keep it a list all the time. This will also improve performance. This would also allow you to use slices for the wins.
- You only need to asses the player that moved last, so you can specify a player in
assessand just test that one.
- I would do
if assess(board) is None: pass(on separate lines) so you can reduce the nesting level.
- I would split the winning test portion of
is_winnerinto a separate function and have the recursive portion call that function. And again, you only need to check the player who moved last.
- An
enumis valid in this case, but it seems a bit overkill. Everywhere else you use strings, so I would do the same.
- I would use a
dictto handle the result ofassessinis_winner.
- I would have just one big
winlist, rather than three.
Context
StackExchange Code Review Q#93380, answer score: 5
Revisions (0)
No revisions yet.