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)
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.
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.
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.
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.
Inherited Members
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.
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():
Run tictactoe example.