[MouseFox logo]
The MouseFox Project
Join The Community Discord Server
[Discord logo]
Edit on GitHub

mousefox.examples.tictactoe

Example Tic-tac-toe game for MouseFox.

  1"""Example Tic-tac-toe game for MouseFox."""
  2
  3from typing import Optional
  4import arrow
  5import copy
  6import json
  7import random
  8import pgnet
  9from pgnet import Packet, Response, Status
 10import kvex as kx
 11
 12
 13WINNING_LINES = [
 14    (0, 1, 2),
 15    (3, 4, 5),
 16    (6, 7, 8),
 17    (0, 3, 6),
 18    (1, 4, 7),
 19    (2, 5, 8),
 20    (0, 4, 8),
 21    (2, 4, 6),
 22]
 23BLANK_DATA = dict(
 24    board=[""] * 9,
 25    players=[],
 26    x_turn=True,
 27    in_progress=False,
 28)
 29BOT_NAME = "Tictactoe Bot"
 30BOT_THINK_TIME = 1
 31
 32
 33class Game(pgnet.Game):
 34    """Tic-tac-toe game logic."""
 35
 36    def __init__(self, *args, save_string: Optional[str] = None, **kwargs):
 37        """Override base method."""
 38        super().__init__(*args, **kwargs)
 39        data = json.loads(save_string or json.dumps(BLANK_DATA))
 40        self._next_bot_turn = arrow.now()
 41        self.board: list[str] = data["board"]
 42        self.players: list[str] = data["players"]
 43        self.x_turn: bool = data["x_turn"]
 44        self.outcome: str = "Waiting for players."
 45        if data["in_progress"]:
 46            self.outcome = "In progress."
 47        self.in_progress: bool = data["in_progress"]
 48        self.commands = dict(
 49            single_player=self._set_single_player,
 50            play_square=self._play_square,
 51        )
 52
 53    @property
 54    def persistent(self):
 55        """Override base property."""
 56        return self.in_progress and any(self.board) and BOT_NAME not in self.players
 57
 58    def get_save_string(self) -> str:
 59        """Override base method."""
 60        data = dict(
 61            board=self.board,
 62            players=self.players,
 63            x_turn=self.x_turn,
 64            in_progress=self.in_progress,
 65        )
 66        return json.dumps(data)
 67
 68    def user_joined(self, player: str):
 69        """Override base method."""
 70        if player not in self.players:
 71            self.players.append(player)
 72        if len(self.players) == 2:
 73            random.shuffle(self.players)
 74            self.in_progress = True
 75            self.outcome = "In progress."
 76
 77    def user_left(self, player: str):
 78        """Override base method."""
 79        if player in self.players[2:]:
 80            self.players.remove(player)
 81
 82    def handle_game_packet(self, packet: Packet) -> Response:
 83        """Override base method."""
 84        meth = self.commands.get(packet.message)
 85        if not meth:
 86            return Response("No such command.")
 87        return meth(packet)
 88
 89    # Logic
 90    @property
 91    def _state_hash(self) -> str:
 92        data = [
 93            str(self.board),
 94            str(self.players),
 95            str(self.x_turn),
 96        ]
 97        final = hash(tuple(data))
 98        return final
 99
