ed-donner commited on
Commit
70830d6
·
1 Parent(s): 28eed69

Repackaged arena from original repo for Spaces deployment

Browse files
Files changed (9) hide show
  1. app.py +2 -6
  2. arena/__init__.py +0 -0
  3. arena/board.py +117 -0
  4. arena/board_view.py +132 -0
  5. arena/c4.py +198 -0
  6. arena/game.py +28 -0
  7. arena/llm.py +383 -0
  8. arena/player.py +126 -0
  9. prototype.ipynb +0 -0
app.py CHANGED
@@ -1,11 +1,7 @@
1
- import gradio as gr
2
 
3
 
4
- def greet(name):
5
- return f"Hello, {name}!"
6
-
7
-
8
- app = gr.Interface(fn=greet, inputs="text", outputs="text")
9
 
10
  if __name__ == "__main__":
11
  app.launch()
 
1
+ from arena.c4 import make_display
2
 
3
 
4
+ app = make_display()
 
 
 
 
5
 
6
  if __name__ == "__main__":
7
  app.launch()
arena/__init__.py ADDED
File without changes
arena/board.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from arena.board_view import to_svg
2
+
3
+ RED = 1
4
+ YELLOW = -1
5
+ EMPTY = 0
6
+ show = {EMPTY: "⚪️", RED: "🔴", YELLOW: "🟡"}
7
+ pieces = {EMPTY: "", RED: "red", YELLOW: "yellow"}
8
+ simple = {EMPTY: ".", RED: "R", YELLOW: "Y"}
9
+ cols = "ABCDEFG"
10
+
11
+
12
+ class Board:
13
+
14
+ def __init__(self):
15
+ self.cells = [[0 for _ in range(7)] for _ in range(6)]
16
+ self.player = RED
17
+ self.winner = EMPTY
18
+ self.draw = False
19
+ self.forfeit = False
20
+ self.latest_x, self.latest_y = -1, -1
21
+
22
+ def __repr__(self):
23
+ result = ""
24
+ for y in range(6):
25
+ for x in range(7):
26
+ result += show[self.cells[5 - y][x]]
27
+ result += "\n"
28
+ result += "\n" + self.message()
29
+ return result
30
+
31
+ def message(self):
32
+ if self.winner and self.forfeit:
33
+ return f"{show[self.winner]} wins after an illegal move by {show[-1*self.winner]}\n"
34
+ elif self.winner:
35
+ return f"{show[self.winner]} wins\n"
36
+ elif self.draw:
37
+ return "The game is a draw\n"
38
+ else:
39
+ return f"{show[self.player]} to play\n"
40
+
41
+ def html(self):
42
+ result = '<div style="text-align: center;font-size:24px">'
43
+ result += self.__repr__().replace("\n", "<br/>")
44
+ result += "</div>"
45
+ return result
46
+
47
+ def svg(self):
48
+ """Convert the board state to an SVG representation"""
49
+ return to_svg(self)
50
+
51
+ def json(self):
52
+ result = "{\n"
53
+ result += ' "Column names": ["A", "B", "C", "D", "E", "F", "G"],\n'
54
+ for y in range(6):
55
+ result += f' "Row {6-y}": ['
56
+ for x in range(7):
57
+ result += f'"{pieces[self.cells[5-y][x]]}", '
58
+ result = result[:-2] + "],\n"
59
+ result = result[:-2] + "\n}"
60
+ return result
61
+
62
+ def alternative(self):
63
+ result = "ABCDEFG\n"
64
+ for y in range(6):
65
+ for x in range(7):
66
+ result += simple[self.cells[5 - y][x]]
67
+ result += "\n"
68
+ return result
69
+
70
+ def height(self, x):
71
+ height = 0
72
+ while height < 6 and self.cells[height][x] != EMPTY:
73
+ height += 1
74
+ return height
75
+
76
+ def legal_moves(self):
77
+ return [cols[x] for x in range(7) if self.height(x) < 6]
78
+
79
+ def illegal_moves(self):
80
+ return [cols[x] for x in range(7) if self.height(x) == 6]
81
+
82
+ def winning_line(self, x, y, dx, dy):
83
+ color = self.cells[y][x]
84
+ for pointer in range(1, 4):
85
+ xp = x + dx * pointer
86
+ yp = y + dy * pointer
87
+ if not (0 <= xp <= 6 and 0 <= yp <= 5) or self.cells[yp][xp] != color:
88
+ return EMPTY
89
+ return color
90
+
91
+ def winning_cell(self, x, y):
92
+ for dx, dy in ((0, 1), (1, 1), (1, 0), (1, -1)):
93
+ if winner := self.winning_line(x, y, dx, dy):
94
+ return winner
95
+ return EMPTY
96
+
97
+ def wins(self):
98
+ for y in range(6):
99
+ for x in range(7):
100
+ if winner := self.winning_cell(x, y):
101
+ return winner
102
+ return EMPTY
103
+
104
+ def move(self, x):
105
+ y = self.height(x)
106
+ self.cells[y][x] = self.player
107
+ self.latest_x, self.latest_y = x, y
108
+ if winner := self.wins():
109
+ self.winner = winner
110
+ elif not self.legal_moves:
111
+ self.draw = True
112
+ else:
113
+ self.player = -1 * self.player
114
+ return self
115
+
116
+ def is_active(self):
117
+ return not self.winner and not self.draw
arena/board_view.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ RED = 1
2
+ YELLOW = -1
3
+ EMPTY = 0
4
+
5
+ def to_svg(board):
6
+ """Convert the board state to an SVG representation"""
7
+ svg = '''
8
+ <div style="display: flex; justify-content: center;">
9
+ <svg width="450" height="420" viewBox="0 0 450 420">
10
+ <!-- Definitions for gradients and clips -->
11
+ <defs>
12
+ <radialGradient id="redGradient" cx="0.5" cy="0.3" r="0.7">
13
+ <stop offset="0%" stop-color="#ff6666"/>
14
+ <stop offset="100%" stop-color="#cc0000"/>
15
+ </radialGradient>
16
+ <radialGradient id="yellowGradient" cx="0.5" cy="0.3" r="0.7">
17
+ <stop offset="0%" stop-color="#ffff88"/>
18
+ <stop offset="100%" stop-color="#cccc00"/>
19
+ </radialGradient>
20
+ <linearGradient id="emptyGradient" x1="0" y1="0" x2="0" y2="1">
21
+ <stop offset="0%" stop-color="#ffffff"/>
22
+ <stop offset="100%" stop-color="#e0e0e0"/>
23
+ </linearGradient>
24
+
25
+ <!-- Define the mask for the holes -->
26
+ <mask id="holes">
27
+ <rect x="25" y="25" width="400" height="320" fill="white"/>
28
+ '''
29
+ # Add the holes to the mask
30
+ svg += ''.join(f'''
31
+ <circle
32
+ cx="{(x * 50) + 75}"
33
+ cy="{(y * 50) + 60}"
34
+ r="20"
35
+ fill="black"
36
+ />
37
+ '''
38
+ for y in range(6)
39
+ for x, cell in enumerate(board.cells[5-y])
40
+ )
41
+
42
+ svg += '''
43
+ </mask>
44
+ </defs>
45
+
46
+ <!-- Stand -->
47
+ <path d="M0 360 L25 300 H425 L450 360 L425 385 H25 Z" fill="#004fa3"/>
48
+
49
+ <!-- Game pieces (will show through the holes) -->
50
+ '''
51
+
52
+ # Add pieces
53
+ svg += ''.join(f'''
54
+ <circle
55
+ class="{f'new-piece' if x == board.latest_x and y == (5-board.latest_y) else ''}"
56
+ cx="{(x * 50) + 75}"
57
+ cy="{(y * 50) + 60}"
58
+ r="20"
59
+ fill="{
60
+ 'url(#redGradient)' if (cell == RED) else
61
+ 'url(#yellowGradient)' if (cell == YELLOW) else
62
+ 'none'
63
+ }"
64
+ stroke="{
65
+ '#cc0000' if (cell == RED) else
66
+ '#cccc00' if (cell == YELLOW) else
67
+ 'none'
68
+ }"
69
+ stroke-width="1"
70
+ />
71
+ <circle
72
+ class="{f'new-piece-highlight' if x == board.latest_x and y == (5-board.latest_y) else ''}"
73
+ cx="{(x * 50) + 75 - 5}"
74
+ cy="{(y * 50) + 60 - 5}"
75
+ r="8"
76
+ fill="{
77
+ '#ff8888' if (cell == RED) else
78
+ '#ffff99' if (cell == YELLOW) else
79
+ 'none'
80
+ }"
81
+ opacity="0.3"
82
+ />
83
+ '''
84
+ for y in range(6)
85
+ for x, cell in enumerate(board.cells[5-y])
86
+ if cell != EMPTY
87
+ )
88
+
89
+ svg += '''
90
+
91
+ <!-- Board overlay with holes -->
92
+ <rect x="25" y="25" width="400" height="320" fill="#0066cc" rx="10" mask="url(#holes)"/>
93
+
94
+ <!-- Hole borders (on top of everything for better 3D effect) -->
95
+ '''
96
+
97
+ # Add hole borders on top
98
+ svg += ''.join(f'''
99
+ <circle
100
+ cx="{(x * 50) + 75}"
101
+ cy="{(y * 50) + 60}"
102
+ r="20"
103
+ fill="none"
104
+ stroke="#005ab3"
105
+ stroke-width="2"
106
+ />
107
+ '''
108
+ for y in range(6)
109
+ for x, cell in enumerate(board.cells[5-y])
110
+ )
111
+
112
+ svg += '''
113
+ </svg>
114
+ </div>
115
+ <style>
116
+ .new-piece {
117
+ animation: dropPiece 0.5s cubic-bezier(0.95, 0.05, 1, 0.5);
118
+ }
119
+ .new-piece-highlight {
120
+ animation: dropPiece 0.5s cubic-bezier(0.95, 0.05, 1, 0.5);
121
+ }
122
+ @keyframes dropPiece {
123
+ from {
124
+ transform: translateY(-300px);
125
+ }
126
+ to {
127
+ transform: translateY(0);
128
+ }
129
+ }
130
+ </style>
131
+ '''
132
+ return svg
arena/c4.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from arena.game import Game
2
+ from arena.board import RED, YELLOW
3
+ from arena.llm import LLM
4
+ import gradio as gr
5
+
6
+ all_model_names = LLM.all_model_names()
7
+
8
+ css = "footer{display:none !important}"
9
+
10
+ js = """
11
+ function refresh() {
12
+ const url = new URL(window.location);
13
+
14
+ if (url.searchParams.get('__theme') !== 'dark') {
15
+ url.searchParams.set('__theme', 'dark');
16
+ window.location.href = url.href;
17
+ }
18
+ }
19
+ """
20
+
21
+
22
+ def message_html(game):
23
+ return (
24
+ f'<div style="text-align: center;font-size:18px">{game.board.message()}</div>'
25
+ )
26
+
27
+
28
+ def load_callback(red_llm, yellow_llm):
29
+ game = Game(red_llm, yellow_llm)
30
+ enabled = gr.Button(interactive=True)
31
+ message = message_html(game)
32
+ return game, game.board.svg(), message, "", "", enabled, enabled, enabled
33
+
34
+
35
+ def move_callback(game):
36
+ game.move()
37
+ message = message_html(game)
38
+ if_active = gr.Button(interactive=game.board.is_active())
39
+ return (
40
+ game,
41
+ game.board.svg(),
42
+ message,
43
+ game.thoughts(RED),
44
+ game.thoughts(YELLOW),
45
+ if_active,
46
+ if_active,
47
+ )
48
+
49
+
50
+ def run_callback(game):
51
+ enabled = gr.Button(interactive=True)
52
+ disabled = gr.Button(interactive=False)
53
+ message = message_html(game)
54
+ yield game, game.board.svg(), message, game.thoughts(RED), game.thoughts(
55
+ YELLOW
56
+ ), disabled, disabled, disabled
57
+ while game.board.is_active():
58
+ game.move()
59
+ message = message_html(game)
60
+ yield game, game.board.svg(), message, game.thoughts(RED), game.thoughts(
61
+ YELLOW
62
+ ), disabled, disabled, disabled
63
+ yield game, game.board.svg(), message, game.thoughts(RED), game.thoughts(
64
+ YELLOW
65
+ ), disabled, disabled, enabled
66
+
67
+
68
+ def model_callback(player_name, game, new_model_name):
69
+ player = game.players[player_name]
70
+ player.switch_model(new_model_name)
71
+ return game
72
+
73
+
74
+ def red_model_callback(game, new_model_name):
75
+ return model_callback(RED, game, new_model_name)
76
+
77
+
78
+ def yellow_model_callback(game, new_model_name):
79
+ return model_callback(YELLOW, game, new_model_name)
80
+
81
+
82
+ def player_section(name, default):
83
+ with gr.Row():
84
+ gr.Markdown(
85
+ f'<div style="text-align: center;font-size:18px">{name} Player</div>'
86
+ )
87
+ with gr.Row():
88
+ dropdown = gr.Dropdown(
89
+ all_model_names, value=default, label="LLM", interactive=True
90
+ )
91
+ with gr.Row():
92
+ gr.Markdown(
93
+ f'<div style="text-align: center;font-size:16px">Inner thoughts</div>'
94
+ )
95
+ with gr.Row():
96
+ thoughts = gr.Markdown(label="Thoughts")
97
+ return thoughts, dropdown
98
+
99
+
100
+ def make_display():
101
+ with gr.Blocks(
102
+ title="C4 Battle",
103
+ css=css,
104
+ js=js,
105
+ theme=gr.themes.Default(primary_hue="sky"),
106
+ ) as blocks:
107
+
108
+ game = gr.State()
109
+
110
+ with gr.Row():
111
+ gr.Markdown(
112
+ '<div style="text-align: center;font-size:24px">Four-in-a-row LLM Showdown</div>'
113
+ )
114
+ with gr.Row():
115
+ with gr.Column(scale=1):
116
+ red_thoughts, red_dropdown = player_section("Red", "gpt-4o")
117
+ with gr.Column(scale=2):
118
+ with gr.Row():
119
+ message = gr.Markdown(
120
+ '<div style="text-align: center;font-size:18px">The Board</div>'
121
+ )
122
+ with gr.Row():
123
+ board_display = gr.HTML()
124
+ with gr.Row():
125
+ with gr.Column(scale=1):
126
+ move_button = gr.Button("Next move")
127
+ with gr.Column(scale=1):
128
+ run_button = gr.Button("Run game", variant="primary")
129
+ with gr.Column(scale=1):
130
+ reset_button = gr.Button("Start Over", variant="stop")
131
+ with gr.Column(scale=1):
132
+ yellow_thoughts, yellow_dropdown = player_section(
133
+ "Yellow", "claude-3-5-sonnet-latest"
134
+ )
135
+
136
+ blocks.load(
137
+ load_callback,
138
+ inputs=[red_dropdown, yellow_dropdown],
139
+ outputs=[
140
+ game,
141
+ board_display,
142
+ message,
143
+ red_thoughts,
144
+ yellow_thoughts,
145
+ move_button,
146
+ run_button,
147
+ reset_button,
148
+ ],
149
+ )
150
+ move_button.click(
151
+ move_callback,
152
+ inputs=[game],
153
+ outputs=[
154
+ game,
155
+ board_display,
156
+ message,
157
+ red_thoughts,
158
+ yellow_thoughts,
159
+ move_button,
160
+ run_button,
161
+ ],
162
+ )
163
+ red_dropdown.change(
164
+ red_model_callback, inputs=[game, red_dropdown], outputs=[game]
165
+ )
166
+ yellow_dropdown.change(
167
+ yellow_model_callback, inputs=[game, yellow_dropdown], outputs=[game]
168
+ )
169
+ run_button.click(
170
+ run_callback,
171
+ inputs=[game],
172
+ outputs=[
173
+ game,
174
+ board_display,
175
+ message,
176
+ red_thoughts,
177
+ yellow_thoughts,
178
+ move_button,
179
+ run_button,
180
+ reset_button,
181
+ ],
182
+ )
183
+ reset_button.click(
184
+ load_callback,
185
+ inputs=[red_dropdown, yellow_dropdown],
186
+ outputs=[
187
+ game,
188
+ board_display,
189
+ message,
190
+ red_thoughts,
191
+ yellow_thoughts,
192
+ move_button,
193
+ run_button,
194
+ reset_button,
195
+ ],
196
+ )
197
+
198
+ return blocks
arena/game.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from arena.board import Board, RED, YELLOW, EMPTY, pieces
2
+ from arena.player import Player
3
+ from dotenv import load_dotenv
4
+
5
+
6
+ class Game:
7
+
8
+ def __init__(self, model_red, model_yellow):
9
+ load_dotenv(override=True)
10
+ self.board = Board()
11
+ self.players = {
12
+ RED: Player(model_red, RED),
13
+ YELLOW: Player(model_yellow, YELLOW),
14
+ }
15
+
16
+ def move(self):
17
+ self.players[self.board.player].move(self.board)
18
+
19
+ def is_active(self):
20
+ return self.board.is_active()
21
+
22
+ def thoughts(self, player):
23
+ return self.players[player].thoughts()
24
+
25
+ def run(self):
26
+ while self.is_active():
27
+ self.move()
28
+ print(self.board)
arena/llm.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC
2
+ from anthropic import Anthropic
3
+ from openai import OpenAI
4
+ from groq import Groq
5
+ import logging
6
+ from typing import Dict, Type, Self, List
7
+ import os
8
+ import time
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class LLMException(Exception):
14
+ pass
15
+
16
+
17
+ class LLM(ABC):
18
+ """
19
+ An abstract superclass for interacting with LLMs - subclass for Claude and GPT
20
+ """
21
+
22
+ model_names = []
23
+
24
+ def __init__(self, model_name: str, temperature: float):
25
+ self.model_name = model_name
26
+ self.client = None
27
+ self.temperature = temperature
28
+
29
+ def send(self, system: str, user: str, max_tokens: int = 3000) -> str:
30
+ """
31
+ Send a message
32
+ :param system: the context in which this message is to be taken
33
+ :param user: the prompt
34
+ :param max_tokens: max number of tokens to generate
35
+ :return: the response from the AI
36
+ """
37
+ print("_____")
38
+ print(f"Calling {self.model_name}")
39
+ print("System prompt:\n" + system)
40
+ print("User prompt:\n" + user)
41
+ result = self.protected_send(system, user, max_tokens)
42
+ print("Response:\n" + result)
43
+ print("_____")
44
+ left = result.find("{")
45
+ right = result.rfind("}")
46
+ if left > -1 and right > -1:
47
+ result = result[left : right + 1]
48
+ return result
49
+
50
+ def protected_send(self, system: str, user: str, max_tokens: int = 3000) -> str:
51
+ retries = 5
52
+ done = False
53
+ while retries:
54
+ retries -= 1
55
+ try:
56
+ return self._send(system, user, max_tokens)
57
+ except Exception as e:
58
+ print(f"Exception on calling LLM of {e}")
59
+ if retries:
60
+ print("Waiting 2s and retrying")
61
+ time.sleep(2)
62
+ return "{}"
63
+
64
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
65
+ pass
66
+
67
+ @classmethod
68
+ def model_map(cls) -> Dict[str, Type[Self]]:
69
+ """
70
+ Generate a mapping of Model Names to LLM classes, by looking at all subclasses of this one
71
+ :return: a mapping dictionary from model name to LLM subclass
72
+ """
73
+ mapping = {}
74
+ for llm in cls.__subclasses__():
75
+ for model_name in llm.model_names:
76
+ mapping[model_name] = llm
77
+ return mapping
78
+
79
+ @classmethod
80
+ def all_model_names(cls) -> List[str]:
81
+ return cls.model_map().keys()
82
+
83
+ @classmethod
84
+ def create(cls, model_name: str, temperature: float = 0.5) -> Self:
85
+ """
86
+ Return an instance of a subclass that corresponds to this model_name
87
+ :param model_name: a string to describe this model
88
+ :param temperature: the creativity setting
89
+ :return: a new instance of a subclass of LLM
90
+ """
91
+ subclass = cls.model_map().get(model_name)
92
+ if not subclass:
93
+ raise LLMException(f"Unrecognized LLM model name specified: {model_name}")
94
+ return subclass(model_name, temperature)
95
+
96
+
97
+ class Claude(LLM):
98
+ """
99
+ A class to act as an interface to the remote AI, in this case Claude
100
+ """
101
+
102
+ model_names = ["claude-3-5-sonnet-latest"]
103
+
104
+ def __init__(self, model_name: str, temperature: float):
105
+ """
106
+ Create a new instance of the Anthropic client
107
+ """
108
+ super().__init__(model_name, temperature)
109
+ self.client = Anthropic()
110
+
111
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
112
+ """
113
+ Send a message to Claude
114
+ :param system: the context in which this message is to be taken
115
+ :param user: the prompt
116
+ :param max_tokens: max number of tokens to generate
117
+ :return: the response from the AI
118
+ """
119
+ response = self.client.messages.create(
120
+ model=self.model_name,
121
+ max_tokens=max_tokens,
122
+ temperature=self.temperature,
123
+ system=system,
124
+ messages=[
125
+ {"role": "user", "content": user},
126
+ ],
127
+ )
128
+ return response.content[0].text
129
+
130
+
131
+ class GPT(LLM):
132
+ """
133
+ A class to act as an interface to the remote AI, in this case GPT
134
+ """
135
+
136
+ model_names = ["gpt-4o-mini", "gpt-4o"]
137
+
138
+ def __init__(self, model_name: str, temperature: float):
139
+ """
140
+ Create a new instance of the OpenAI client
141
+ """
142
+ super().__init__(model_name, temperature)
143
+ self.client = OpenAI()
144
+
145
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
146
+ """
147
+ Send a message to GPT
148
+ :param system: the context in which this message is to be taken
149
+ :param user: the prompt
150
+ :param max_tokens: max number of tokens to generate
151
+ :return: the response from the AI
152
+ """
153
+ response = self.client.chat.completions.create(
154
+ model=self.model_name,
155
+ messages=[
156
+ {"role": "system", "content": system},
157
+ {"role": "user", "content": user},
158
+ ],
159
+ response_format={"type": "json_object"},
160
+ )
161
+ return response.choices[0].message.content
162
+
163
+
164
+ class O1(LLM):
165
+ """
166
+ A class to act as an interface to the remote AI, in this case GPT
167
+ """
168
+
169
+ model_names = ["o1-mini"]
170
+
171
+ def __init__(self, model_name: str, temperature: float):
172
+ """
173
+ Create a new instance of the OpenAI client
174
+ """
175
+ super().__init__(model_name, temperature)
176
+ self.client = OpenAI()
177
+
178
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
179
+ """
180
+ Send a message to GPT
181
+ :param system: the context in which this message is to be taken
182
+ :param user: the prompt
183
+ :param max_tokens: max number of tokens to generate
184
+ :return: the response from the AI
185
+ """
186
+ message = system + "\n\n" + user
187
+ response = self.client.chat.completions.create(
188
+ model=self.model_name,
189
+ messages=[
190
+ {"role": "user", "content": message},
191
+ ],
192
+ )
193
+ return response.choices[0].message.content
194
+
195
+
196
+ class O3(LLM):
197
+ """
198
+ A class to act as an interface to the remote AI, in this case GPT
199
+ """
200
+
201
+ model_names = ["o3-mini"]
202
+
203
+ def __init__(self, model_name: str, temperature: float):
204
+ """
205
+ Create a new instance of the OpenAI client
206
+ """
207
+ super().__init__(model_name, temperature)
208
+ override = os.getenv("OPENAI_API_KEY_O3")
209
+ if override:
210
+ print("Using special key with o3 access")
211
+ self.client = OpenAI(api_key=override)
212
+ else:
213
+ self.client = OpenAI()
214
+
215
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
216
+ """
217
+ Send a message to GPT
218
+ :param system: the context in which this message is to be taken
219
+ :param user: the prompt
220
+ :param max_tokens: max number of tokens to generate
221
+ :return: the response from the AI
222
+ """
223
+ message = system + "\n\n" + user
224
+ response = self.client.chat.completions.create(
225
+ model=self.model_name,
226
+ messages=[
227
+ {"role": "user", "content": message},
228
+ ],
229
+ )
230
+ return response.choices[0].message.content
231
+
232
+
233
+ class Ollama(LLM):
234
+ """
235
+ A class to act as an interface to the remote AI, in this case Ollama via the OpenAI client
236
+ """
237
+
238
+ model_names = ["llama3.2 local", "gemma2 local", "qwen2.5 local", "phi4 local"]
239
+
240
+ def __init__(self, model_name: str, temperature: float):
241
+ """
242
+ Create a new instance of the OpenAI client
243
+ """
244
+ super().__init__(model_name.replace(" local", ""), temperature)
245
+ self.client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
246
+
247
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
248
+ """
249
+ Send a message to Ollama
250
+ :param system: the context in which this message is to be taken
251
+ :param user: the prompt
252
+ :param max_tokens: max number of tokens to generate
253
+ :return: the response from the AI
254
+ """
255
+
256
+ response = self.client.chat.completions.create(
257
+ model=self.model_name,
258
+ messages=[
259
+ {"role": "system", "content": system},
260
+ {"role": "user", "content": user},
261
+ ],
262
+ response_format={"type": "json_object"},
263
+ )
264
+ reply = response.choices[0].message.content
265
+ if "</think>" in reply:
266
+ print("Thoughts:\n" + reply.split("</think>")[0].replace("<think>", ""))
267
+ reply = reply.split("</think>")[1]
268
+ return reply
269
+
270
+
271
+ class DeepSeekAPI(LLM):
272
+ """
273
+ A class to act as an interface to the remote AI, in this case DeepSeek via the OpenAI client
274
+ """
275
+
276
+ model_names = ["deepseek-V3", "deepseek-r1"]
277
+
278
+ model_map = {"deepseek-V3": "deepseek-chat", "deepseek-r1": "deepseek-reasoner"}
279
+
280
+ def __init__(self, model_name: str, temperature: float):
281
+ """
282
+ Create a new instance of the OpenAI client
283
+ """
284
+ super().__init__(self.model_map[model_name], temperature)
285
+ deepseek_api_key = os.getenv("DEEPSEEK_API_KEY")
286
+ self.client = OpenAI(
287
+ api_key=deepseek_api_key, base_url="https://api.deepseek.com"
288
+ )
289
+
290
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
291
+ """
292
+ Send a message to DeepSeek
293
+ :param system: the context in which this message is to be taken
294
+ :param user: the prompt
295
+ :param max_tokens: max number of tokens to generate
296
+ :return: the response from the AI
297
+ """
298
+
299
+ response = self.client.chat.completions.create(
300
+ model=self.model_name,
301
+ messages=[
302
+ {"role": "system", "content": system},
303
+ {"role": "user", "content": user},
304
+ ],
305
+ # response_format={"type": "json_object"},
306
+ )
307
+ reply = response.choices[0].message.content
308
+ return reply
309
+
310
+
311
+ class DeepSeekLocal(LLM):
312
+ """
313
+ A class to act as an interface to the remote AI, in this case Ollama via the OpenAI client
314
+ """
315
+
316
+ model_names = ["deepseek-r1:14b local"]
317
+
318
+ def __init__(self, model_name: str, temperature: float):
319
+ """
320
+ Create a new instance of the OpenAI client
321
+ """
322
+ super().__init__(model_name.replace(" local", ""), temperature)
323
+ self.client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
324
+
325
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
326
+ """
327
+ Send a message to Ollama
328
+ :param system: the context in which this message is to be taken
329
+ :param user: the prompt
330
+ :param max_tokens: max number of tokens to generate
331
+ :return: the response from the AI
332
+ """
333
+ system += "\nImportant: avoid overthinking. Think briefly and decisively. The final response must follow the given json format or you forfeit the game. Do not overthink. Respond with json."
334
+ user += "\nImportant: avoid overthinking. Think briefly and decisively. The final response must follow the given json format or you forfeit the game. Do not overthink. Respond with json."
335
+ response = self.client.chat.completions.create(
336
+ model=self.model_name,
337
+ messages=[
338
+ {"role": "system", "content": system},
339
+ {"role": "user", "content": user},
340
+ ],
341
+ )
342
+ reply = response.choices[0].message.content
343
+ if "</think>" in reply:
344
+ print("Thoughts:\n" + reply.split("</think>")[0].replace("<think>", ""))
345
+ reply = reply.split("</think>")[1]
346
+ return reply
347
+
348
+
349
+ class GroqAPI(LLM):
350
+ """
351
+ A class to act as an interface to the remote AI, in this case Groq
352
+ """
353
+
354
+ model_names = [
355
+ "deepseek-r1-distill-llama-70b via Groq",
356
+ "llama-3.3-70b-versatile via Groq",
357
+ "mixtral-8x7b-32768 via Groq",
358
+ ]
359
+
360
+ def __init__(self, model_name: str, temperature: float):
361
+ """
362
+ Create a new instance of the OpenAI client
363
+ """
364
+ super().__init__(model_name[:-9], temperature)
365
+ self.client = Groq()
366
+
367
+ def _send(self, system: str, user: str, max_tokens: int = 3000) -> str:
368
+ """
369
+ Send a message to GPT
370
+ :param system: the context in which this message is to be taken
371
+ :param user: the prompt
372
+ :param max_tokens: max number of tokens to generate
373
+ :return: the response from the AI
374
+ """
375
+ response = self.client.chat.completions.create(
376
+ model=self.model_name,
377
+ messages=[
378
+ {"role": "system", "content": system},
379
+ {"role": "user", "content": user},
380
+ ],
381
+ response_format={"type": "json_object"},
382
+ )
383
+ return response.choices[0].message.content
arena/player.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from arena.llm import LLM
2
+ from arena.board import pieces, cols
3
+ import json
4
+ import random
5
+
6
+
7
+ class Player:
8
+
9
+ def __init__(self, model, color):
10
+ self.color = color
11
+ self.model = model
12
+ self.llm = LLM.create(self.model)
13
+ self.evaluation = ""
14
+ self.threats = ""
15
+ self.opportunities = ""
16
+ self.strategy = ""
17
+
18
+ def system(self, board, legal_moves, illegal_moves):
19
+ return f"""You are playing the board game Connect 4.
20
+ Players take turns to drop counters into one of 7 columns A, B, C, D, E, F, G.
21
+ The winner is the first player to get 4 counters in a row in any direction.
22
+ You are {pieces[self.color]} and your opponent is {pieces[self.color * -1]}.
23
+ You must pick a column for your move. You must pick one of the following legal moves: {legal_moves}.
24
+ You should respond in JSON according to this spec:
25
+
26
+ {{
27
+ "evaluation": "my assessment of the board",
28
+ "threats": "any threats from my opponent that I should block",
29
+ "opportunities": "my best chances to win",
30
+ "strategy": "my thought process",
31
+ "move_column": "one letter from this list of legal moves: {legal_moves}"
32
+ }}
33
+
34
+ You must pick one of these letters for your move_column: {legal_moves}{illegal_moves}"""
35
+
36
+ def user(self, board, legal_moves, illegal_moves):
37
+ return f"""It is your turn to make a move as {pieces[self.color]}.
38
+ Here is the current board, with row 1 at the bottom of the board:
39
+
40
+ {board.json()}
41
+
42
+ Here's another way of looking at the board visually, where R represents a red counter and Y for a yellow counter.
43
+
44
+ {board.alternative()}
45
+
46
+ Your final response should be only in JSON strictly according to this spec:
47
+
48
+ {{
49
+ "evaluation": "my assessment of the board",
50
+ "threats": "any threats from my opponent that I should block",
51
+ "opportunities": "my best chances to win",
52
+ "strategy": "my thought process",
53
+ "move_column": "one of {legal_moves} which are the legal moves"
54
+ }}
55
+
56
+ For example, the following could be a response:
57
+
58
+ {{
59
+ "evaluation": "the board is equally balanced but I have a slight advantage",
60
+ "threats": "my opponent has a threat but I can block it",
61
+ "opportunities": "I've developed several promising 3 in a row opportunities",
62
+ "strategy": "I must first block my opponent, then I can continue to develop",
63
+ "move_column": "{random.choice(board.legal_moves())}"
64
+ }}
65
+
66
+ And this is another example of a well formed response:
67
+
68
+ {{
69
+ "evaluation": "although my opponent has more threats, I can win immediately",
70
+ "threats": "my opponent has several threats",
71
+ "opportunities": "I can immediately win the game by making a diagonal 4",
72
+ "strategy": "I will take the winning move",
73
+ "move_column": "{random.choice(board.legal_moves())}"
74
+ }}
75
+
76
+
77
+ Now make your decision.
78
+ You must pick one of these letters for your move_column: {legal_moves}{illegal_moves}
79
+ """
80
+
81
+ def process_move(self, reply, board):
82
+ print(reply)
83
+ try:
84
+ if len(reply) == 3 and reply[0] == "{" and reply[2] == "}":
85
+ reply = f'{{"move_column": "{reply[1]}"}}'
86
+ result = json.loads(reply)
87
+ move = result.get("move_column") or "missing"
88
+ move = move.upper()
89
+ col = cols.find(move)
90
+ if not (0 <= col <= 6) or board.height(col) == 6:
91
+ raise ValueError("Illegal move")
92
+ board.move(col)
93
+ self.evaluation = result.get("evaluation") or ""
94
+ self.threats = result.get("threats") or ""
95
+ self.opportunities = result.get("opportunities") or ""
96
+ self.strategy = result.get("strategy") or ""
97
+ except Exception as e:
98
+ print(f"Exception {e}")
99
+ board.forfeit = True
100
+ board.winner = -1 * board.player
101
+
102
+ def move(self, board):
103
+ legal_moves = ", ".join(board.legal_moves())
104
+ if illegal := board.illegal_moves():
105
+ illegal_moves = (
106
+ "\nYou must NOT make any of these moves which are ILLEGAL: "
107
+ + ", ".join(illegal)
108
+ )
109
+ else:
110
+ illegal_moves = ""
111
+ system = self.system(board, legal_moves, illegal_moves)
112
+ user = self.user(board, legal_moves, illegal_moves)
113
+ reply = self.llm.send(system, user)
114
+ self.process_move(reply, board)
115
+
116
+ def thoughts(self):
117
+ result = '<div style="text-align: left;font-size:14px"><br/>'
118
+ result += f"<b>Evaluation:</b><br/>{self.evaluation}<br/><br/>"
119
+ result += f"<b>Threats:</b><br/>{self.threats}<br/><br/>"
120
+ result += f"<b>Opportunities:</b><br/>{self.opportunities}<br/><br/>"
121
+ result += f"<b>Strategy:</b><br/>{self.strategy}"
122
+ result += "</div>"
123
+ return result
124
+
125
+ def switch_model(self, new_model_name):
126
+ self.llm = LLM.create(new_model_name)
prototype.ipynb ADDED
The diff for this file is too large to render. See raw diff