menu.pyΒΆ
Part of Tic Tac Toe.
1"""TicTacToe: Main menu with score tracking.
2
3Provides a 2D UI menu that wraps the game, tracking wins/draws across
4multiple rounds. Demonstrates VBoxContainer, Label, Button, and signal-driven
5navigation between menu and gameplay.
6"""
7
8from simvx.core import AnchorPreset, Button, Label, Node, Signal, VBoxContainer
9from simvx.core.math.types import Vec2
10
11
12class ScoreBoard:
13 """Tracks wins/draws for the current session."""
14
15 __slots__ = ("x_wins", "o_wins", "draws")
16
17 def __init__(self):
18 self.x_wins = 0
19 self.o_wins = 0
20 self.draws = 0
21
22 @property
23 def total(self) -> int:
24 return self.x_wins + self.o_wins + self.draws
25
26 def record(self, winner: str | None):
27 if winner == "X":
28 self.x_wins += 1
29 elif winner == "O":
30 self.o_wins += 1
31 else:
32 self.draws += 1
33
34 def summary(self) -> str:
35 return f"X: {self.x_wins} | O: {self.o_wins} | Draws: {self.draws}"
36
37
38# ============================================================================
39# Colours
40# ============================================================================
41
42_ACCENT = (0.3, 0.7, 1.0, 1.0)
43_GOLD = (1.0, 0.85, 0.3, 1.0)
44_BTN_BG = (0.15, 0.25, 0.4, 1.0)
45_BTN_HOVER = (0.2, 0.35, 0.55, 1.0)
46_BTN_PRESSED = (0.1, 0.18, 0.3, 1.0)
47_BTN_BORDER = (0.35, 0.55, 0.8, 1.0)
48_SUBTLE = (0.5, 0.55, 0.65, 1.0)
49
50
51class MainMenu(Node):
52 """Full-screen main menu with Play button, scores, and quit.
53
54 The layout is anchor-driven, so it stays centred at any window size.
55
56 Signals:
57 play_pressed: emitted when the user clicks Play.
58 quit_pressed: emitted when the user clicks Quit.
59 """
60
61 def __init__(self, scores: ScoreBoard, **kw):
62 super().__init__(**kw)
63 self.name = "MainMenu"
64 self.play_pressed = Signal()
65 self.quit_pressed = Signal()
66 self._scores = scores
67
68 def on_ready(self):
69 w = 360
70 h = 430
71
72 root = VBoxContainer(name="MenuLayout")
73 # Horizontally centred, vertically centred on the viewport so the menu
74 # scales with window resize instead of clipping at a fixed 400x550.
75 root.set_anchor_preset(AnchorPreset.CENTER)
76 root.margin_left = -w // 2
77 root.margin_right = w // 2
78 root.margin_top = -h // 2
79 root.margin_bottom = h // 2
80 root.size = Vec2(w, h)
81 root.separation = 14
82
83 # Title
84 title = Label("TIC TAC TOE", name="Title")
85 title.font_size = 32.0
86 title.text_colour = _GOLD
87 title.alignment = "center"
88 title.size = Vec2(w, 44)
89 root.add_child(title)
90
91 # Subtitle
92 sub = Label("Two Player", name="Subtitle")
93 sub.font_size = 16.0
94 sub.text_colour = _ACCENT
95 sub.alignment = "center"
96 sub.size = Vec2(w, 24)
97 root.add_child(sub)
98
99 # Spacer
100 spacer = Label("", name="Spacer1")
101 spacer.size = Vec2(w, 20)
102 root.add_child(spacer)
103
104 # Score display
105 score_label = Label(self._scores.summary(), name="Scores")
106 score_label.font_size = 16.0
107 score_label.text_colour = (0.85, 0.9, 1.0, 1.0)
108 score_label.alignment = "center"
109 score_label.size = Vec2(w, 24)
110 root.add_child(score_label)
111
112 games_label = Label(f"Games played: {self._scores.total}", name="GamesPlayed")
113 games_label.font_size = 12.0
114 games_label.text_colour = _SUBTLE
115 games_label.alignment = "center"
116 games_label.size = Vec2(w, 18)
117 root.add_child(games_label)
118
119 # Spacer
120 spacer2 = Label("", name="Spacer2")
121 spacer2.size = Vec2(w, 30)
122 root.add_child(spacer2)
123
124 # Play button
125 play_btn = Button("Play", name="PlayBtn")
126 play_btn.size = Vec2(w, 50)
127 play_btn.font_size = 22.0
128 play_btn.text_colour = (1.0, 1.0, 1.0, 1.0)
129 play_btn.bg_colour = _BTN_BG
130 play_btn.hover_colour = _BTN_HOVER
131 play_btn.pressed_colour = _BTN_PRESSED
132 play_btn.border_colour = _BTN_BORDER
133 play_btn.pressed.connect(self.play_pressed.emit)
134 root.add_child(play_btn)
135
136 # Spacer
137 spacer3 = Label("", name="Spacer3")
138 spacer3.size = Vec2(w, 8)
139 root.add_child(spacer3)
140
141 # Quit button
142 quit_btn = Button("Quit", name="QuitBtn")
143 quit_btn.size = Vec2(w, 40)
144 quit_btn.font_size = 16.0
145 quit_btn.text_colour = (0.7, 0.7, 0.75, 1.0)
146 quit_btn.bg_colour = (0.12, 0.12, 0.16, 1.0)
147 quit_btn.hover_colour = (0.2, 0.12, 0.14, 1.0)
148 quit_btn.pressed_colour = (0.1, 0.08, 0.1, 1.0)
149 quit_btn.border_colour = (0.3, 0.3, 0.35, 1.0)
150 quit_btn.pressed.connect(self.quit_pressed.emit)
151 root.add_child(quit_btn)
152
153 # Footer
154 footer = Label("Built with SimVX Engine", name="Footer")
155 footer.font_size = 10.0
156 footer.text_colour = (0.35, 0.38, 0.45, 1.0)
157 footer.alignment = "center"
158 footer.size = Vec2(w, 16)
159 root.add_child(footer)
160
161 self.add_child(root)