100    @property
101    def _current_username(self) -> str:
102        if not self.in_progress:
103            return ""
104        return self.players[int(self.x_turn)]
105
106    def _winning_line(
107        self,
108        board: Optional[list[str]] = None,
109    ) -> Optional[tuple[int, int, int]]:
110        if board is None:
111            board = self.board
112        for a, b, c in WINNING_LINES:
113            mark = board[a]
114            if mark and mark == board[b] == board[c]:
115                return a, b, c
116        return None
117
118    def _check_progress(self):
119        winning_line = self._winning_line()
120        if winning_line:
121            mark = self.board[winning_line[0]]
122            self.in_progress = False
123            self.outcome = (f"{self._mark_to_username(mark)} playing as {mark} wins!")
124            return
125        if all(self.board):
126            self.in_progress = False
127            self.outcome = "Draw."
128
129    def _mark_to_username(self, mark: str):
130        assert mark
131        return self.players[0 if mark == "O" else 1]
132
133    def _username_to_mark(self, username: str):
134        assert username in self.players[:2]
135        return "O" if username == self.players[0] else "X"
136
137    def update(self):
138        """Override base method."""
139        if self._current_username != BOT_NAME:
140            return
141        if arrow.now() <= self._next_bot_turn:
142            return
143        my_mark = self._username_to_mark(BOT_NAME)
144        enemy_player_idx = int(not bool(self.players.index(self._current_username)))
145        enemy_mark = self._username_to_mark(self.players[enemy_player_idx])
146        empty_squares = [s for s in range(9) if not self.board[s]]
147        random.shuffle(empty_squares)
148        # Find winning moves
149        for s in empty_squares:
150            new_board = copy.copy(self.board)
151            new_board[s] = my_mark
152            if self._winning_line(new_board):
153                self._do_play_square(s, my_mark)
154                return
155        # Find losing threats
156        for s in empty_squares:
157            new_board = copy.copy(self.board)
158            new_board[s] = enemy_mark
159            if self._winning_line(new_board):
160                break
161        self._do_play_square(s, my_mark)
162
163    # Commands
164    def handle_heartbeat(self, packet: Packet) -> Response:
165        """Override base method."""
166        state_hash = self._state_hash
167        client_hash = packet.payload.get("state_hash")
168        if client_hash == state_hash:
169            return Response("Up to date.", dict(state_hash=state_hash))
170        payload = dict(
171            state_hash=state_hash,
172            players=self.players,
173            board=self.board,
174            your_turn=packet.username == self._current_username,
175            info=self._get_user_info(packet.username),
176            in_progress=self.in_progress,
177            winning_line=self._winning_line(),
178        )
179        return Response("Updated state.", payload)
180
181    def _set_single_player(self, packet: Packet) -> Response:
182        if len(self.players) >= 2:
183            return Response("Game has already started.", status=Status.UNEXPECTED)
184        self.user_joined(BOT_NAME)
185        self._next_bot_turn = arrow.now().shift(seconds=BOT_THINK_TIME)
186        return Response("Started single player mode.")
187
188    def _play_square(self, packet: Packet) -> Response:
189        username = self._current_username
190        if packet.username != username or username == BOT_NAME:
191            return Response("Not your turn.", status=Status.UNEXPECTED)
192        square = int(packet.payload["square"])
193        if self.board[square]:
194            return Response("Square is already marked.", status=Status.UNEXPECTED)
195        self._do_play_square(square, self._username_to_mark(username))
196        return Response("Marked square.")
197
198    def _do_play_square(self, square: int, mark: str, /):
199        self.board[square] = mark
200        self.x_turn = not self.x_turn
201        self._check_progress()
202        self._next_bot_turn = arrow.now().shift(seconds=BOT_THINK_TIME)
203
204    def _get_user_info(self, username: str) -> str:
205        if not self.in_progress:
206            return self.outcome
207        current_username = self._current_username
208        if username not in self.players[:2]:
209            mark = self._username_to_mark(current_username)
210            return f"{self.outcome}\nSpectating {current_username}'s turn as {mark}"
211        turn = "Your turn" if username == current_username else "Awaiting turn"
212        return f"{turn}, playing as: {self._username_to_mark(username)}"
213
214
215class GameWidget(kx.XFrame):
216    """Tic-tac-toe GUI widget."""
217
218    def __init__(self, client: pgnet.Client, **kwargs):
219        """Override base method."""
220        super().__init__(**kwargs)
221        self.client = client
222        self._make_widgets()
223        self.game_state = dict(state_hash=None)
224        client.on_heartbeat = self.on_heartbeat
225        client.heartbeat_payload = self.heartbeat_payload
226
227    def on_subtheme(self, *args, **kwargs):
228        """Override base method."""
229        super().on_subtheme(*args, **kwargs)
230        self._refresh_widgets()
231
232    def on_heartbeat(self, heartbeat_response: pgnet.Response):
233        """Update game state."""
234        server_hash = heartbeat_response.payload.get("state_hash")
235        if server_hash == self.game_state.get("state_hash"):
236            return
237        self.game_state = heartbeat_response.payload
238        new_hash = self.game_state.get("state_hash")
239        print(f"New game state (hash: {new_hash})")
240        if new_hash:
241            self._refresh_widgets()
242        else:
243            print(f"Missing state hash: {self.game_state=}")
244
245    def heartbeat_payload(self) -> dict:
246        """Send latest known state hash."""
247        return dict(state_hash=self.game_state.get("state_hash"))
248
249    def _make_widgets(self):
250        # Info panel
251        self.info_panel = kx.XLabel(halign="left", valign="top", padding=(10, 5))
252        self.single_player_btn = kx.XButton(
253            text="Start single player",
254            on_release=self._single_player,
255        )
256        self.single_player_btn.set_size(hx=0.5)
257        spbtn = kx.pwrap(self.single_player_btn)
258        spbtn.set_size(y="75sp")
259        panel_box = kx.XBox(orientation="vertical")
260        panel_box.add_widgets(self.info_panel, spbtn)
261        panel_frame = kx.pwrap(panel_box)
262        panel_frame.set_size(x="350dp")
263        # Board
264        board_frame = kx.XGrid(cols=3)
265        self.board = []
266        for i in range(9):
267            square = kx.XButton(
268                font_size=36,
269                background_normal=kx.from_atlas("vkeyboard_key_normal"),
270                background_down=kx.from_atlas("vkeyboard_key_down"),
271                on_release=lambda *a, idx=i: self._play_square(idx),
272            )
273            square.set_size(hx=0.85, hy=0.85)
274            self.board.append(square)
275            board_frame.add_widget(kx.pwrap(square))
276        # Assemble
277        main_frame = kx.XBox()
278        main_frame.add_widgets(panel_frame, board_frame)
279        self.clear_widgets()
280        self.add_widget(main_frame)
281
282    def _refresh_widgets(self, *args):
283        state = self.game_state
284        fg2 = self.subtheme.fg2.markup
285        bullet = fg2("•")
286        players = state.get("players", [])[:2]
287        spectators = state.get("players", [])[2:]
288        info = state.get("info", "Awaiting data from server...")
289        if state.get("your_turn"):
290            info = f"[b]{info}[/b]"
291        self.info_panel.text = "\n".join([
292            "\n",
293            f"[i]{info}[/i]",
294            "\n",
295            fg2("[u][b]Game[/b][/u]"),
296            self.client.game,
297            "\n",
298            fg2("[u][b]Players[/b][/u]"),
299            *(f" ( [b]{'OX'[i]}[/b] ) {p}" for i, p in enumerate(players)),
300            "\n",
301            fg2("[u][b]Spectators[/b][/u]"),
302            *(f" {bullet} {s}" for s in spectators),
303        ])
304        winning_line = state.get("winning_line") or tuple()
305        marks = tuple(str(s or "") for s in state.get("board", [None] * 9))
306        for i, (square_btn, mark) in enumerate(zip(self.board, marks)):
307            square_btn.text = mark
308            winning_square = i in winning_line
309            square_btn.bold = winning_square
310            square_btn.subtheme_name = "accent" if winning_square else "secondary"
311        self.single_player_btn.disabled = len(state.get("players", "--")) >= 2
312
313    def _play_square(self, index: int, /):
314        self.client.send(pgnet.Packet("play_square", dict(square=index)))
315
316    def _single_player(self, *args):
317        self.client.send(pgnet.Packet("single_player"), print)
318
319
320INFO_TEXT = (
321    "[b][u]Welcome to MouseFox[/u][/b]"
322    "\n\n"
323    "This game of Tic-tac-toe is a builtin game example to demo MouseFox."
324)
325ONLINE_INFO_TEXT = (
326    "[u]Connecting to a server[/u]"
327    "\n\n"
328    "To register (if the server allows it) simply choose a username and password"
329    " and log in."
330)
331APP_CONFIG = dict(
332    game_class=Game,
333    game_widget=GameWidget,
334    title="MouseFox Tic-tac-toe",
335    info_text=INFO_TEXT,
336    online_info_text=ONLINE_INFO_TEXT,
337)
338
339
340def run():
341    """Run tictactoe example."""
342    from .. import run
343
344    run(**APP_CONFIG)
class Game(pgnet.util.Game):
 34class Game(pgnet.Game):
 35    """Tic-tac-toe game logic."""
 36
 37    def __init__(self, *args, save_string: Optional[str] = None, **kwargs):
 38        """Override base method."""
 39        super().__init__(*args, **kwargs)
 40        data = json.loads(save_string or json.dumps(BLANK_DATA))
 41        self._next_bot_turn = arrow.now()
 42        self.board: list[str] = data["board"]
 43        self.players: list[str] = data["players"]
 44        self.x_turn: bool = data["x_turn"]
 45        self.outcome: str = "Waiting for players."
 46        if data["in_progress"]:
 47            self.outcome = "In progress."
 48        self.in_progress: bool = data["in_progress"]
 49        self.commands = dict(
 50            single_player=self._set_single_player,
 51            play_square=self._play_square,
 52        )
 53
 54    @property
 55    def persistent(self):
 56        """Override base property."""
 57        return self.in_progress and any(self.board) and BOT_NAME not in self.players
 58
 59    def get_save_string(self) -> str:
 60        """Override base method."""
 61        data = dict(
 62            board=self.board,
 63            players=self.players,
 64            x_turn=self.x_turn,
 65            in_progress=self.in_progress,
 66        )
 67        return json.dumps(data)
 68
 69    def user_joined(self, player: str):
 70        """Override base method."""
 71        if player not in self.players:
 72            self.players.append(player)
 73        if len(self.players) == 2:
 74            random.shuffle(self.players)
 75            self.in_progress = True
 76            self.outcome = "In progress."
 77
 78    def user_left(self, player: str):
 79        """Override base method."""
 80        if player in self.players[2:]:
 81            self.players.remove(player)
 82
 83    def handle_game_packet(self, packet: Packet) -> Response:
 84        """Override base method."""
 85        meth = self.commands.get(packet.message)
 86        if not meth:
 87            return Response("No such command.")
 88        return meth(packet)
 89
 90    # Logic
 91    @property
 92    def _state_hash(self) -> str:
 93        data = [
 94            str(self.board),
 95            str(self.players),
 96            str(self.x_turn),
 97        ]
 98        final = hash(tuple(data))
 99        return final
