Skip to content

Commit 757eec8

Browse files
committed
Project 22 solution
1 parent 385e76b commit 757eec8

File tree

2 files changed

+155
-14
lines changed

2 files changed

+155
-14
lines changed

22_itictactoe/itictactoe.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Author : Luke McGuire <[email protected]>
4+
Date : 2024-07-06
5+
Purpose: Interactive Tic-Tac-Toe
6+
"""
7+
8+
import os
9+
from typing import List, NamedTuple, Optional
10+
11+
12+
# --------------------------------------------------
13+
class State(NamedTuple):
14+
"""Holds the state information for the Tic-Tac-Toe game"""
15+
16+
board: List[str] = list("." * 9)
17+
player: str = "X"
18+
quit: bool = False
19+
draw: bool = False
20+
error: Optional[str] = None
21+
winner: Optional[str] = None
22+
23+
24+
# --------------------------------------------------
25+
def clear() -> None:
26+
"""Clear the terminal screen"""
27+
if os.name == "nt":
28+
os.system("cls")
29+
else:
30+
os.system("clear")
31+
32+
33+
# --------------------------------------------------
34+
def get_move(state: State) -> State:
35+
"""Get the next move from user input"""
36+
37+
player = state.player
38+
cell = input(f"Player {player} What is your move? [q to quit]: ")
39+
40+
if cell == "q":
41+
return state._replace(quit=True)
42+
43+
if not (cell.isdigit() and int(cell) in range(1, 10)):
44+
return state._replace(error=f'Invalid cell "{cell}", please use 1-9')
45+
46+
board = state.board
47+
48+
idx = int(cell) - 1
49+
if board[idx] != ".":
50+
return state._replace(error=f'Cell "{cell}" already taken')
51+
52+
board[idx] = player
53+
return state._replace(
54+
board=board,
55+
player="O" if player == "X" else "X",
56+
winner=find_winner(board),
57+
draw="." not in board,
58+
)
59+
60+
61+
# --------------------------------------------------
62+
def format_board(board: List[str]) -> str:
63+
"""Formats the board string as a tic-tac-toe grid"""
64+
65+
row_divider = "-------------"
66+
cells = [str(i) if c == "." else c for i, c in enumerate(board, start=1)]
67+
68+
cells_templ = "| {} | {} | {} |"
69+
70+
return "\n".join(
71+
[
72+
row_divider,
73+
cells_templ.format(*cells[:3]),
74+
row_divider,
75+
cells_templ.format(*cells[3:6]),
76+
row_divider,
77+
cells_templ.format(*cells[6:9]),
78+
row_divider,
79+
]
80+
)
81+
82+
83+
# --------------------------------------------------
84+
def find_winner(board: List[str]) -> Optional[str]:
85+
"""Determine if there is a winner"""
86+
winning = [
87+
[0, 1, 2],
88+
[3, 4, 5],
89+
[6, 7, 8],
90+
[0, 3, 6],
91+
[1, 4, 7],
92+
[2, 5, 8],
93+
[0, 4, 8],
94+
[2, 4, 6],
95+
]
96+
97+
for player in ["X", "O"]:
98+
for i, j, k in winning:
99+
combo = [board[i], board[j], board[k]]
100+
if combo == [player, player, player]:
101+
return player
102+
103+
return None
104+
105+
106+
# --------------------------------------------------
107+
def main() -> None:
108+
"""Make a jazz noise here"""
109+
110+
state = State()
111+
112+
while True:
113+
clear()
114+
print(format_board(state.board))
115+
116+
if state.error:
117+
print(state.error)
118+
state = state._replace(error=None)
119+
elif state.winner:
120+
print(f"{state.winner} has won!")
121+
break
122+
elif state.draw:
123+
print("All right, we'll call it a draw.")
124+
break
125+
126+
state = get_move(state)
127+
if state.quit:
128+
print("You lose, loser!")
129+
break
130+
131+
132+
# --------------------------------------------------
133+
if __name__ == "__main__":
134+
main()

22_itictactoe/unit.py

+21-14
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_board_no_state():
1616
-------------
1717
""".strip()
1818

19-
assert format_board('.' * 9) == board
19+
assert format_board(list("." * 9)) == board
2020

2121

2222
# --------------------------------------------------
@@ -33,36 +33,43 @@ def test_board_with_state():
3333
-------------
3434
""".strip()
3535

36-
assert format_board('...OXX...') == board
36+
assert format_board(list("...OXX...")) == board
3737

3838

3939
# --------------------------------------------------
4040
def test_winning():
4141
"""test winning states"""
4242

43-
wins = [('PPP......'), ('...PPP...'), ('......PPP'), ('P..P..P..'),
44-
('.P..P..P.'), ('..P..P..P'), ('P...P...P'), ('..P.P.P..')]
43+
wins = [
44+
("PPP......"),
45+
("...PPP..."),
46+
("......PPP"),
47+
("P..P..P.."),
48+
(".P..P..P."),
49+
("..P..P..P"),
50+
("P...P...P"),
51+
("..P.P.P.."),
52+
]
4553

46-
for player in 'XO':
47-
other_player = 'O' if player == 'X' else 'X'
54+
for player in "XO":
55+
other_player = "O" if player == "X" else "X"
4856

4957
for state in wins:
50-
state = state.replace('P', player)
51-
dots = [i for i in range(len(state)) if state[i] == '.']
58+
state = state.replace("P", player)
59+
dots = [i for i in range(len(state)) if state[i] == "."]
5260
mut = random.sample(dots, k=2)
53-
test_state = ''.join([
54-
other_player if i in mut else state[i]
55-
for i in range(len(state))
56-
])
61+
test_state = "".join(
62+
[other_player if i in mut else state[i] for i in range(len(state))]
63+
)
5764
assert find_winner(test_state) == player
5865

5966

6067
# --------------------------------------------------
6168
def test_losing():
6269
"""test losing states"""
6370

64-
losing_state = list('XXOO.....')
71+
losing_state = list("XXOO.....")
6572

6673
for i in range(10):
6774
random.shuffle(losing_state)
68-
assert find_winner(''.join(losing_state)) == None
75+
assert find_winner("".join(losing_state)) == None

0 commit comments

Comments
 (0)