Repackaged arena from original repo for Spaces deployment
Browse files- app.py +2 -6
- arena/__init__.py +0 -0
- arena/board.py +117 -0
- arena/board_view.py +132 -0
- arena/c4.py +198 -0
- arena/game.py +28 -0
- arena/llm.py +383 -0
- arena/player.py +126 -0
- prototype.ipynb +0 -0
app.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
|
| 4 |
-
|
| 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
|
|
|