100
101    @property
102    def _current_username(self) -> str:
103        if not self.in_progress:
104            return ""
105        return self.players[int(self.x_turn)]
106
107    def _winning_line(
108        self,
109        board: Optional[list[str]] = None,
110    ) -> Optional[tuple[int, int, int]]:
111        if board is None:
112            board = self.board
113        for a, b, c in WINNING_LINES:
114            mark = board[a]
115            if mark and mark == board[b] == board[c]:
116                return a, b, c
117        return None
118
119    def _check_progress(self):
120        winning_line = self._winning_line()
121        if winning_line:
122            mark = self.board[winning_line[0]]
123            self.in_progress = False
124            self.outcome = (f"{self._mark_to_username(mark)} playing as {mark} wins!")
125            return
126        if all(self.board):
127            self.in_progress = False
128            self.outcome = "Draw."
129
130    def _mark_to_username(self, mark: str):
131        assert mark
132        return self.players[0 if mark == "O" else 1]
133
134    def _username_to_mark(self, username: str):
135        assert username in self.players[:2]
136        return "O" if username == self.players[0] else "X"
137
138    def update(self):
139        """Override base method."""
140        if self._current_username != BOT_NAME:
141            return
142        if arrow.now() <= self._next_bot_turn:
143            return
144        my_mark = self._username_to_mark(BOT_NAME)
145        enemy_player_idx = int(not bool(self.players.index(self._current_username)))
146        enemy_mark = self._username_to_mark(self.players[enemy_player_idx])
147        empty_squares = [s for s in range(9) if not self.board[s]]
148        random.shuffle(empty_squares)
149        # Find winning moves
150        for s in empty_squares:
151            new_board = copy.copy(self.board)
152            new_board[s] = my_mark
153            if self._winning_line(new_board):
154                self._do_play_square(s, my_mark)
155                return
156        # Find losing threats
157        for s in empty_squares:
158            new_board = copy.copy(self.board)
159            new_board[s] = enemy_mark
160            if self._winning_line(new_board):
161                break
162        self._do_play_square(s, my_mark)
163
164    # Commands
165    def handle_heartbeat(self, packet: Packet) -> Response:
166        """Override base method."""
167        state_hash = self._state_hash
168        client_hash = packet.payload.get("state_hash")
169        if client_hash == state_hash:
170            return Response("Up to date.", dict(state_hash=state_hash))
171        payload = dict(
172            state_hash=state_hash,
173            players=self.players,
174            board=self.board,
175            your_turn=packet.username == self._current_username,
176            info=self._get_user_info(packet.username),
177            in_progress=self.in_progress,
178            winning_line=self._winning_line(),
179        )
180        return Response("Updated state.", payload)
181
182    def _set_single_player(self, packet: Packet) -> Response:
183        if len(self.players) >= 2:
184            return Response("Game has already started.", status=Status.UNEXPECTED)
185        self.user_joined(BOT_NAME)
186        self._next_bot_turn = arrow.now().shift(seconds=BOT_THINK_TIME)
187        return Response("Started single player mode.")
188
189    def _play_square(self, packet: Packet) -> Response:
190        username = self._current_username
191        if packet.username != username or username == BOT_NAME:
192            return Response("Not your turn.", status=Status.UNEXPECTED)
193        square = int(packet.payload["square"])
194        if self.board[square]:
195            return Response("Square is already marked.", status=Status.UNEXPECTED)
196        self._do_play_square(square, self._username_to_mark(username))
197        return Response("Marked square.")
198
199    def _do_play_square(self, square: int, mark: str, /):
200        self.board[square] = mark
201        self.x_turn = not self.x_turn
202        self._check_progress()
203        self._next_bot_turn = arrow.now().shift(seconds=BOT_THINK_TIME)
204
205    def _get_user_info(self, username: str) -> str:
206        if not self.in_progress:
207            return self.outcome
208        current_username = self._current_username
209        if username not in self.players[:2]:
210            mark = self._username_to_mark(current_username)
211            return f"{self.outcome}\nSpectating {current_username}'s turn as {mark}"
212        turn = "Your turn" if username == current_username else "Awaiting turn"
213        return f"{turn}, playing as: {self._username_to_mark(username)}"

