Created
September 9, 2025 08:31
-
-
Save vslala/fce9dc7091437d5f9832f0892b2019e0 to your computer and use it in GitHub Desktop.
Play Tic Tac Toe with Claude 3.7 - GUI built using TKinter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from functools import partial | |
| import random | |
| import re | |
| import threading | |
| import tkinter as tk | |
| from tkinter import messagebox | |
| from typing import Any, Literal, Optional | |
| from langchain_aws import ChatBedrock | |
| class ClaudeSonnetChatbot: | |
| def __init__(self, temperature: float = 0, max_tokens: int = 2): | |
| stage = os.getenv("STAGE", "local").lower() | |
| bedrock_kwargs = { | |
| "model": "us.anthropic.claude-3-7-sonnet-20250219-v1:0", | |
| "model_kwargs": {"temperature": temperature, "max_tokens": max_tokens}, | |
| "region": os.getenv("AWS_REGION", "us-east-1"), | |
| } | |
| bedrock_kwargs["credentials_profile_name"] = os.getenv("AWS_PROFILE", "searchexpert") | |
| self.llm = ChatBedrock(**bedrock_kwargs) | |
| def get_text_response(self, prompt: str) -> str: | |
| response = self.llm.invoke(prompt) | |
| print(response.text()) | |
| return response.text() | |
| def generate_prompt(ai_symbol: str, board_state: list[list[str]]) -> str: | |
| return f""" | |
| You are a tic-tac-toe playing AI. You are playing as {ai_symbol}. | |
| The board is represented as a 3x3 grid with rows and columns indexed from 0 to 2. | |
| The current state of the board is as follows: | |
| {board_state} | |
| It is your turn now. Generate a chain of thoughts to decide your next move. And once you have decided your move, respond with only the move enclosed in <move></move> tags. | |
| Please respond with your move in the format <move>row,column</move>, where row and column are integers between 0 and 2. | |
| For example, if you want to place your symbol in the top-left corner, you would respond with <move>0,0</move>. | |
| CRITICAL: DO NOT RESPOND WITHOUT ENCLOSED <move></move>. ONLY RESPOND WITH THE MOVE IN THE SPECIFIED FORMAT. | |
| """ | |
| llm = ClaudeSonnetChatbot(temperature=0.7, max_tokens=1024) | |
| def extract_tag_content(text: str, tag: str) -> list[str]: | |
| """ | |
| Extracts all text contents inside provided tag. | |
| Works with both <tag>content</tag> style tags. | |
| """ | |
| if text is None: | |
| return [] | |
| esc = re.escape(tag) | |
| pattern = rf"<{esc}>(.*?)</{esc}>" | |
| matches = re.findall(pattern, text, flags=re.DOTALL) | |
| return matches | |
| class GameState: | |
| turn: Literal["X", "O"] = "X" | |
| board: list[list[tk.StringVar]] = [] | |
| player_symbol: Literal["X", "O"] = "X" | |
| class GameController: | |
| def __init__(self, root: tk.Misc, state: GameState) -> None: | |
| self.root = root | |
| self._ai_busy = False | |
| self.board = None # will be set by attach_board | |
| state.board = [[tk.StringVar(master=root, value="") for _ in range(3)] for _ in range(3)] | |
| state.player_symbol = random.choice(["X", "O"]) | |
| self.state = state | |
| if not self.is_player_turn(): | |
| self.root.after_idle(self.play_ai) | |
| def is_player_turn(self) -> bool: | |
| return self.state.turn == self.state.player_symbol | |
| def play_ai(self) -> None: | |
| if self._ai_busy: | |
| return | |
| def _apply_ai_move(r: int, c: int) -> None: | |
| self.state.board[r][c].set(self.state.turn) | |
| self.state.turn = "O" if self.state.turn == "X" else "X" | |
| self._ai_busy = False | |
| self._end_if_done() | |
| def _ai_worker() -> None: | |
| response = llm.get_text_response(prompt=generate_prompt( | |
| ai_symbol="O" if self.state.player_symbol == "X" else "X", | |
| board_state=[[cell.get() for cell in row] for row in self.state.board] | |
| )) | |
| move = extract_tag_content(response, "move") | |
| assert len(move) == 1 | |
| rr_str, cc_str = move[0].split(",") | |
| rr, cc = int(rr_str), int(cc_str) | |
| self.root.after(0, _apply_ai_move, rr, cc) | |
| self._ai_busy = True | |
| threading.Thread(target=_ai_worker, daemon=True).start() | |
| def reset(self) -> None: | |
| if self.board is not None: | |
| self.board.clear_highlight() | |
| for row in self.state.board: | |
| for v in row: | |
| v.set("") | |
| self.state.turn = "X" | |
| self.state.player_symbol = random.choice(["X", "O"]) | |
| if not self.is_player_turn(): | |
| self.play_ai() | |
| def _end_if_done(self) -> None: | |
| def _no_more_moves() -> bool: | |
| return all(cell.get() for row in self.state.board for cell in row) | |
| def _winning_line() -> Optional[list[tuple[int, int]]]: | |
| lines = ( | |
| [[(r, 0), (r, 1), (r, 2)] for r in range(3)] + | |
| [[(0, c), (1, c), (2, c)] for c in range(3)] + | |
| [[(0, 0), (1, 1), (2, 2)], | |
| [(0, 2), (1, 1), (2, 0)]] | |
| ) | |
| g = lambda r, c: self.state.board[r][c].get() | |
| for line in lines: | |
| a, b, c = line | |
| va, vb, vc = g(*a), g(*b), g(*c) | |
| if va and va == vb == vc: | |
| return line | |
| return None | |
| line = _winning_line() | |
| if line: | |
| if self.board is not None: | |
| self.board.highlight_line(line) | |
| self.root.update_idletasks() | |
| r0, c0 = line[0] | |
| winner = self.state.board[r0][c0].get() | |
| # schedule dialog so highlight paints first | |
| self.root.after(0, lambda: (self.reset() if messagebox.askyesno("Game over", f"{winner} wins. Play again?") else None)) | |
| return | |
| if _no_more_moves(): | |
| self.root.after(0, lambda: (self.reset() if messagebox.askyesno("Game over", "Draw. Play again?") else None)) | |
| def play(self, r: int, c: int) -> None: | |
| if self._ai_busy: | |
| return | |
| if self.state.board[r][c].get(): | |
| return | |
| self.state.board[r][c].set(self.state.turn) | |
| self.state.turn = "O" if self.state.turn == "X" else "X" | |
| self._end_if_done() | |
| self.root.after_idle(self.play_ai) | |
| def attach_board(self, board: "GameBoard") -> None: | |
| self.board = board | |
| class GameBoard(tk.Frame): | |
| def __init__(self, controller: GameController, master: tk.Misc | None = None, | |
| cnf: dict[str, Any] | None = None, **kw: Any) -> None: | |
| super().__init__(master=master, cnf=cnf or {}, **kw) | |
| for i in range(3): | |
| self.rowconfigure(i, weight=1, uniform="rows") | |
| self.columnconfigure(i, weight=1, uniform="cols") | |
| self.cells: list[list[tk.Button]] = [] | |
| for r in range(3): | |
| row: list[tk.Button] = [] | |
| for c in range(3): | |
| b = tk.Button( | |
| self, | |
| textvariable=controller.state.board[r][c], | |
| bd=0, | |
| relief="flat", | |
| highlightthickness=0, | |
| command=partial(controller.play, r, c), | |
| ) | |
| b.grid(row=r, column=c, sticky="nsew", padx=0, pady=0) | |
| row.append(b) | |
| self.cells.append(row) | |
| controller.attach_board(self) | |
| def highlight_line(self, coord: list[tuple[int, int]], color: str = "#b5f5b5") -> None: | |
| for r, c in coord: | |
| btn = self.cells[r][c] | |
| btn.configure(highlightthickness=3, highlightbackground=color, highlightcolor=color) | |
| def clear_highlight(self) -> None: | |
| for row in self.cells: | |
| for btn in row: | |
| btn.configure(highlightthickness=0) | |
| class TicTacToeApp(tk.Tk): | |
| def __init__(self) -> None: | |
| super().__init__() | |
| self.title("Tic Tac Toe") | |
| self.geometry("350x350") | |
| self.resizable(True, True) | |
| self.controller = GameController(root=self, state=GameState()) | |
| container = tk.Frame(self, padx=12, pady=12) | |
| container.pack(fill="both", expand=True) | |
| container.grid_rowconfigure(0, weight=0) | |
| container.grid_rowconfigure(1, weight=1) | |
| container.grid_columnconfigure(0, weight=1) | |
| tk.Label(container, text="Tic Tac Toe", font=("Helvetica", 16))\ | |
| .grid(row=0, column=0, sticky="n", pady=(0, 8)) | |
| self.board = GameBoard(controller=self.controller, master=container) | |
| self.board.grid(row=1, column=0, sticky="nsew") | |
| if __name__ == "__main__": | |
| app = TicTacToeApp() | |
| app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment