Skip to content

Commit 658b8b0

Browse files
authored
Merge pull request #266 from realpython/python-tic-tac-toe-game-tkinter
Create the Tic-tac-toe SbSP final code
2 parents d59c019 + e185df8 commit 658b8b0

File tree

7 files changed

+887
-0
lines changed

7 files changed

+887
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Build a Tic-Tac-Toe Game With Python and Tkinter
2+
3+
A tic-tac-toe game built with Python and Tkinter. It allows two players to play a tic-tac-toe game on a shared device.
4+
5+
## Installation
6+
7+
The game uses Tkinter to build its user interface. Since Tkinter is part of the Python standard library, you don't need to install anything to run the game. Just make sure that you have a version of Tkinter greater than 8.5.
8+
9+
## Run the Game
10+
11+
To run the game, just execute the following command on your command line:
12+
13+
```sh
14+
$ python tic_tac_toe.py
15+
```
16+
17+
## About the Author
18+
19+
Leodanis Pozo Ramos - Email: [email protected]
20+
21+
## License
22+
23+
Distributed under the MIT license. See `LICENSE` in the root directory of this `materials` repo for more information.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""A tic-tac-toe game built with Python and Tkinter."""
2+
3+
import tkinter as tk
4+
from itertools import cycle
5+
from tkinter import font
6+
from typing import NamedTuple
7+
8+
9+
class Player(NamedTuple):
10+
label: str
11+
color: str
12+
13+
14+
class Move(NamedTuple):
15+
row: int
16+
col: int
17+
label: str = ""
18+
19+
20+
BOARD_SIZE = 3
21+
DEFAULT_PLAYERS = (
22+
Player(label="X", color="blue"),
23+
Player(label="O", color="green"),
24+
)
25+
26+
27+
class TicTacToeGame:
28+
def __init__(self, players=DEFAULT_PLAYERS, board_size=BOARD_SIZE):
29+
self._players = cycle(players)
30+
self.board_size = board_size
31+
self.current_player = next(self._players)
32+
self.winner_combo = []
33+
self._current_moves = []
34+
self._has_winner = False
35+
self._winning_combos = []
36+
self._setup_board()
37+
38+
def _setup_board(self):
39+
"""Set the board to its initial state."""
40+
self._current_moves = [
41+
[Move(row, col) for col in range(self.board_size)]
42+
for row in range(self.board_size)
43+
]
44+
self._winning_combos = self._get_winning_combos()
45+
46+
def _get_winning_combos(self):
47+
rows = [
48+
[(move.row, move.col) for move in row]
49+
for row in self._current_moves
50+
]
51+
columns = [list(col) for col in zip(*rows)]
52+
first_diagonal = [row[i] for i, row in enumerate(rows)]
53+
second_diagonal = [col[j] for j, col in enumerate(reversed(columns))]
54+
return rows + columns + [first_diagonal, second_diagonal]
55+
56+
def is_valid_move(self, move):
57+
"""Return True if move is valid, and False otherwise."""
58+
row, col = move.row, move.col
59+
move_was_not_played = self._current_moves[row][col].label == ""
60+
no_winner = not self._has_winner
61+
return no_winner and move_was_not_played
62+
63+
def process_move(self, move):
64+
"""Process the current move and check if it's a win."""
65+
row, col = move.row, move.col
66+
self._current_moves[row][col] = move
67+
for combo in self._winning_combos:
68+
results = set(self._current_moves[n][m].label for n, m in combo)
69+
is_win = (len(results) == 1) and ("" not in results)
70+
if is_win:
71+
self._has_winner = True
72+
self.winner_combo = combo
73+
break
74+
75+
def has_winner(self):
76+
"""Return True if the game has a winner, and False otherwise."""
77+
return self._has_winner
78+
79+
def is_tied(self):
80+
"""Return True if the game is tied, and False otherwise."""
81+
no_winner = not self._has_winner
82+
played_moves = (
83+
move.label for row in self._current_moves for move in row
84+
)
85+
return no_winner and all(played_moves)
86+
87+
def toggle_player(self):
88+
"""Return a toggled player."""
89+
self.current_player = next(self._players)
90+
91+
def reset_game(self):
92+
"""Reset the game state to play again."""
93+
for row, row_content in enumerate(self._current_moves):
94+
for col, _ in enumerate(row_content):
95+
row_content[col] = Move(row, col)
96+
self._has_winner = False
97+
self.winner_combo = []
98+
99+
100+
class TicTacToeBoard(tk.Tk):
101+
def __init__(self, game):
102+
super().__init__()
103+
self.title("Tic-Tac-Toe Game")
104+
self._cells = {}
105+
self._game = game
106+
self._create_menu()
107+
self._create_board_display()
108+
self._create_board_grid()
109+
110+
def _create_menu(self):
111+
menu_bar = tk.Menu(master=self)
112+
self.config(menu=menu_bar)
113+
file_menu = tk.Menu(master=menu_bar)
114+
file_menu.add_command(label="Play Again", command=self.reset_board)
115+
file_menu.add_separator()
116+
file_menu.add_command(label="Exit", command=quit)
117+
menu_bar.add_cascade(label="File", menu=file_menu)
118+
119+
def _create_board_display(self):
120+
display_frame = tk.Frame(master=self)
121+
display_frame.pack(fill=tk.X)
122+
self.display = tk.Label(
123+
master=display_frame,
124+
text="Ready?",
125+
font=font.Font(size=28, weight="bold"),
126+
)
127+
self.display.pack()
128+
129+
def _create_board_grid(self):
130+
grid_frame = tk.Frame(master=self)
131+
grid_frame.pack()
132+
for row in range(self._game.board_size):
133+
self.rowconfigure(row, weight=1, minsize=50)
134+
self.columnconfigure(row, weight=1, minsize=75)
135+
for col in range(self._game.board_size):
136+
button = tk.Button(
137+
master=grid_frame,
138+
text="",
139+
font=font.Font(size=36, weight="bold"),
140+
fg="black",
141+
width=3,
142+
height=2,
143+
highlightbackground="lightblue",
144+
)
145+
self._cells[button] = (row, col)
146+
button.bind("<ButtonPress-1>", self.play)
147+
button.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
148+
149+
def play(self, event):
150+
"""Handle a player's move."""
151+
clicked_btn = event.widget
152+
row, col = self._cells[clicked_btn]
153+
move = Move(row, col, self._game.current_player.label)
154+
if self._game.is_valid_move(move):
155+
self._update_button(clicked_btn)
156+
self._game.process_move(move)
157+
if self._game.is_tied():
158+
self._update_display(msg="Tied game!", color="red")
159+
elif self._game.has_winner():
160+
self._highlight_cells()
161+
msg = f'Player "{self._game.current_player.label}" won!'
162+
color = self._game.current_player.color
163+
self._update_display(msg, color)
164+
else:
165+
self._game.toggle_player()
166+
msg = f"{self._game.current_player.label}'s turn"
167+
self._update_display(msg)
168+
169+
def _update_button(self, clicked_btn):
170+
clicked_btn.config(text=self._game.current_player.label)
171+
clicked_btn.config(fg=self._game.current_player.color)
172+
173+
def _update_display(self, msg, color="black"):
174+
self.display["text"] = msg
175+
self.display["fg"] = color
176+
177+
def _highlight_cells(self):
178+
for button, coordinates in self._cells.items():
179+
if coordinates in self._game.winner_combo:
180+
button.config(highlightbackground="red")
181+
182+
def reset_board(self):
183+
"""Reset the game's board to play again."""
184+
self._game.reset_game()
185+
self._update_display(msg="Ready?")
186+
for button in self._cells.keys():
187+
button.config(highlightbackground="lightblue")
188+
button.config(text="")
189+
button.config(fg="black")
190+
191+
192+
def main():
193+
"""Create the game's board and run its main loop."""
194+
game = TicTacToeGame()
195+
board = TicTacToeBoard(game)
196+
board.mainloop()
197+
198+
199+
if __name__ == "__main__":
200+
main()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""A tic-tac-toe game built with Python and Tkinter."""
2+
3+
import tkinter as tk
4+
from tkinter import font
5+
6+
7+
class TicTacToeBoard(tk.Tk):
8+
def __init__(self):
9+
super().__init__()
10+
self.title("Tic-Tac-Toe Game")
11+
self._cells = {}
12+
self._create_board_display()
13+
self._create_board_grid()
14+
15+
def _create_board_display(self):
16+
display_frame = tk.Frame(master=self)
17+
display_frame.pack(fill=tk.X)
18+
self.display = tk.Label(
19+
master=display_frame,
20+
text="Ready?",
21+
font=font.Font(size=28, weight="bold"),
22+
)
23+
self.display.pack()
24+
25+
def _create_board_grid(self):
26+
grid_frame = tk.Frame(master=self)
27+
grid_frame.pack()
28+
for row in range(3):
29+
self.rowconfigure(row, weight=1, minsize=50)
30+
self.columnconfigure(row, weight=1, minsize=75)
31+
for col in range(3):
32+
button = tk.Button(
33+
master=grid_frame,
34+
text="",
35+
font=font.Font(size=36, weight="bold"),
36+
fg="black",
37+
width=3,
38+
height=2,
39+
highlightbackground="lightblue",
40+
)
41+
self._cells[button] = (row, col)
42+
button.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
43+
44+
45+
def main():
46+
"""Create the game's board and run its main loop."""
47+
board = TicTacToeBoard()
48+
board.mainloop()
49+
50+
51+
if __name__ == "__main__":
52+
main()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""A tic-tac-toe game built with Python and Tkinter."""
2+
3+
import tkinter as tk
4+
from itertools import cycle
5+
from tkinter import font
6+
from typing import NamedTuple
7+
8+
9+
class Player(NamedTuple):
10+
label: str
11+
color: str
12+
13+
14+
class Move(NamedTuple):
15+
row: int
16+
col: int
17+
label: str = ""
18+
19+
20+
BOARD_SIZE = 3
21+
DEFAULT_PLAYERS = (
22+
Player(label="X", color="blue"),
23+
Player(label="O", color="green"),
24+
)
25+
26+
27+
class TicTacToeGame:
28+
def __init__(self, players=DEFAULT_PLAYERS, board_size=BOARD_SIZE):
29+
self._players = cycle(players)
30+
self.board_size = board_size
31+
self.current_player = next(self._players)
32+
self.winner_combo = []
33+
self._current_moves = []
34+
self._has_winner = False
35+
self._winning_combos = []
36+
self._setup_board()
37+
38+
def _setup_board(self):
39+
"""Set the board to its initial state."""
40+
self._current_moves = [
41+
[Move(row, col) for col in range(self.board_size)]
42+
for row in range(self.board_size)
43+
]
44+
self._winning_combos = self._get_winning_combos()
45+
46+
def _get_winning_combos(self):
47+
rows = [
48+
[(move.row, move.col) for move in row]
49+
for row in self._current_moves
50+
]
51+
columns = [list(col) for col in zip(*rows)]
52+
first_diagonal = [row[i] for i, row in enumerate(rows)]
53+
second_diagonal = [col[j] for j, col in enumerate(reversed(columns))]
54+
return rows + columns + [first_diagonal, second_diagonal]
55+
56+
57+
class TicTacToeBoard(tk.Tk):
58+
def __init__(self):
59+
super().__init__()
60+
self.title("Tic-Tac-Toe Game")
61+
self._cells = {}
62+
self._create_board_display()
63+
self._create_board_grid()
64+
65+
def _create_board_display(self):
66+
display_frame = tk.Frame(master=self)
67+
display_frame.pack(fill=tk.X)
68+
self.display = tk.Label(
69+
master=display_frame,
70+
text="Ready?",
71+
font=font.Font(size=28, weight="bold"),
72+
)
73+
self.display.pack()
74+
75+
def _create_board_grid(self):
76+
grid_frame = tk.Frame(master=self)
77+
grid_frame.pack()
78+
for row in range(3):
79+
self.rowconfigure(row, weight=1, minsize=50)
80+
self.columnconfigure(row, weight=1, minsize=75)
81+
for col in range(3):
82+
button = tk.Button(
83+
master=grid_frame,
84+
text="",
85+
font=font.Font(size=36, weight="bold"),
86+
fg="black",
87+
width=3,
88+
height=2,
89+
highlightbackground="lightblue",
90+
)
91+
self._cells[button] = (row, col)
92+
button.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
93+
94+
95+
def main():
96+
"""Create the game's board and run its main loop."""
97+
board = TicTacToeBoard()
98+
board.mainloop()
99+
100+
101+
if __name__ == "__main__":
102+
main()

0 commit comments

Comments
 (0)