Tic-tac-toe game logic.

Game(*args, save_string: Optional[str] = None, **kwargs)
37    def __init__(self, *args, save_string: Optional[str] = None, **kwargs):
38        """Override base method."""
39        super().__init__(*args, **kwargs)
40        data = json.loads(save_string or json.dumps(BLANK_DATA))
41        self._next_bot_turn = arrow.now()
42        self.board: list[str] = data["board"]
43        self.players: list[str] = data["players"]
44        self.x_turn: bool = data["x_turn"]
45        self.outcome: str = "Waiting for players."
46        if data["in_progress"]:
47            self.outcome = "In progress."
48        self.in_progress: bool = data["in_progress"]
49        self.commands = dict(
50            single_player=self._set_single_player,
51            play_square=self._play_square,
52        )

Override base method.

persistent

Override base property.

def get_save_string(self) -> str:
59    def get_save_string(self) -> str:
60        """Override base method."""
61        data = dict(
62            board=self.board,
63            players=self.players,
64            x_turn=self.x_turn,
65            in_progress=self.in_progress,
66        )
67        return json.dumps(data)

Override base method.

def user_joined(self, player: str):
69    def user_joined(self, player: str):
70        """Override base method."""
71        if player not in self.players:
72            self.players.append(player)
73        if len(self.players) == 2:
74            random.shuffle(self.players)
75            self.in_progress = True
76            self.outcome = "In progress."

