srk-dot-ai commited on
Commit
ee656d3
Β·
1 Parent(s): 72048de

Refactor Tic Tac Toe game by adding DeepSeek and OpenAI agents, updating README title, enhancing game logic, and implementing game statistics tracking.

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Tic Tac Toe
3
  emoji: 😻
4
  colorFrom: pink
5
  colorTo: red
 
1
  ---
2
+ title: Tic-Tac-Toe
3
  emoji: 😻
4
  colorFrom: pink
5
  colorTo: red
agents/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/agents/__pycache__/__init__.cpython-312.pyc and b/agents/__pycache__/__init__.cpython-312.pyc differ
 
agents/__pycache__/agent.cpython-312.pyc CHANGED
Binary files a/agents/__pycache__/agent.cpython-312.pyc and b/agents/__pycache__/agent.cpython-312.pyc differ
 
agents/__pycache__/deepseek_agent.cpython-312.pyc ADDED
Binary file (2.24 kB). View file
 
agents/__pycache__/google_agent.cpython-312.pyc CHANGED
Binary files a/agents/__pycache__/google_agent.cpython-312.pyc and b/agents/__pycache__/google_agent.cpython-312.pyc differ
 
agents/__pycache__/ollama_agent.cpython-312.pyc CHANGED
Binary files a/agents/__pycache__/ollama_agent.cpython-312.pyc and b/agents/__pycache__/ollama_agent.cpython-312.pyc differ
 
agents/__pycache__/openai_agent.cpython-312.pyc ADDED
Binary file (1.74 kB). View file
 
agents/deepseek_agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from azure.ai.inference import ChatCompletionsClient
4
+ from azure.ai.inference.models import SystemMessage, UserMessage
5
+ from azure.core.credentials import AzureKeyCredential
6
+ from agents.agent import Agent
7
+
8
+ logging.getLogger("azure").setLevel(logging.WARNING)
9
+ logging.getLogger("azure.core.pipeline").setLevel(logging.WARNING)
10
+
11
+
12
+ class DeepSeekAgent(Agent):
13
+ name = "DeepSeek"
14
+ color = Agent.CYAN
15
+
16
+ def __init__(self, model_name):
17
+ """
18
+ Set up this instance
19
+ """
20
+ self.endpoint = "https://models.github.ai/inference"
21
+ self.token = os.environ["GITHUB_TOKEN"]
22
+ self.client = ChatCompletionsClient(
23
+ endpoint=self.endpoint,
24
+ credential=AzureKeyCredential(self.token),
25
+ )
26
+ self.log(f"DeepSeek agent is getting called with model: {model_name}")
27
+
28
+ def make_move(self, model_key, prompt):
29
+ response = self.client.complete(
30
+ messages=[
31
+ SystemMessage(
32
+ content="You are a snarky Tic-Tac-Toe player who loves to mock opponents."
33
+ ),
34
+ UserMessage(content=prompt),
35
+ ],
36
+ max_tokens=1000,
37
+ model=model_key["value"],
38
+ )
39
+ return response.choices[0].message.content
agents/openai_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from agents.agent import Agent
4
+
5
+
6
+ class OpenAIAgent(Agent):
7
+ name = "OpenAI"
8
+ color = Agent.GREEN
9
+
10
+ def __init__(self, model_name):
11
+ """
12
+ Set up this instance
13
+ """
14
+ self.token = os.environ["GITHUB_TOKEN"]
15
+ self.endpoint = "https://models.github.ai/inference"
16
+ self.client = OpenAI(base_url=self.endpoint, api_key=self.token)
17
+ self.log(f"OpenAI agent is getting called with model: {model_name}")
18
+
19
+ def make_move(self, model_key, prompt):
20
+ response = self.client.chat.completions.create(
21
+ messages=[
22
+ {
23
+ "role": "system",
24
+ "content": "You are a snarky Tic-Tac-Toe player who loves to mock opponents.",
25
+ },
26
+ {
27
+ "role": "user",
28
+ "content": prompt,
29
+ },
30
+ ],
31
+ model=model_key["value"],
32
+ )
33
+ return response.choices[0].message.content
game/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/game/__pycache__/__init__.cpython-312.pyc and b/game/__pycache__/__init__.cpython-312.pyc differ
 
game/__pycache__/logic.cpython-312.pyc CHANGED
Binary files a/game/__pycache__/logic.cpython-312.pyc and b/game/__pycache__/logic.cpython-312.pyc differ
 
game/__pycache__/start_game.cpython-312.pyc CHANGED
Binary files a/game/__pycache__/start_game.cpython-312.pyc and b/game/__pycache__/start_game.cpython-312.pyc differ
 
game/__pycache__/stats.cpython-312.pyc ADDED
Binary file (3.71 kB). View file
 
game/logic.py CHANGED
@@ -1,8 +1,11 @@
1
  from transformers import pipeline
2
- from models.registry import models
3
  from models.provider_constant import PROVIDER_CONSTANT
4
  from agents.ollama_agent import OllamaAgent
5
  from agents.google_agent import GoogleAgent
 
 
 
6
 
7
  def parse_move_and_comment(text):
8
  """Parse 'MOVE: N' and 'COMMENT: ...' from model response."""
@@ -19,42 +22,43 @@ def parse_move_and_comment(text):
19
  comment = line.split(":", 1)[-1].strip()[:200]
