Tic Tac Toe¶
A classic two-player Tic Tac Toe built entirely with SimVX UI widgets. Demonstrates buttons, grids, labels, signals, and game state management without any custom draw code.
What You Will Learn¶
UI widgets – Build interfaces with
Button,Label,GridContainer,VBoxContainerSignal connections – Wire button presses to game logic with
btn.pressed.connect()GridContainer – Automatic grid layout for the 3x3 board
Dynamic UI updates – Change button text, colours, and label content at runtime
Game reset – Clear and reinitialise UI state
Controls¶
Click any empty cell to place X or O. Click “New Game” to reset.
How It Works¶
TicTacToeGame builds the UI in ready():
A
VBoxContainerholds the title, status label, game grid, and reset buttonA
GridContainerwithcolumns=3contains 9Buttonwidgets for the cellsEach button’s
pressedsignal connects tomake_move(row, col)using a lambdamake_move()places the current player’s mark, updates the button’s text and colour, then calls_check_winner()to scan rows, columns, and diagonalsA win or draw updates the status label and emits the
game_oversignal
Source Code¶
1"""Tic Tac Toe -- UI Widget Game
2
3[Play Demo](/demos/game_tictactoe.html)
4
5A classic two-player Tic Tac Toe built entirely with SimVX UI widgets.
6Demonstrates buttons, grids, labels, signals, and game state management
7without any custom draw code.
8
9## What You Will Learn
10
11- **UI widgets** -- Build interfaces with `Button`, `Label`, `GridContainer`, `VBoxContainer`
12- **Signal connections** -- Wire button presses to game logic with `btn.pressed.connect()`
13- **GridContainer** -- Automatic grid layout for the 3x3 board
14- **Dynamic UI updates** -- Change button text, colours, and label content at runtime
15- **Game reset** -- Clear and reinitialise UI state
16
17## Controls
18
19Click any empty cell to place X or O. Click "New Game" to reset.
20
21## How It Works
22
23`TicTacToeGame` builds the UI in `ready()`:
24
251. A `VBoxContainer` holds the title, status label, game grid, and reset button
262. A `GridContainer` with `columns=3` contains 9 `Button` widgets for the cells
273. Each button's `pressed` signal connects to `make_move(row, col)` using a lambda
284. `make_move()` places the current player's mark, updates the button's text and
29 colour, then calls `_check_winner()` to scan rows, columns, and diagonals
305. A win or draw updates the status label and emits the `game_over` signal
31"""
32
33
34from simvx.core import (
35 Button,
36 GridContainer,
37 Label,
38 Node,
39 Signal,
40 VBoxContainer,
41)
42from simvx.core.math.types import Vec2
43from simvx.core.ui.core import Colour
44from simvx.graphics import App
45
46
47class TicTacToeGame(Node):
48 """Complete Tic Tac Toe game with UI."""
49
50 def __init__(self, **kwargs):
51 super().__init__(**kwargs)
52 self.name = "TicTacToeGame"
53 self.board: list[list[str | None]] = [[None] * 3 for _ in range(3)]
54 self.current_player = "X"
55 self.winner: str | None = None
56 self.game_over = Signal()
57
58 # UI references (set in ready)
59 self.cells: list[list[Button | None]] = [[None] * 3 for _ in range(3)]
60 self.status_label: Label | None = None
61 self.new_game_btn: Button | None = None
62
63 def ready(self):
64 root = VBoxContainer(name="Root")
65 root.position = Vec2(20, 20)
66 root.size = Vec2(360, 440)
67 root.separation = 10
68
69 # Title
70 title = Label("Tic Tac Toe", name="Title")
71 title.font_size = 24.0
72 title.text_colour = Colour.WHITE
73 title.alignment = "center"
74 title.size = Vec2(360, 36)
75 root.add_child(title)
76
77 # Status
78 self.status_label = Label("Player X's turn", name="Status")
79 self.status_label.font_size = 16.0
80 self.status_label.text_colour = (0.7, 0.9, 1.0, 1.0)
81 self.status_label.alignment = "center"
82 self.status_label.size = Vec2(360, 24)
83 root.add_child(self.status_label)
84
85 # Grid
86 grid = GridContainer(columns=3, name="Grid")
87 grid.size = Vec2(360, 330)
88 grid.separation = 6
89
90 for row in range(3):
91 for col in range(3):
92 btn = Button("", name=f"Cell_{row}_{col}")
93 btn.size = Vec2(110, 100)
94 btn.font_size = 36.0
95 btn.bg_colour = (0.15, 0.15, 0.2, 1.0)
96 btn.hover_colour = (0.25, 0.25, 0.35, 1.0)
97 btn.pressed_colour = (0.1, 0.1, 0.15, 1.0)
98 btn.border_colour = (0.4, 0.4, 0.5, 1.0)
99 r, c = row, col
100 btn.pressed.connect(lambda r=r, c=c: self.make_move(r, c))
101 grid.add_child(btn)
102 self.cells[row][col] = btn
103
104 root.add_child(grid)
105
106 # New Game button
107 self.new_game_btn = Button("New Game", name="NewGame")
108 self.new_game_btn.size = Vec2(360, 35)
109 self.new_game_btn.bg_colour = (0.2, 0.5, 0.3, 1.0)
110 self.new_game_btn.hover_colour = (0.3, 0.6, 0.4, 1.0)
111 self.new_game_btn.pressed.connect(self.reset)
112 root.add_child(self.new_game_btn)
113
114 self.add_child(root)
115
116 def make_move(self, row: int, col: int) -> bool:
117 """Place current player's mark. Returns True if move was valid."""
118 if self.winner is not None or self.board[row][col] is not None:
119 return False
120 self.board[row][col] = self.current_player
121 btn = self.cells[row][col]
122 btn.text = self.current_player
123 btn.text_colour = (0.3, 0.8, 1.0, 1.0) if self.current_player == "X" else (1.0, 0.4, 0.4, 1.0)
124 self._check_winner()
125 if self.winner is None:
126 self.current_player = "O" if self.current_player == "X" else "X"
127 self.status_label.text = f"Player {self.current_player}'s turn"
128 return True
129
130 def _check_winner(self):
131 b = self.board
132 lines = [
133 # Rows
134 [(0, 0), (0, 1), (0, 2)],
135 [(1, 0), (1, 1), (1, 2)],
136 [(2, 0), (2, 1), (2, 2)],
137 # Columns
138 [(0, 0), (1, 0), (2, 0)],
139 [(0, 1), (1, 1), (2, 1)],
140 [(0, 2), (1, 2), (2, 2)],
141 # Diagonals
142 [(0, 0), (1, 1), (2, 2)],
143 [(0, 2), (1, 1), (2, 0)],
144 ]
145 for line in lines:
146 vals = [b[r][c] for r, c in line]
147 if vals[0] is not None and vals[0] == vals[1] == vals[2]:
148 self.winner = vals[0]
149 self.status_label.text = f"Player {self.winner} wins!"
150 self.status_label.text_colour = (0.2, 1.0, 0.4, 1.0)
151 self.game_over()
152 return
153 # Check draw
154 if all(b[r][c] is not None for r in range(3) for c in range(3)):
155 self.winner = "draw"
156 self.status_label.text = "It's a draw!"
157 self.status_label.text_colour = (1.0, 1.0, 0.4, 1.0)
158 self.game_over()
159
160 def reset(self):
161 """Reset the board for a new game."""
162 self.board = [[None] * 3 for _ in range(3)]
163 self.current_player = "X"
164 self.winner = None
165 for row in range(3):
166 for col in range(3):
167 self.cells[row][col].text = ""
168 self.status_label.text = f"Player {self.current_player}'s turn"
169 self.status_label.text_colour = (0.7, 0.9, 1.0, 1.0)
170
171
172_SCREEN_W = 400
173_SCREEN_H = 550
174
175
176class TicTacToeApp(Node):
177 """Root node: manages menu and game scenes, tracks scores.
178
179 State machine:
180 menu → game → result → menu (loop)
181 """
182
183 def __init__(self, **kw):
184 super().__init__(**kw)
185 self.name = "TicTacToeApp"
186 self.scores = None
187 self.state = "menu" # "menu" | "game" | "result"
188 self.menu = None
189 self.game: TicTacToeGame | None = None
190 self._result_ui: Node | None = None
191
192 def ready(self):
193 from menu import ScoreBoard
194
195 self.scores = ScoreBoard()
196 self._show_menu()
197
198 def _show_menu(self):
199 from menu import MainMenu
200
201 self.state = "menu"
202 self._clear_game()
203 self._clear_result()
204 self.menu = MainMenu(self.scores, _SCREEN_W, _SCREEN_H)
205 self.menu.play_pressed.connect(self._start_game)
206 self.menu.quit_pressed.connect(self._quit)
207 self.add_child(self.menu)
208
209 def _start_game(self):
210 self.state = "game"
211 if self.menu:
212 self.menu.destroy()
213 self.menu = None
214 self.game = TicTacToeGame()
215 self.game.game_over.connect(self._on_game_over)
216 self.add_child(self.game)
217
218 def _on_game_over(self):
219 if not self.game:
220 return
221 self.scores.record(self.game.winner if self.game.winner != "draw" else None)
222 self.state = "result"
223 self._show_result_overlay()
224
225 def _show_result_overlay(self):
226 self._result_ui = Node(name="ResultOverlay")
227
228 layout = VBoxContainer(name="ResultLayout")
229 layout.position = Vec2((_SCREEN_W - 360) // 2, _SCREEN_H // 2 - 30)
230 layout.size = Vec2(360, 140)
231 layout.separation = 10
232
233 # Play Again
234 again = Button("Play Again", name="PlayAgain")
235 again.size = Vec2(360, 45)
236 again.font_size = 18.0
237 again.bg_colour = (0.15, 0.35, 0.2, 1.0)
238 again.hover_colour = (0.2, 0.45, 0.3, 1.0)
239 again.pressed_colour = (0.1, 0.25, 0.15, 1.0)
240 again.border_colour = (0.3, 0.6, 0.4, 1.0)
241 again.pressed.connect(self._play_again)
242 layout.add_child(again)
243
244 # Back to Menu
245 back = Button("Back to Menu", name="BackToMenu")
246 back.size = Vec2(360, 40)
247 back.font_size = 16.0
248 back.bg_colour = (0.15, 0.15, 0.2, 1.0)
249 back.hover_colour = (0.2, 0.2, 0.3, 1.0)
250 back.pressed_colour = (0.1, 0.1, 0.15, 1.0)
251 back.border_colour = (0.3, 0.3, 0.4, 1.0)
252 back.pressed.connect(self._show_menu)
253 layout.add_child(back)
254
255 self._result_ui.add_child(layout)
256 self.add_child(self._result_ui)
257
258 def _play_again(self):
259 self._clear_result()
260 self._clear_game()
261 self._start_game()
262
263 def _clear_game(self):
264 if self.game:
265 self.game.destroy()
266 self.game = None
267
268 def _clear_result(self):
269 if self._result_ui:
270 self._result_ui.destroy()
271 self._result_ui = None
272
273 def _quit(self):
274 import sys as _sys
275
276 _sys.exit(0)
277
278
279if __name__ == "__main__":
280 App(title="Tic Tac Toe", width=_SCREEN_W, height=_SCREEN_H).run(TicTacToeApp())