Override base method.

def user_left(self, player: str):
78    def user_left(self, player: str):
79        """Override base method."""
80        if player in self.players[2:]:
81            self.players.remove(player)

Override base method.

def handle_game_packet(self, packet: pgnet.util.Packet) -> pgnet.util.Response:
83    def handle_game_packet(self, packet: Packet) -> Response:
84        """Override base method."""
85        meth = self.commands.get(packet.message)
86        if not meth:
87            return Response("No such command.")
88        return meth(packet)

Override base method.

def update(self):
138    def update(self):
139        """Override base method."""
140        if self._current_username != BOT_NAME:
141            return
142        if arrow.now() <= self._next_bot_turn:
143            return
144        my_mark = self._username_to_mark(BOT_NAME)
145        enemy_player_idx = int(not bool(self.players.index(self._current_username)))
146        enemy_mark = self._username_to_mark(self.players[enemy_player_idx])
147        empty_squares = [s for s in range(9) if not self.board[s]]
148        random.shuffle(empty_squares)
149        # Find winning moves
150        for s in empty_squares:
151            new_board = copy.copy(self.board)
152            new_board[s] = my_mark
153            if self._winning_line(new_board):
154                self._do_play_square(s, my_mark)
155                return
156        # Find losing threats
157        for s in empty_squares:
158            new_board = copy.copy(self.board)
159            new_board[s] = enemy_mark
160            if self._winning_line(new_board):
161                break
162        self._do_play_square(s, my_mark)