20
  return move, comment or "No comment."
21
 
22
- def ask_model_for_best_move(board, player, model_name):
23
 
 
24
  grid = """
25
  [{0}] [{1}] [{2}]
26
  [{3}] [{4}] [{5}]
27
  [{6}] [{7}] [{8}]
28
- """.format(
29
- *(board[i] if board[i] is not None else "_" for i in range(9))
30
- )
31
-
32
- prompt = f"""
33
- You are playing Tic-Tac-Toe as "{player}". Be snarky and competitive.
34
 
35
- Current board (empty cells are "_"):
36
- {grid}
37
 
38
- Rules you MUST follow:
39
- 1. You may ONLY choose a cell that currently contains "_".
40
- 2. Choosing a filled cell is an INVALID MOVE and a FAILURE.
41
- 3. Before answering, identify all empty cell indices and pick ONE of them.
42
- 4. Double-check that your chosen cell is empty before replying.
43
 
44
- Cell indices:
45
- Top row: 0, 1, 2
46
- Middle row: 3, 4, 5
47
- Bottom row: 6, 7, 8
 
 
48
 
49
- Your MOVE must be a single digit from the empty cells.
50
- Do not explain your reasoning.
51
- You will look foolish and stupid if you return number which is already filled.
 
 
 
52
 
53
- Reply with EXACTLY two lines and nothing else:
54
- MOVE: <one digit from the empty cells only>
55
- COMMENT: <one short snarky sentence mocking the opponent or bragging>
56
- """
 
 
57
 
 
 
 
58
 
59
  model_key = next((model for model in models if model["name"] == model_name), None)
60
  if not model_key:
@@ -64,19 +68,24 @@ def ask_model_for_best_move(board, player, model_name):
64
 
65
  if model_key["provider"] == PROVIDER_CONSTANT["OLLAMA"]:
66
  ollama_model = OllamaAgent(model_key["name"])
67
- text = ollama_model.make_move(model_key,prompt)
68
  elif model_key["provider"] == PROVIDER_CONSTANT["GOOGLE"]:
69
  google_model = GoogleAgent(model_key["name"])
70
- text = google_model.make_move(model_key,prompt)
 
 
 
 
 
 
71
  else:
72
  generator = pipeline("text-generation", model=model_key["value"])
73
  text = generator(
74
  prompt,
75
- max_new_tokens=20, # keeps answer short
76
- do_sample=False, # reduces hallucination
77
  pad_token_id=generator.tokenizer.eos_token_id,
78
  )[0]["generated_text"]
79
 
80
-
81
  move, comment = parse_move_and_comment(text)
82
- return move, comment
 
1
  from transformers import pipeline
2
+ from models.registry import models
3
  from models.provider_constant import PROVIDER_CONSTANT
4
  from agents.ollama_agent import OllamaAgent
5
  from agents.google_agent import GoogleAgent
6
+ from agents.openai_agent import OpenAIAgent
7
+ from agents.deepseek_agent import DeepSeekAgent
8
+
9
 
10
  def parse_move_and_comment(text):
11
  """Parse 'MOVE: N' and 'COMMENT: ...' from model response."""
 
22
  comment = line.split(":", 1)[-1].strip()[:200]
23
  return move, comment or "No comment."
24
 
 
25
 
26
+ def ask_model_for_best_move(board, player, model_name):
27
  grid = """
28
  [{0}] [{1}] [{2}]
29
  [{3}] [{4}] [{5}]
30
  [{6}] [{7}] [{8}]
31
+ """.format(*(board[i] if board[i] is not None else "_" for i in range(9)))
 
 
 
 
 
32
 
33
+ prompt = f"""You are playing Tic-Tac-Toe as "{player}". You are a strategic player.
 
34
 
35
+ Current board state (empty cells are "_"):
36
+ {grid}
 
 
 
37
 
38
+ Cell positions:
39
+ 0 | 1 | 2
40
+ ----------
41
+ 3 | 4 | 5
42
+ ----------
43
+ 6 | 7 | 8
44
 
45
+ CRITICAL RULES:
46
+ 1. You MUST choose a cell that currently shows "_" (underscore).
47
+ 2. NEVER choose an already filled cell - this is an instant loss.
48
+ 3. Winning moves (completing 3 in a row) take PRIORITY.
49
+ 4. Blocking moves (stopping opponent's winning move) take second priority.
50
+ 5. Center cell (4) is valuable early game.
51
 
52
+ STRATEGY (in priority order):
53
+ 1. If you can win in one move, take it immediately.
54
+ 2. If opponent can win next turn, block them.
55
+ 3. Take center (4) if available.
56
+ 4. Take a corner if available.
57
+ 5. Avoid giving opponent a winning setup.
58
 
59
+ Reply with EXACTLY these two lines only, nothing else:
60
+ MOVE: <cell number 0-8 from available cells only>
61
+ COMMENT: <short competitive trash talk>"""
62
 
63
  model_key = next((model for model in models if model["name"] == model_name), None)
64
  if not model_key:
 
68
 
69
  if model_key["provider"] == PROVIDER_CONSTANT["OLLAMA"]:
70
  ollama_model = OllamaAgent(model_key["name"])
71
+ text = ollama_model.make_move(model_key, prompt)
72
  elif model_key["provider"] == PROVIDER_CONSTANT["GOOGLE"]:
73
  google_model = GoogleAgent(model_key["name"])
74
+ text = google_model.make_move(model_key, prompt)
75
+ elif model_key["provider"] == PROVIDER_CONSTANT["OPENAI"]:
76
+ openai_model = OpenAIAgent(model_key["name"])
77
+ text = openai_model.make_move(model_key, prompt)
78
+ elif model_key["provider"] == PROVIDER_CONSTANT["DEEPSEEK"]:
79
+ deepseek_model = DeepSeekAgent(model_key["name"])
80
+ text = deepseek_model.make_move(model_key, prompt)
81
  else:
82
  generator = pipeline("text-generation", model=model_key["value"])
83
  text = generator(
84
  prompt,
85
+ max_new_tokens=20,
86
+ do_sample=False,
87
  pad_token_id=generator.tokenizer.eos_token_id,
88
  )[0]["generated_text"]
89
 
 
90
  move, comment = parse_move_and_comment(text)
91
+ return move, comment
game/start_game.py CHANGED
@@ -1,23 +1,32 @@
1
  import time
2
  from game.logic import ask_model_for_best_move
 
3
 
4
 
5
  WIN_LINES = [
6
- (0, 1, 2), (3, 4, 5), (6, 7, 8), # rows
7
- (0, 3, 6), (1, 4, 7), (2, 5, 8), # columns
8
- (0, 4, 8), (2, 4, 6), # diagonals
 
 
 
 
 
9
  ]
10
 
 
11
  def check_winner(board):
12
  """Return 'X', 'O', or None."""
13
  for a, b, c in WIN_LINES:
14
  if board[a] and board[a] == board[b] == board[c]:
15
- return board[a],(a,b,c)
16
- return None,None
 
17
 
18
  def check_draw(board):
19
  return all(cell is not None for cell in board)
20
 
 
21
  def get_valid_move(board, player, model_name):
22
  """Get move from model; if invalid, pick first empty cell as fallback."""
23
  move, comment = ask_model_for_best_move(board, player, model_name)
@@ -28,6 +37,7 @@ def get_valid_move(board, player, model_name):
28
  return i, comment
29
  return None, comment
30
 
 
31
  def play_full_game(board_state, current_player_move, model_1_name, model_2_name):
32
  board = list(board_state) if board_state else [None] * 9
33
  current = current_player_move
@@ -49,7 +59,7 @@ def play_full_game(board_state, current_player_move, model_1_name, model_2_name)
49
  model_2_comment = comment
50
 
51
  board[move] = current
52
- winner,winning_line = check_winner(board)
53
  if winner:
54
  status = f"{winner} wins! ({model})"
55
  game_over = True
@@ -57,11 +67,11 @@ def play_full_game(board_state, current_player_move, model_1_name, model_2_name)
57
  status = "Draw! No winner."
58
  game_over = True
59
  else:
60
- status = f"{model} played at position {move+1}."
61
  game_over = False
62
 
63
  next_player = "O" if current == "X" else "X"
64
-
65
  button_values = []
66
  for i in range(9):
67
  if board[i] is None:
@@ -72,11 +82,23 @@ def play_full_game(board_state, current_player_move, model_1_name, model_2_name)
72
  button_values.append(board[i])
73
 
74
  # yield after *every* move
75
- yield (status, board, next_player, *button_values, model_1_comment, model_2_comment)
 
 
 
 
 
 
 
76
 
77
  if game_over:
78
  break
79
 
80
  time.sleep(5) # wait 5 seconds before next move
81
 
82
- current = next_player
 
 
 
 
 
 
1
  import time
2
  from game.logic import ask_model_for_best_move
3
+ from game.stats import record_game
4
 
5
 
6
  WIN_LINES = [
7
+ (0, 1, 2),
8
+ (3, 4, 5),
9
+ (6, 7, 8), # rows
10
+ (0, 3, 6),
11
+ (1, 4, 7),
12
+ (2, 5, 8), # columns
13
+ (0, 4, 8),
14
+ (2, 4, 6), # diagonals
15
  ]
16
 
17
+
18
  def check_winner(board):
19
  """Return 'X', 'O', or None."""
20
  for a, b, c in WIN_LINES:
21
  if board[a] and board[a] == board[b] == board[c]:
22
+ return board[a], (a, b, c)
23
+ return None, None
24
+
25
 
26
  def check_draw(board):
27
  return all(cell is not None for cell in board)
28
 
29
+
30
  def get_valid_move(board, player, model_name):
31
  """Get move from model; if invalid, pick first empty cell as fallback."""
32
  move, comment = ask_model_for_best_move(board, player, model_name)
 
37
  return i, comment
38
  return None, comment
39
 
40
+
41
  def play_full_game(board_state, current_player_move, model_1_name, model_2_name):
42
  board = list(board_state) if board_state else [None] * 9
43
  current = current_player_move
 
59
  model_2_comment = comment
60
 
61
  board[move] = current
62
+ winner, winning_line = check_winner(board)
63
  if winner:
64
  status = f"{winner} wins! ({model})"
65
  game_over = True
 
67
  status = "Draw! No winner."
68
  game_over = True
69
  else:
70
+ status = f"{model} played at position {move + 1}."
71
  game_over = False
72
 
73
  next_player = "O" if current == "X" else "X"
74
+
75
  button_values = []
76
  for i in range(9):
77
  if board[i] is None:
 
82
  button_values.append(board[i])
83
 
84
  # yield after *every* move
85
+ yield (
86
+ status,
87
+ board,
88
+ next_player,
89
+ *button_values,
90
+ model_1_comment,
91
+ model_2_comment,
92
+ )
93
 
94
  if game_over:
95
  break
96
 
97
  time.sleep(5) # wait 5 seconds before next move
98
 
99
+ current = next_player
100
+
101
+ if winner:
102
+ record_game(winner, model_1_name, model_2_name)
103
+ else:
104
+ record_game("Draw", model_1_name, model_2_name)
game/stats.json ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "leaderboard": {
3
+ "Gemini-2.5 flash": 6,
4
+ "GPT-4.1 nano": 2,
5
+ "GPT-4o": 1,
6
+ "Gemini-2.5 flash lite": 2,
7
+ "DeepSeek-V3": 1,
8
+ "GPT-4.1 mini": 1
9
+ },
10
+ "matchups": {
11
+ "Gemini-2.5 flash vs Gemini-2.5 flash lite": {
12
+ "X_wins": 2,
13
+ "O_wins": 0,
14
+ "draws": 0
15
+ },
16
+ "Gemini-2.5 flash lite vs Gemini-2.5 flash": {
17
+ "X_wins": 0,
18
+ "O_wins": 1,
19
+ "draws": 0
20
+ },
21
+ "GPT-4.1 vs Gemini-2.5 flash": {
22
+ "X_wins": 0,
23
+ "O_wins": 1,
24
+ "draws": 0
25
+ },
26
+ "GPT-4.1 mini vs GPT-4.1 nano": {
27
+ "X_wins": 0,
28
+ "O_wins": 2,
29
+ "draws": 0
30
+ },
31
+ "DeepSeek-R1 vs DeepSeek-V3": {
32
+ "X_wins": 0,
33
+ "O_wins": 0,
34
+ "draws": 1
35
+ },
36
+ "Gemini-2.5 flash vs DeepSeek-R1": {
37
+ "X_wins": 1,
38
+ "O_wins": 0,
39
+ "draws": 0
40
+ },
41
+ "GPT-4.1 nano vs Gemini-2.5 flash": {
42
+ "X_wins": 0,
43
+ "O_wins": 1,
44
+ "draws": 0
45
+ },
46
+ "Gemini-2.5 flash lite vs GPT-4o": {
47
+ "X_wins": 1,
48
+ "O_wins": 1,
49
+ "draws": 0
50
+ },
51
+ "Gemini-2.5 flash lite vs GPT-4.1 mini": {
52
+ "X_wins": 1,
53
+ "O_wins": 0,
54
+ "draws": 0
55
+ },
56
+ "DeepSeek-V3 vs GPT-4.1 nano": {
57
+ "X_wins": 1,
58
+ "O_wins": 0,
59
+ "draws": 0
60
+ },
61
+ "GPT-4.1 mini vs GPT-4o": {
62
+ "X_wins": 1,
63
+ "O_wins": 0,
64
+ "draws": 0
65
+ },
66
+ "GPT-4.1 nano vs GPT-4o": {
67
+ "X_wins": 0,
68
+ "O_wins": 0,
69
+ "draws": 1
70
+ }
71
+ },
72
+ "total_games": 14,
73
+ "game_history": [
74
+ {
75
+ "timestamp": "2026-03-18T18:25:00",
76
+ "model_x": "DeepSeek-R1",
77
+ "model_o": "DeepSeek-V3",
78
+ "winner": "Draw"
79
+ },
80
+ {
81
+ "timestamp": "2026-03-18T18:30:00",
82
+ "model_x": "Gemini-2.5 flash",
83
+ "model_o": "DeepSeek-R1",
84
+ "winner": "X"
85
+ },
86
+ {
87
+ "timestamp": "2026-03-19T00:53:53.714735",
88
+ "model_x": "Gemini-2.5 flash",
89
+ "model_o": "Gemini-2.5 flash lite",
90
+ "winner": "X"
91
+ },
92
+ {
93
+ "timestamp": "2026-03-19T01:07:46.372107",
94
+ "model_x": "GPT-4.1 nano",
95
+ "model_o": "Gemini-2.5 flash",
96
+ "winner": "O"
97
+ },
98
+ {
99
+ "timestamp": "2026-03-19T01:08:35.941844",
100
+ "model_x": "Gemini-2.5 flash lite",
101
+ "model_o": "GPT-4o",
102
+ "winner": "O"
103
+ },
104
+ {
105
+ "timestamp": "2026-03-19T01:09:30.275028",
106
+ "model_x": "Gemini-2.5 flash lite",
107
+ "model_o": "GPT-4o",
108
+ "winner": "X"
109
+ },
110
+ {
111
+ "timestamp": "2026-03-19T01:11:01.137755",
112
+ "model_x": "Gemini-2.5 flash lite",
113
+ "model_o": "GPT-4.1 mini",
114
+ "winner": "X"
115
+ },
116
+ {
117
+ "timestamp": "2026-03-19T01:14:38.001316",
118
+ "model_x": "DeepSeek-V3",
119
+ "model_o": "GPT-4.1 nano",
120
+ "winner": "X"
121
+ },
122
+ {
123
+ "timestamp": "2026-03-19T01:19:46.735939",
124
+ "model_x": "GPT-4.1 mini",
125
+ "model_o": "GPT-4o",
126
+ "winner": "X"
127
+ },
128
+ {
129
+ "timestamp": "2026-03-19T01:24:48.844413",
130
+ "model_x": "GPT-4.1 nano",
131
+ "model_o": "GPT-4o",
132
+ "winner": "Draw"
133
+ }
134
+ ]
135
+ }
game/stats.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+
5
+ STATS_FILE = Path(__file__).parent / "stats.json"
6
+
7
+
8
+ def load_stats():
9
+ if STATS_FILE.exists():
10
+ with open(STATS_FILE) as f:
11
+ return json.load(f)
12
+ return {"leaderboard": {}, "matchups": {}, "total_games": 0, "game_history": []}
13
+
14
+
15
+ def save_stats(stats):
16
+ with open(STATS_FILE, "w") as f:
17
+ json.dump(stats, f, indent=2)
18
+
19
+
20
+ def record_game(winner, model_x, model_o):
21
+ stats = load_stats()
22
+
23
+ matchup_key = f"{model_x} vs {model_o}"
24
+ if matchup_key not in stats["matchups"]:
25
+ stats["matchups"][matchup_key] = {"X_wins": 0, "O_wins": 0, "draws": 0}
26
+
27
+ if winner == "X":
28
+ stats["matchups"][matchup_key]["X_wins"] += 1
29
+ stats["leaderboard"][model_x] = stats["leaderboard"].get(model_x, 0) + 1
30
+ elif winner == "O":
31
+ stats["matchups"][matchup_key]["O_wins"] += 1
32
+ stats["leaderboard"][model_o] = stats["leaderboard"].get(model_o, 0) + 1
33
+ else:
34
+ stats["matchups"][matchup_key]["draws"] += 1
35
+
36
+ game_record = {
37
+ "timestamp": datetime.now().isoformat(),
38
+ "model_x": model_x,
39
+ "model_o": model_o,
40
+ "winner": winner,
41
+ }
42
+ stats.setdefault("game_history", [])
43
+ stats["game_history"].append(game_record)
44
+ stats["game_history"] = stats["game_history"][-10:]
45
+
46
+ stats["total_games"] = stats.get("total_games", 0) + 1
47
+
48
+ save_stats(stats)
49
+
50
+
51
+ def get_leaderboard_data():
52
+ stats = load_stats()
53
+ leaderboard = stats.get("leaderboard", {})
54
+ return [
55
+ [model, wins]
56
+ for model, wins in sorted(leaderboard.items(), key=lambda x: x[1], reverse=True)
57
+ ]
58
+
59
+
60
+ def get_total_games():
61
+ stats = load_stats()
62
+ return stats.get("total_games", 0)
63
+
64
+
65
+ def get_last_10_games():
66
+ stats = load_stats()
67
+ history = stats.get("game_history", [])[-10:]
68
+ result = []
69
+ for game in reversed(history):
70
+ winner_label = game["winner"] if game["winner"] in ("X", "O") else "Draw"
71
+ result.append(
72
+ f"{game['model_x']} (X) vs {game['model_o']} (O) β†’ {winner_label}"
73
+ )
74
+ return result
models/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/models/__pycache__/__init__.cpython-312.pyc and b/models/__pycache__/__init__.cpython-312.pyc differ
 
