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
|
|