Override base method.

def handle_heartbeat(self, packet: pgnet.util.Packet) -> pgnet.util.Response:
165    def handle_heartbeat(self, packet: Packet) -> Response:
166        """Override base method."""
167        state_hash = self._state_hash
168        client_hash = packet.payload.get("state_hash")
169        if client_hash == state_hash:
170            return Response("Up to date.", dict(state_hash=state_hash))
171        payload = dict(
172            state_hash=state_hash,
173            players=self.players,
174            board=self.board,
175            your_turn=packet.username == self._current_username,
176            info=self._get_user_info(packet.username),
177            in_progress=self.in_progress,
178            winning_line=self._winning_line(),
179        )
180        return Response("Updated state.", payload)

Override base method.

class GameWidget(kvex.widgets.layouts.XFrame):
216class GameWidget(kx.XFrame):
217    """Tic-tac-toe GUI widget."""
218
219    def __init__(self, client: pgnet.Client, **kwargs):
220        """Override base method."""
221        super().__init__(**kwargs)
222        self.client = client
223        self._make_widgets()
224        self.game_state = dict(state_hash=None)
225        client.on_heartbeat = self.on_heartbeat
226        client.heartbeat_payload = self.heartbeat_payload
227
228    def on_subtheme(self, *args, **kwargs):
229        """Override base method."""
230        super().on_subtheme(*args, **kwargs)
231        self._refresh_widgets()
232
233    def on_heartbeat(self, heartbeat_response: pgnet.Response):
234        """Update game state."""
235        server_hash = heartbeat_response.payload.get("state_hash")
236        if server_hash == self.game_state.get("state_hash"):
237            return
238        self.game_state = heartbeat_response.payload
239        new_hash = self.game_state.get("state_hash")
240        print(f"New game state (hash: {new_hash})")
241        if new_hash:
242            self._refresh_widgets()
243        else:
244            print(f"Missing state hash: {self.game_state=}")
245
246    def heartbeat_payload(self) -> dict:
247        """Send latest known state hash."""
248        return dict(state_hash=self.game_state.get("state_hash"))
249
250    def _make_widgets(self):
251        # Info panel
252        self.info_panel = kx.XLabel(halign="left", valign="top", padding=(10, 5))
253        self.single_player_btn = kx.XButton(
254            text="Start single player",
255            on_release=self._single_player,
256        )
257        self.single_player_btn.set_size(hx=0.5)
258        spbtn = kx.pwrap(self.single_player_btn)
259        spbtn.set_size(y="75sp")
260        panel_box = kx.XBox(orientation="vertical")
261        panel_box.add_widgets(self.info_panel, spbtn)
262        panel_frame = kx.pwrap(panel_box)
263        panel_frame.set_size(x="350dp")
264        # Board
265        board_frame = kx.XGrid(cols=3)
266        self.board = []
267        for i in range(9):
268            square = kx.XButton(
269                font_size=36,
270                background_normal=kx.from_atlas("vkeyboard_key_normal"),
271                background_down=kx.from_atlas("vkeyboard_key_down"),
272                on_release=lambda *a, idx=i: self._play_square(idx),
273            )
274            square.set_size(hx=0.85, hy=0.85)
275            self.board.append(square)
276            board_frame.add_widget(kx.pwrap(square))
277        # Assemble
278        main_frame = kx.XBox()
279        main_frame.add_widgets(panel_frame, board_frame)
280        self.clear_widgets()
281        self.add_widget(main_frame)
282
283    def _refresh_widgets(self, *args):
284        state = self.game_state
285        fg2 = self.subtheme.fg2.markup
286        bullet = fg2("•")
287        players = state.get("players", [])[:2]
288        spectators = state.get("players", [])[2:]
289        info = state.get("info", "Awaiting data from server...")
290        if state.get("your_turn"):
291            info = f"[b]{info}[/b]"
292        self.info_panel.text = "\n".join([
293            "\n",
294            f"[i]{info}[/i]",
295            "\n",
296            fg2("[u][b]Game[/b][/u]"),
297            self.client.game,
298            "\n",
299            fg2("[u][b]Players[/b][/u]"),
300            *(f" ( [b]{'OX'[i]}[/b] ) {p}" for i, p in enumerate(players)),
301            "\n",
302            fg2("[u][b]Spectators[/b][/u]"),
303            *(f" {bullet} {s}" for s in spectators),
304        ])
305        winning_line = state.get("winning_line") or tuple()
306        marks = tuple(str(s or "") for s in state.get("board", [None] * 9))
307        for i, (square_btn, mark) in enumerate(zip(self.board, marks)):
308            square_btn.text = mark
309            winning_square = i in winning_line
310            square_btn.bold = winning_square
311            square_btn.subtheme_name = "accent" if winning_square else "secondary"
312        self.single_player_btn.disabled = len(state.get("players", "--")) >= 2
313
314    def _play_square(self, index: int, /):
315        self.client.send(pgnet.Packet("play_square", dict(square=index)))
316
317    def _single_player(self, *args):
318        self.client.send(pgnet.Packet("single_player"), print)

