Skip to content

Instantly share code, notes, and snippets.

@vslala
Created September 9, 2025 08:31
Show Gist options
  • Select an option

  • Save vslala/fce9dc7091437d5f9832f0892b2019e0 to your computer and use it in GitHub Desktop.

Select an option

Save vslala/fce9dc7091437d5f9832f0892b2019e0 to your computer and use it in GitHub Desktop.
Play Tic Tac Toe with Claude 3.7 - GUI built using TKinter
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