models/__pycache__/provider_constant.cpython-312.pyc CHANGED
Binary files a/models/__pycache__/provider_constant.cpython-312.pyc and b/models/__pycache__/provider_constant.cpython-312.pyc differ
 
models/__pycache__/registry.cpython-312.pyc CHANGED
Binary files a/models/__pycache__/registry.cpython-312.pyc and b/models/__pycache__/registry.cpython-312.pyc differ
 
models/provider_constant.py CHANGED
@@ -7,4 +7,6 @@ PROVIDER_CONSTANT = {
7
  "MISTRAL": "mistral",
8
  "HUGGINGFACE": "huggingface",
9
  "QWEN": "qwen",
10
- }
 
 
 
7
  "MISTRAL": "mistral",
8
  "HUGGINGFACE": "huggingface",
9
  "QWEN": "qwen",
10
+ "OPENAI": "openai",
11
+ "DEEPSEEK": "deepseek",
12
+ }
models/registry.py CHANGED
@@ -1,9 +1,50 @@
1
  from models.provider_constant import PROVIDER_CONSTANT
2
 
3
  models = [
4
- {"name": "Llama-3.2", "value": "llama3.2:latest", "provider": PROVIDER_CONSTANT["OLLAMA"]},
5
- {"name": "Gemini-2.5 flash", "value": "gemini-2.5-flash", "provider": PROVIDER_CONSTANT["GOOGLE"]},
6
- {"name": "Gemini-2.5 flash lite", "value": "gemini-2.5-flash-lite", "provider": PROVIDER_CONSTANT["GOOGLE"]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  # {"name": "Deepseek-r1:8b", "value": "deepseek-r1:8b", "provider": PROVIDER_CONSTANT["OLLAMA"]},
8
  # {"name": "TinyLlama-1.1B-Chat", "value": "TinyLlama/TinyLlama-1.1B-Chat-v1.0", "provider": PROVIDER_CONSTANT["TINYLLAMA"]},
9
  # {"name": "Gemma-2-2B", "value": "google/gemma-2-2b", "provider": PROVIDER_CONSTANT["GOOGLE"]},
@@ -11,4 +52,4 @@ models = [
11
  # {"name": "Qwen2-0.5B-Instruct", "value": "Qwen/Qwen2-0.5B-Instruct", "provider": PROVIDER_CONSTANT["QWEN"]},
12
  # {"name": "SmolLM-360M-Instruct", "value": "HuggingFaceTB/SmolLM-360M-Instruct", "provider": PROVIDER_CONSTANT["HUGGINGFACE"]},
13
  # {"name": "Mistral-7B-Instruct", "value": "mistralai/Mistral-7B-Instruct-v0.2", "provider": PROVIDER_CONSTANT["MISTRAL"]},
14
- ]
 
1
  from models.provider_constant import PROVIDER_CONSTANT
2
 
3
  models = [
4
+ # {"name": "Llama-3.2", "value": "llama3.2:latest", "provider": PROVIDER_CONSTANT["OLLAMA"]},
5
+ {
6
+ "name": "Gemini-2.5 flash",
7
+ "value": "gemini-2.5-flash",
8
+ "provider": PROVIDER_CONSTANT["GOOGLE"],
9
+ },
10
+ {
11
+ "name": "Gemini-2.5 flash lite",
12
+ "value": "gemini-2.5-flash-lite",
13
+ "provider": PROVIDER_CONSTANT["GOOGLE"],
14
+ },
15
+ {
16
+ "name": "GPT-4.1",
17
+ "value": "openai/gpt-4.1",
18
+ "provider": PROVIDER_CONSTANT["OPENAI"],
19
+ },
20
+ {
21
+ "name": "GPT-4.1 mini",
22
+ "value": "openai/gpt-4.1-mini",
23
+ "provider": PROVIDER_CONSTANT["OPENAI"],
24
+ },
25
+ {
26
+ "name": "GPT-4.1 nano",
27
+ "value": "openai/gpt-4.1-nano",
28
+ "provider": PROVIDER_CONSTANT["OPENAI"],
29
+ },
30
+ {
31
+ "name": "GPT-4o",
32
+ "value": "openai/gpt-4o",
33
+ "provider": PROVIDER_CONSTANT["OPENAI"],
34
+ },
35
+ {
36
+ "name": "DeepSeek-R1",
37
+ "value": "deepseek/DeepSeek-R1",
38
+ "provider": PROVIDER_CONSTANT["DEEPSEEK"],
39
+ },
40
+ {
41
+ "name": "DeepSeek-V3",
42
+ "value": "deepseek/DeepSeek-V3-0324",
43
+ "provider": PROVIDER_CONSTANT["DEEPSEEK"],
44
+ },
45
+ # {"name": "o3", "value": "openai/o3", "provider": PROVIDER_CONSTANT["OPENAI"]},
46
+ # {"name": "o4-mini", "value": "openai/o4-mini", "provider": PROVIDER_CONSTANT["OPENAI"]},
47
+ # {"name": "o3-mini", "value": "openai/o3-mini", "provider": PROVIDER_CONSTANT["OPENAI"]},
48
  # {"name": "Deepseek-r1:8b", "value": "deepseek-r1:8b", "provider": PROVIDER_CONSTANT["OLLAMA"]},
49
  # {"name": "TinyLlama-1.1B-Chat", "value": "TinyLlama/TinyLlama-1.1B-Chat-v1.0", "provider": PROVIDER_CONSTANT["TINYLLAMA"]},
50
  # {"name": "Gemma-2-2B", "value": "google/gemma-2-2b", "provider": PROVIDER_CONSTANT["GOOGLE"]},
 
52
  # {"name": "Qwen2-0.5B-Instruct", "value": "Qwen/Qwen2-0.5B-Instruct", "provider": PROVIDER_CONSTANT["QWEN"]},
53
  # {"name": "SmolLM-360M-Instruct", "value": "HuggingFaceTB/SmolLM-360M-Instruct", "provider": PROVIDER_CONSTANT["HUGGINGFACE"]},
54
  # {"name": "Mistral-7B-Instruct", "value": "mistralai/Mistral-7B-Instruct-v0.2", "provider": PROVIDER_CONSTANT["MISTRAL"]},
55
+ ]
requirements.txt CHANGED
@@ -7,6 +7,9 @@ annotated-doc==0.0.4
7
  annotated-types==0.7.0
8
  ansi2html==1.9.2
9
  anthropic==0.76.0
 
 
 
10
  anyio==4.12.1
11
  appnope==0.1.4
12
  asttokens==3.0.1
@@ -59,7 +62,6 @@ google-auth-httplib2==0.3.0
59
  google-genai==1.60.0
60
  google-generativeai==0.8.6
61
  googleapis-common-protos==1.72.0
62
- gradio==6.4.0
63
  gradio-client==2.0.3
64
  groovy==0.1.2
65
  grpcio==1.76.0
 
7
  annotated-types==0.7.0
8
  ansi2html==1.9.2
9
  anthropic==0.76.0
10
+ altair
11
+ azure-ai-inference
12
+ azure-core
13
  anyio==4.12.1
14
  appnope==0.1.4
15
  asttokens==3.0.1
 
62
  google-genai==1.60.0
63
  google-generativeai==0.8.6
64
  googleapis-common-protos==1.72.0
 
65
  gradio-client==2.0.3
66
  groovy==0.1.2
67
  grpcio==1.76.0
ui/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/ui/__pycache__/__init__.cpython-312.pyc and b/ui/__pycache__/__init__.cpython-312.pyc differ
 
ui/__pycache__/bindings.cpython-312.pyc CHANGED
Binary files a/ui/__pycache__/bindings.cpython-312.pyc and b/ui/__pycache__/bindings.cpython-312.pyc differ
 
ui/__pycache__/components.cpython-312.pyc CHANGED
Binary files a/ui/__pycache__/components.cpython-312.pyc and b/ui/__pycache__/components.cpython-312.pyc differ
 
ui/bindings.py CHANGED
@@ -1,5 +1,6 @@
1
  from game.start_game import play_full_game
2
 
 
3
  def bind_events(
4
  start_button,
5
  reset_button,
@@ -10,32 +11,34 @@ def bind_events(
10
  current_player_move,
11
  board_buttons,
12
  model_1_comment,
13
- model_2_comment
 
 
 
 
14
  ):
15
-
16
  start_button.click(
17
  fn=play_full_game,
18
  inputs=[board_state, current_player_move, model_1_dropdown, model_2_dropdown],
19
- outputs=[status_text, board_state, current_player_move] + flatten_buttons(board_buttons) + [model_1_comment, model_2_comment]
 
 
 
 
 
20
  )
21
 
22
-
23
  def reset_game():
24
- return (
25
- "Click 'Start Game' to begin!",
26
- [None] * 9,
27
- "X", # default current_player_move
28
- *[" "]*9
29
- )
30
 
31
  reset_button.click(
32
  fn=reset_game,
33
  inputs=[],
34
- outputs=[status_text, board_state, current_player_move] + flatten_buttons(board_buttons)
 
35
  )
36
 
37
 
38
- # Helper to unpack UI buttons as flat array
39
  def flatten_buttons(board_buttons):
40
- """ Convert 3x3 β†’ 1x9 """
41
  return [btn for row in board_buttons for btn in row]
 
1
  from game.start_game import play_full_game
2
 
3
+
4
  def bind_events(
5
  start_button,
6
  reset_button,
 
11
  current_player_move,
12
  board_buttons,
13
  model_1_comment,
14
+ model_2_comment,
15
+ total_games_html=None,
16
+ last_games_html=None,
17
+ leaderboard_plot=None,
18
+ refresh_leaderboard=None,
19
  ):
 
20
  start_button.click(
21
  fn=play_full_game,
22
  inputs=[board_state, current_player_move, model_1_dropdown, model_2_dropdown],
23
+ outputs=[status_text, board_state, current_player_move]
24
+ + flatten_buttons(board_buttons)
25
+ + [model_1_comment, model_2_comment],
26
+ ).then(
27
+ fn=refresh_leaderboard,
28
+ outputs=[total_games_html, last_games_html, leaderboard_plot],
29
  )
30
 
 
31
  def reset_game():
32
+ return ("Click 'Start Game' to begin!", [None] * 9, "X", *[" "] * 9)
 
 
 
 
 
33
 
34
  reset_button.click(
35
  fn=reset_game,
36
  inputs=[],
37
+ outputs=[status_text, board_state, current_player_move]
38
+ + flatten_buttons(board_buttons),
39
  )
40
 
41
 
 
42
  def flatten_buttons(board_buttons):
43
+ """Convert 3x3 β†’ 1x9"""
44
  return [btn for row in board_buttons for btn in row]
ui/components.py CHANGED
@@ -1,13 +1,70 @@
1
  import gradio as gr
 
 
2
  from models.registry import models
3
  from ui.bindings import bind_events
 
4
 
5
  model_choices = [model["name"] for model in models]
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def create_ui():
8
  with gr.Blocks(css=open("ui/styles.css").read()) as demo:
9
  gr.Markdown("# 🎲 Tic-Tac-Toe Game")
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  with gr.Row():
12
  with gr.Column(scale=1):
13
  model_1_dropdown = gr.Dropdown(
@@ -27,7 +84,7 @@ def create_ui():
27
  with gr.Column(scale=2):
28
  board_buttons = []
29
  board_state = gr.State([None] * 9)
30
- current_player_move = gr.State("X")
31
 
32
  for r in range(3):
33
  row_buttons = []
@@ -37,7 +94,7 @@ def create_ui():
37
  value=" ",
38
  elem_classes=["tic-cell", "tic-btn"],
39
  elem_id=f"cell-{r}-{c}",
40
- interactive=False
41
  )
42
  row_buttons.append(btn)
43
  board_buttons.append(row_buttons)
@@ -45,18 +102,18 @@ def create_ui():
45
  status_text = gr.Textbox(
46
  label="Game Status",
47
  value="Click 'Start Game' to begin the battle!",
48
- interactive=False
49
  )
50
 
51
- start_button = gr.Button("Start Game",variant="primary")
52
- reset_button = gr.Button("Reset Game",variant="stop")
53
 
54
  with gr.Column(scale=1):
55
  model_2_dropdown = gr.Dropdown(
56
  choices=model_choices,
57
  value=models[0]["name"],
58
  label="Select Model for Player 2 (O)",
59
- interactive=True
60
  )
61
 
62
  model_2_comment = gr.Textbox(
@@ -65,6 +122,15 @@ def create_ui():
65
  interactive=False,
66
  lines=3,
67
  )
 
 
 
 
 
 
 
 
 
68
  bind_events(
69
  start_button,
70
  reset_button,
@@ -75,7 +141,11 @@ def create_ui():
75
  current_player_move,
76
  board_buttons,
77
  model_1_comment,
78
- model_2_comment
 
 
 
 
79
  )
80
 
81
- return demo
 
1
  import gradio as gr
2
+ import altair as alt
3
+ import pandas as pd
4
  from models.registry import models
5
  from ui.bindings import bind_events
6
+ from game.stats import get_leaderboard_data, get_total_games, get_last_10_games
7
 
8
  model_choices = [model["name"] for model in models]
9
 
10
+
11
+ def create_leaderboard_chart():
12
+ data = get_leaderboard_data()
13
+ if not data:
14
+ return None
15
+ df = pd.DataFrame(data, columns=["Model", "Wins"])
16
+ chart = (
17
+ alt.Chart(df)
18
+ .mark_bar(color="#4c78a8")
19
+ .encode(
20
+ x=alt.X("Wins:Q", title="Total Wins"),
21
+ y=alt.Y("Model:N", sort="-x", title="Model"),
22
+ tooltip=["Model:N", "Wins:Q"],
23
+ )
24
+ .properties(title="Model Wins Leaderboard", width=400, height=200)
25
+ )
26
+ return chart
27
+
28
+
29
+ def format_total_games():
30
+ total = get_total_games()
31
+ return f"<p style='margin-bottom:10px;color:#333;'><b>Total Games: {total}</b></p>"
32
+
33
+
34
+ def format_last_games():
35
+ games = get_last_10_games()
36
+ if not games:
37
+ return "<p style='color:#666;'>No games played yet</p>"
38
+
39
+ html = "<div style='font-size:13px;'>"
40
+ for i, game in enumerate(games, 1):
41
+ html += f"<p style='margin:5px 0;padding:5px;background:#f5f5f5;border-radius:4px;color:#333;'>{i}. {game}</p>"
42
+ html += "</div>"
43
+ return html
44
+
45
+
46
+ def refresh_leaderboard():
47
+ return format_total_games(), format_last_games(), create_leaderboard_chart()
48
+
49
+
50
  def create_ui():
51
  with gr.Blocks(css=open("ui/styles.css").read()) as demo:
52
  gr.Markdown("# 🎲 Tic-Tac-Toe Game")
53
 
54
+ leaderboard_btn = gr.Button("πŸ† Leaderboard", variant="secondary", size="sm")
55
+
56
+ with gr.Row(visible=False) as leaderboard_row:
57
+ with gr.Column(scale=1):
58
+ total_games_html = gr.HTML(value=format_total_games)
59
+ gr.Markdown("### πŸ… Last 10 Games")
60
+ last_games_html = gr.HTML(value=format_last_games)
61
+ with gr.Column(scale=1):
62
+ gr.Markdown("### πŸ“Š Wins Leaderboard")
63
+ leaderboard_plot = gr.Plot(value=create_leaderboard_chart)
64
+
65
+ def toggle_leaderboard(visible):
66
+ return gr.Row(visible=not visible)
67
+
68
  with gr.Row():
69
  with gr.Column(scale=1):
70
  model_1_dropdown = gr.Dropdown(
 
84
  with gr.Column(scale=2):
85
  board_buttons = []
86
  board_state = gr.State([None] * 9)
87
+ current_player_move = gr.State("X")
88
 
89
  for r in range(3):
90
  row_buttons = []
 
94
  value=" ",
95
  elem_classes=["tic-cell", "tic-btn"],
96
  elem_id=f"cell-{r}-{c}",
97
+ interactive=False,
98
  )
99
  row_buttons.append(btn)
100
  board_buttons.append(row_buttons)
 
102
  status_text = gr.Textbox(
103
  label="Game Status",
104
  value="Click 'Start Game' to begin the battle!",
105
+ interactive=False,
106
  )
107
 
108
+ start_button = gr.Button("Start Game", variant="primary")
109
+ reset_button = gr.Button("Reset Game", variant="stop")
110
 
111
  with gr.Column(scale=1):
112
  model_2_dropdown = gr.Dropdown(
113
  choices=model_choices,
114
  value=models[0]["name"],
115
  label="Select Model for Player 2 (O)",
116
+ interactive=True,
117
  )
118
 
119
  model_2_comment = gr.Textbox(
 
122
  interactive=False,
123
  lines=3,
124
  )
125
+
126
+ leaderboard_state = gr.State(False)
127
+
128
+ leaderboard_btn.click(
129
+ fn=toggle_leaderboard, inputs=[leaderboard_state], outputs=[leaderboard_row]
130
+ ).then(
131
+ fn=lambda x: not x, inputs=[leaderboard_state], outputs=[leaderboard_state]
132
+ )
133
+
134
  bind_events(
135
  start_button,
136
  reset_button,
 
141
  current_player_move,
142
  board_buttons,
143
  model_1_comment,
144
+ model_2_comment,
145
+ total_games_html,
146
+ last_games_html,
147
+ leaderboard_plot,
148
+ refresh_leaderboard,
149
  )
150
 
151
+ return demo