Tic-tac-toe GUI widget.

GameWidget(client: pgnet.client.Client, **kwargs)
219    def __init__(self, client: pgnet.Client, **kwargs):
220        """Override base method."""
221        super().__init__(**kwargs)
222        self.client = client
223        self._make_widgets()
224        self.game_state = dict(state_hash=None)
225        client.on_heartbeat = self.on_heartbeat
226        client.heartbeat_payload = self.heartbeat_payload

Override base method.

def on_subtheme(self, *args, **kwargs):
228    def on_subtheme(self, *args, **kwargs):
229        """Override base method."""
230        super().on_subtheme(*args, **kwargs)
231        self._refresh_widgets()

Override base method.

def on_heartbeat(self, heartbeat_response: pgnet.util.Response):
233    def on_heartbeat(self, heartbeat_response: pgnet.Response):
234        """Update game state."""
235        server_hash = heartbeat_response.payload.get("state_hash")
236        if server_hash == self.game_state.get("state_hash"):
237            return
238        self.game_state = heartbeat_response.payload
239        new_hash = self.game_state.get("state_hash")
240        print(f"New game state (hash: {new_hash})")
241        if new_hash:
242            self._refresh_widgets()
243        else:
244            print(f"Missing state hash: {self.game_state=}")

Update game state.

def heartbeat_payload(self) -> dict:
246    def heartbeat_payload(self) -> dict:
247        """Send latest known state hash."""
248        return dict(state_hash=self.game_state.get("state_hash"))

Send latest known state hash.

Inherited Members
kivy.uix.anchorlayout.AnchorLayout
padding
anchor_x
anchor_y
do_layout
kivy.uix.layout.Layout
add_widget
remove_widget
layout_hint_with_bounds
kivy.uix.widget.Widget
proxy_ref
apply_class_lang_rules
collide_point
collide_widget
on_motion
on_touch_down
on_touch_move
on_touch_up
on_kv_post
clear_widgets
register_for_motion_event
unregister_for_motion_event
export_to_png
export_as_image
get_root_window
get_parent_window
walk
walk_reverse
to_widget
to_window
to_parent
to_local
get_window_matrix
x
y
width
height
pos
size
get_right
set_right
right
get_top
set_top
top
get_center_x
set_center_x
center_x
get_center_y
set_center_y
center_y
center
cls
children
parent
size_hint_x
size_hint_y
size_hint
pos_hint
size_hint_min_x
size_hint_min_y
size_hint_min
size_hint_max_x
size_hint_max_y
size_hint_max
ids
opacity
on_opacity
canvas
get_disabled
set_disabled
inc_disabled
dec_disabled
disabled
motion_filter
kivy._event.EventDispatcher
register_event_type
unregister_event_types
unregister_event_type
is_event_type
bind
unbind
fbind
funbind
unbind_uid
get_property_observers
events
dispatch
dispatch_generic
dispatch_children
setter
getter
property
properties
create_property
apply_property
def run():
341def run():
342    """Run tictactoe example."""
343    from .. import run
344
345    run(**APP_CONFIG)

Run tictactoe example.