Mattimax commited on
Commit
5655dd0
·
1 Parent(s): b07ff6c

Add LLMArena Gradio app

Browse files
Files changed (2) hide show
  1. app.py +656 -0
  2. requirements.txt +9 -0
app.py ADDED
@@ -0,0 +1,656 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLMArena_no_outlines.py
2
+ import io
3
+ import os
4
+ import re
5
+ import time
6
+ import math
7
+ import random
8
+ from collections import defaultdict
9
+ from datetime import datetime
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ import requests
14
+ from tqdm.auto import tqdm
15
+ from PIL import Image, ImageDraw, ImageFont
16
+ import plotly.graph_objects as go
17
+ from plotly.subplots import make_subplots
18
+ import gradio as gr
19
+ import gistyc
20
+
21
+ # Try optional transformers
22
+ try:
23
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
24
+ TRANSFORMERS_AVAILABLE = True
25
+ except Exception:
26
+ TRANSFORMERS_AVAILABLE = False
27
+
28
+ # Constants
29
+ ARENA_SIZE = 10
30
+ ROBOT_SIZE = 1.5
31
+ MAX_HEALTH = 100
32
+ MAX_ENERGY = 50
33
+ MAX_ROUNDS = 20
34
+ TURN_TIME_LIMIT = 30
35
+
36
+ # Environment variables
37
+ GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
38
+ RESULTS_GIST_ID = os.environ.get("RESULTS_GIST_ID", "battle_results_gist")
39
+ LEADERBOARD_GIST_ID = os.environ.get("LEADERBOARD_GIST_ID", "battle_leaderboard_gist")
40
+
41
+ # --- Robot and Arena classes (identical logic, trimmed) ---
42
+ class Robot:
43
+ def __init__(self, name, model_id, gen_mode="heuristic"):
44
+ self.name = name
45
+ self.model_id = model_id
46
+ self.health = MAX_HEALTH
47
+ self.energy = MAX_ENERGY
48
+ self.position = [0, 0]
49
+ self.facing = 0 # degrees
50
+ self.actions = []
51
+ self.gen_mode = gen_mode
52
+ self._generator = None
53
+
54
+ def load_model(self):
55
+ # For transformer mode, create a generation pipeline cached on robot
56
+ if self.gen_mode == "transformers":
57
+ if not TRANSFORMERS_AVAILABLE:
58
+ raise RuntimeError("transformers not available in environment")
59
+ if self._generator is None:
60
+ # Attempt to build a text-generation pipeline from the model_id
61
+ try:
62
+ # Use model_id as HF model identifier
63
+ tok = AutoTokenizer.from_pretrained(self.model_id)
64
+ model = AutoModelForCausalLM.from_pretrained(self.model_id)
65
+ self._generator = pipeline("text-generation", model=model, tokenizer=tok, device=-1)
66
+ except Exception as e:
67
+ raise RuntimeError(f"Failed to load HF model {self.model_id}: {e}")
68
+ return self._generator
69
+
70
+ class BattleArena:
71
+ def __init__(self):
72
+ self.robot1 = None
73
+ self.robot2 = None
74
+ self.turn = 1
75
+ self.round = 1
76
+ self.game_over = False
77
+ self.winner = None
78
+ self.history = []
79
+
80
+ def initialize_battle(self, model_id1, model_id2, mode1="heuristic", mode2="heuristic"):
81
+ self.robot1 = Robot("Robot A", model_id1, gen_mode=mode1)
82
+ self.robot2 = Robot("Robot B", model_id2, gen_mode=mode2)
83
+ self.robot1.position = [-3, 0]
84
+ self.robot2.position = [3, 0]
85
+ self.robot1.facing = 0
86
+ self.robot2.facing = 180
87
+ self.turn = 1
88
+ self.round = 1
89
+ self.game_over = False
90
+ self.winner = None
91
+ self.history = []
92
+
93
+ def get_legal_actions(self, robot):
94
+ actions = [
95
+ "MOVE_FORWARD", "MOVE_BACKWARD", "TURN_LEFT", "TURN_RIGHT",
96
+ "PUNCH", "KICK", "ENERGY_BLAST", "BLOCK", "CHARGE"
97
+ ]
98
+ legal = []
99
+ for action in actions:
100
+ if action in ["PUNCH", "KICK"] and robot.energy < 10:
101
+ continue
102
+ if action == "ENERGY_BLAST" and robot.energy < 25:
103
+ continue
104
+ if action == "CHARGE" and robot.energy >= MAX_ENERGY:
105
+ continue
106
+ if "MOVE" in action:
107
+ new_pos = self.calculate_new_position(robot, action)
108
+ if not (-ARENA_SIZE/2 <= new_pos[0] <= ARENA_SIZE/2 and -ARENA_SIZE/2 <= new_pos[1] <= ARENA_SIZE/2):
109
+ continue
110
+ legal.append(action)
111
+ return legal
112
+
113
+ def calculate_new_position(self, robot, action):
114
+ x, y = robot.position
115
+ angle = math.radians(robot.facing)
116
+ if action == "MOVE_FORWARD":
117
+ x += math.cos(angle) * 1.5
118
+ y += math.sin(angle) * 1.5
119
+ elif action == "MOVE_BACKWARD":
120
+ x -= math.cos(angle) * 1.0
121
+ y -= math.sin(angle) * 1.0
122
+ return [x, y]
123
+
124
+ def calculate_distance(self):
125
+ x1, y1 = self.robot1.position
126
+ x2, y2 = self.robot2.position
127
+ return math.sqrt((x2-x1)**2 + (y2-y1)**2)
128
+
129
+ def is_facing_opponent(self, attacker, target):
130
+ dx = target.position[0] - attacker.position[0]
131
+ dy = target.position[1] - attacker.position[1]
132
+ target_angle = math.degrees(math.atan2(dy, dx)) % 360
133
+ angle_diff = abs((attacker.facing - target_angle + 180) % 360 - 180)
134
+ return angle_diff <= 45
135
+
136
+ def execute_action(self, attacker, target, action):
137
+ damage = 0
138
+ energy_cost = 0
139
+ message = ""
140
+ distance = self.calculate_distance()
141
+
142
+ if action == "MOVE_FORWARD":
143
+ attacker.position = self.calculate_new_position(attacker, action)
144
+ energy_cost = 5
145
+ message = f"{attacker.name} moves forward"
146
+ elif action == "MOVE_BACKWARD":
147
+ attacker.position = self.calculate_new_position(attacker, action)
148
+ energy_cost = 3
149
+ message = f"{attacker.name} moves backward"
150
+ elif action == "TURN_LEFT":
151
+ attacker.facing = (attacker.facing - 45) % 360
152
+ energy_cost = 2
153
+ message = f"{attacker.name} turns left"
154
+ elif action == "TURN_RIGHT":
155
+ attacker.facing = (attacker.facing + 45) % 360
156
+ energy_cost = 2
157
+ message = f"{attacker.name} turns right"
158
+ elif action == "PUNCH":
159
+ if distance <= 2.5 and self.is_facing_opponent(attacker, target):
160
+ damage = 15
161
+ message = f"{attacker.name} lands a solid punch on {target.name}!"
162
+ else:
163
+ message = f"{attacker.name} tries to punch but misses!"
164
+ energy_cost = 10
165
+ elif action == "KICK":
166
+ if distance <= 3.0 and self.is_facing_opponent(attacker, target):
167
+ damage = 25
168
+ message = f"{attacker.name} delivers a powerful kick to {target.name}!"
169
+ else:
170
+ message = f"{attacker.name}'s kick misses the target!"
171
+ energy_cost = 15
172
+ elif action == "ENERGY_BLAST":
173
+ if distance <= 6.0 and self.is_facing_opponent(attacker, target):
174
+ damage = 35
175
+ message = f"{attacker.name} hits {target.name} with an energy blast! 💥"
176
+ else:
177
+ message = f"{attacker.name}'s energy blast misses!"
178
+ energy_cost = 25
179
+ elif action == "BLOCK":
180
+ energy_cost = 5
181
+ message = f"{attacker.name} assumes defensive stance"
182
+ elif action == "CHARGE":
183
+ energy_gain = 15
184
+ attacker.energy = min(MAX_ENERGY, attacker.energy + energy_gain)
185
+ message = f"{attacker.name} charges up energy +{energy_gain}"
186
+
187
+ if damage > 0:
188
+ target.health = max(0, target.health - damage)
189
+ attacker.energy = max(0, attacker.energy - energy_cost)
190
+ attacker.actions.append(action)
191
+
192
+ return message, damage
193
+
194
+ def check_game_over(self):
195
+ if self.robot1.health <= 0:
196
+ self.game_over = True
197
+ self.winner = self.robot2
198
+ return True
199
+ elif self.robot2.health <= 0:
200
+ self.game_over = True
201
+ self.winner = self.robot1
202
+ return True
203
+ return False
204
+
205
+ # --- Prompt generator (for transformer mode) ---
206
+ def generate_action_prompt(arena, current_robot, opponent):
207
+ legal_actions = arena.get_legal_actions(current_robot)
208
+ actions_str = "|".join(legal_actions)
209
+ prompt = (
210
+ f"BATTLE ARENA - ROUND {arena.round}\n\n"
211
+ f"You are {current_robot.name} controlling a battle robot.\n"
212
+ f"Your opponent is {opponent.name}.\n\n"
213
+ "CURRENT STATUS:\n"
214
+ f"- Your Health: {current_robot.health}/{MAX_HEALTH}\n"
215
+ f"- Your Energy: {current_robot.energy}/{MAX_ENERGY}\n"
216
+ f"- Your Position: ({current_robot.position[0]:.1f}, {current_robot.position[1]:.1f})\n"
217
+ f"- Your Facing: {current_robot.facing}°\n"
218
+ f"- Opponent Health: {opponent.health}/{MAX_HEALTH}\n"
219
+ f"- Opponent Position: ({opponent.position[0]:.1f}, {opponent.position[1]:.1f})\n"
220
+ f"- Distance to opponent: {arena.calculate_distance():.1f}\n\n"
221
+ f"LEGAL ACTIONS: {actions_str}\n\n"
222
+ "Choose ONE action from the legal moves above. Respond with only the action name.\n\n"
223
+ "ACTION:"
224
+ )
225
+ return prompt, legal_actions
226
+
227
+ # --- Validation ---
228
+ def validate_and_sanitize_action(generated_text, legal_actions):
229
+ cleaned = (generated_text or "").strip().upper()
230
+ # exact match
231
+ if cleaned in legal_actions:
232
+ return cleaned
233
+ # partial match heuristics
234
+ for action in legal_actions:
235
+ if action in cleaned:
236
+ return action
237
+ # match keywords
238
+ keywords = {
239
+ "PUNCH": ["PUNCH", "HIT", "STRIKE"],
240
+ "KICK": ["KICK", "KICKED"],
241
+ "ENERGY_BLAST": ["ENERGY", "BLAST", "BEAM"],
242
+ "BLOCK": ["BLOCK", "DEFEND", "GUARD"],
243
+ "CHARGE": ["CHARGE", "RECHARGE", "ENERGIZE"],
244
+ "MOVE_FORWARD": ["FORWARD", "ADVANCE", "MOVE_FORWARD"],
245
+ "MOVE_BACKWARD": ["BACK", "RETREAT", "MOVE_BACKWARD"],
246
+ "TURN_LEFT": ["LEFT", "TURN_LEFT"],
247
+ "TURN_RIGHT": ["RIGHT", "TURN_RIGHT"]
248
+ }
249
+ for act, kws in keywords.items():
250
+ if act in legal_actions:
251
+ for kw in kws:
252
+ if kw in cleaned:
253
+ return act
254
+ # fallback: prefer CHARGE if available, else random legal
255
+ if "CHARGE" in legal_actions:
256
+ return "CHARGE"
257
+ if legal_actions:
258
+ return random.choice(legal_actions)
259
+ return "CHARGE"
260
+
261
+ # --- Heuristic policy (fallback) ---
262
+ def heuristic_policy(arena, robot, opponent):
263
+ legal = arena.get_legal_actions(robot)
264
+ dist = arena.calculate_distance()
265
+
266
+ # If low on energy, charge
267
+ if robot.energy <= 10 and "CHARGE" in legal:
268
+ return "CHARGE"
269
+ # If close and facing, prefer PUNCH or KICK
270
+ if dist <= 2.5:
271
+ if "PUNCH" in legal:
272
+ return "PUNCH"
273
+ if "KICK" in legal:
274
+ return "KICK"
275
+ # If mid-range and have energy, ENERGY_BLAST
276
+ if 2.5 < dist <= 6.0 and robot.energy >= 25 and "ENERGY_BLAST" in legal:
277
+ return "ENERGY_BLAST"
278
+ # If opponent behind, turn
279
+ if not arena.is_facing_opponent(robot, opponent):
280
+ # choose turn that minimizes angle difference
281
+ # simple random left/right
282
+ return random.choice([a for a in legal if a.startswith("TURN")] or legal)
283
+ # Otherwise move towards opponent if not too close
284
+ if dist > 3.0 and "MOVE_FORWARD" in legal:
285
+ return "MOVE_FORWARD"
286
+ # Default: random legal
287
+ return random.choice(legal) if legal else "CHARGE"
288
+
289
+ # --- Transformers policy ---
290
+ def transformers_policy(arena, robot, opponent, prompt, legal_actions):
291
+ """
292
+ Use HF pipeline to generate text. We then extract action name by regex/validation.
293
+ """
294
+ try:
295
+ gen = robot.load_model()
296
+ except Exception as e:
297
+ # If model fails to load, fallback to heuristic
298
+ print(f"Transformer load error: {e}")
299
+ return heuristic_policy(arena, robot, opponent)
300
+
301
+ # generate short completion
302
+ try:
303
+ # some models respond better with small max_new_tokens
304
+ out = gen(prompt, max_new_tokens=16, do_sample=True, temperature=0.7, top_k=50, num_return_sequences=1)
305
+ text = out[0]["generated_text"] if isinstance(out, list) else str(out)
306
+ # keep only the appended text after the prompt (some pipelines return full text)
307
+ if text.startswith(prompt):
308
+ text = text[len(prompt):]
309
+ # extract first token-like word
310
+ candidate = text.strip().splitlines()[0].strip()
311
+ # sanitize to action
312
+ return validate_and_sanitize_action(candidate, legal_actions)
313
+ except Exception as e:
314
+ print(f"Transformers generation failed: {e}")
315
+ return heuristic_policy(arena, robot, opponent)
316
+
317
+ # --- ELO and persistence (same logic) ---
318
+ def calculate_elo(rank1, rank2, result):
319
+ K = 32
320
+ expected_score1 = 1 / (1 + 10 ** ((rank2 - rank1) / 400))
321
+ new_rank1 = rank1 + K * (result - expected_score1)
322
+ return round(new_rank1)
323
+
324
+ def update_elo_ratings(battle_data):
325
+ elo_ratings = defaultdict(lambda: 1000)
326
+ for index, row in battle_data.iterrows():
327
+ if row["Result"] == "DRAW":
328
+ continue
329
+ model1 = row["Model1"]
330
+ model2 = row["Model2"]
331
+ result = row["Result"]
332
+ model1_elo = elo_ratings[model1]
333
+ model2_elo = elo_ratings[model2]
334
+ if result == "WIN1":
335
+ elo_ratings[model1] = calculate_elo(model1_elo, model2_elo, 1)
336
+ elo_ratings[model2] = calculate_elo(model2_elo, model1_elo, 0)
337
+ elif result == "WIN2":
338
+ elo_ratings[model1] = calculate_elo(model1_elo, model2_elo, 0)
339
+ elo_ratings[model2] = calculate_elo(model2_elo, model1_elo, 1)
340
+ return elo_ratings
341
+
342
+ def save_battle_result(model_id1, model_id2, winner, termination, rounds):
343
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
344
+ if winner == "Robot A":
345
+ result = "WIN1"
346
+ elif winner == "Robot B":
347
+ result = "WIN2"
348
+ else:
349
+ result = "DRAW"
350
+ data_str = f"{timestamp},{model_id1},{model_id2},{result},{termination},{rounds}\n"
351
+ with open("battle_results.csv", "a") as file:
352
+ file.write(data_str)
353
+ if GITHUB_TOKEN:
354
+ try:
355
+ gist_api = gistyc.GISTyc(auth_token=GITHUB_TOKEN)
356
+ response = gist_api.update_gist(file_name="battle_results.csv", gist_id=RESULTS_GIST_ID)
357
+ print("Results Gist updated")
358
+ except Exception as e:
359
+ print(f"Failed to update results Gist: {e}")
360
+
361
+ def update_leaderboard():
362
+ try:
363
+ battle_data = pd.read_csv("battle_results.csv")
364
+ model_stats = defaultdict(lambda: {"Wins": 0, "Losses": 0, "Draws": 0})
365
+ for _, row in battle_data.iterrows():
366
+ if row["Result"] == "WIN1":
367
+ model_stats[row["Model1"]]["Wins"] += 1
368
+ model_stats[row["Model2"]]["Losses"] += 1
369
+ elif row["Result"] == "WIN2":
370
+ model_stats[row["Model2"]]["Wins"] += 1
371
+ model_stats[row["Model1"]]["Losses"] += 1
372
+ else:
373
+ model_stats[row["Model1"]]["Draws"] += 1
374
+ model_stats[row["Model2"]]["Draws"] += 1
375
+ elo_ratings = update_elo_ratings(battle_data)
376
+ leaderboard_data = []
377
+ for model, stats in model_stats.items():
378
+ total_games = stats["Wins"] + stats["Losses"] + stats["Draws"]
379
+ win_rate = (stats["Wins"] / total_games * 100) if total_games > 0 else 0
380
+ leaderboard_data.append({
381
+ "Model": model,
382
+ "Wins": stats["Wins"],
383
+ "Losses": stats["Losses"],
384
+ "Draws": stats["Draws"],
385
+ "Win Rate": f"{win_rate:.1f}%",
386
+ "ELO Rating": elo_ratings[model]
387
+ })
388
+ leaderboard_df = pd.DataFrame(leaderboard_data)
389
+ leaderboard_df.sort_values("ELO Rating", ascending=False, inplace=True)
390
+ leaderboard_df.reset_index(drop=True, inplace=True)
391
+ leaderboard_df.to_csv("battle_leaderboard.csv", index=False)
392
+ if GITHUB_TOKEN:
393
+ gist_api = gistyc.GISTyc(auth_token=GITHUB_TOKEN)
394
+ response = gist_api.update_gist(file_name="battle_leaderboard.csv", gist_id=LEADERBOARD_GIST_ID)
395
+ print("Leaderboard Gist updated")
396
+ return leaderboard_df
397
+ except Exception as e:
398
+ print(f"Leaderboard update failed: {e}")
399
+ return get_leaderboard()
400
+
401
+ def get_leaderboard():
402
+ try:
403
+ return pd.read_csv("battle_leaderboard.csv")
404
+ except:
405
+ return pd.DataFrame({
406
+ "Model": ["Example Model 1", "Example Model 2"],
407
+ "Wins": [5, 3],
408
+ "Losses": [1, 2],
409
+ "Draws": [0, 1],
410
+ "Win Rate": ["83.3%", "50.0%"],
411
+ "ELO Rating": [1150, 1050]
412
+ })
413
+
414
+ # --- Rendering ---
415
+ def create_robot_mesh(x, y, color, name, facing):
416
+ angle = math.radians(facing)
417
+ base_vertices = [
418
+ [x-0.4, y-0.3, 0], [x+0.4, y-0.3, 0],
419
+ [x+0.4, y+0.3, 0], [x-0.4, y+0.3, 0],
420
+ [x-0.4, y-0.3, 1.2], [x+0.4, y-0.3, 1.2],
421
+ [x+0.4, y+0.3, 1.2], [x-0.4, y+0.3, 1.2],
422
+ ]
423
+ head_vertices = [
424
+ [x-0.2, y-0.2, 1.2], [x+0.2, y-0.2, 1.2],
425
+ [x+0.2, y+0.2, 1.2], [x-0.2, y+0.2, 1.2],
426
+ [x-0.2, y-0.2, 1.8], [x+0.2, y-0.2, 1.8],
427
+ [x+0.2, y+0.2, 1.8], [x-0.2, y+0.2, 1.8],
428
+ ]
429
+ vertices = base_vertices + head_vertices
430
+ faces = [
431
+ [0,1,2], [0,2,3], [4,5,6], [4,6,7],
432
+ [0,4,7], [0,7,3], [1,5,6], [1,6,2],
433
+ [8,9,10], [8,10,11], [12,13,14], [12,14,15]
434
+ ]
435
+ return go.Mesh3d(
436
+ x=[v[0] for v in vertices],
437
+ y=[v[1] for v in vertices],
438
+ z=[v[2] for v in vertices],
439
+ i=[f[0] for f in faces],
440
+ j=[f[1] for f in faces],
441
+ k=[f[2] for f in faces],
442
+ opacity=0.9,
443
+ name=name
444
+ )
445
+
446
+ def render_arena_3d(arena):
447
+ fig = make_subplots(rows=1, cols=1, specs=[[{'type':'scatter3d'}]])
448
+ x_floor, y_floor = np.meshgrid(np.linspace(-5,5,10), np.linspace(-5,5,10))
449
+ z_floor = np.zeros(x_floor.shape)
450
+ fig.add_trace(go.Surface(x=x_floor, y=y_floor, z=z_floor, colorscale='Greys', opacity=0.7, showscale=False, name='Arena'))
451
+ boundary_x = [-5,-5,5,5,-5]
452
+ boundary_y = [-5,5,5,-5,-5]
453
+ boundary_z = [0,0,0,0,0]
454
+ fig.add_trace(go.Scatter3d(x=boundary_x, y=boundary_y, z=boundary_z, mode='lines', line=dict(color='red', width=4), name='Boundary'))
455
+ fig.add_trace(create_robot_mesh(arena.robot1.position[0], arena.robot1.position[1], 'red', arena.robot1.name, arena.robot1.facing))
456
+ fig.add_trace(create_robot_mesh(arena.robot2.position[0], arena.robot2.position[1], 'blue', arena.robot2.name, arena.robot2.facing))
457
+ r1_angle = math.radians(arena.robot1.facing)
458
+ r2_angle = math.radians(arena.robot2.facing)
459
+ fig.add_trace(go.Scatter3d(x=[arena.robot1.position[0], arena.robot1.position[0] + math.cos(r1_angle)],
460
+ y=[arena.robot1.position[1], arena.robot1.position[1] + math.sin(r1_angle)],
461
+ z=[1.0,1.0], mode='lines', line=dict(color='darkred', width=6), showlegend=False))
462
+ fig.add_trace(go.Scatter3d(x=[arena.robot2.position[0], arena.robot2.position[0] + math.cos(r2_angle)],
463
+ y=[arena.robot2.position[1], arena.robot2.position[1] + math.sin(r2_angle)],
464
+ z=[1.0,1.0], mode='lines', line=dict(color='darkblue', width=6), showlegend=False))
465
+ fig.update_layout(scene=dict(xaxis=dict(range=[-6,6], showbackground=False, showticklabels=False, title=''),
466
+ yaxis=dict(range=[-6,6], showbackground=False, showticklabels=False, title=''),
467
+ zaxis=dict(range=[0,6], showbackground=False, showticklabels=False, title=''),
468
+ aspectmode='cube', camera=dict(eye=dict(x=1.5, y=1.5, z=1.2))),
469
+ margin=dict(l=0,r=0,t=0,b=0), height=500, showlegend=True)
470
+ return fig
471
+
472
+ def create_status_image(arena):
473
+ img = Image.new('RGB', (800, 300), color='black')
474
+ draw = ImageDraw.Draw(img)
475
+ try:
476
+ font_large = ImageFont.truetype("arial.ttf", 20)
477
+ font_medium = ImageFont.truetype("arial.ttf", 16)
478
+ font_small = ImageFont.truetype("arial.ttf", 14)
479
+ except:
480
+ font_large = font_medium = font_small = ImageFont.load_default()
481
+ draw.rectangle([5,5,395,145], outline='red', width=2)
482
+ draw.text((15,15), f"🤖 {arena.robot1.name}", fill='red', font=font_large)
483
+ hp1 = arena.robot1.health / MAX_HEALTH
484
+ bw1 = int(350 * hp1)
485
+ draw.rectangle([15,45,15 + bw1,65], fill='red')
486
+ draw.rectangle([15,45,365,65], outline='white', width=1)
487
+ draw.text((15,70), f"Health: {arena.robot1.health}/{MAX_HEALTH}", fill='white', font=font_medium)
488
+ e1 = arena.robot1.energy / MAX_ENERGY
489
+ be1 = int(350 * e1)
490
+ draw.rectangle([15,85,15 + be1,105], fill='yellow')
491
+ draw.rectangle([15,85,365,105], outline='white', width=1)
492
+ draw.text((15,110), f"Energy: {arena.robot1.energy}/{MAX_ENERGY}", fill='white', font=font_medium)
493
+ draw.text((15,130), f"Position: ({arena.robot1.position[0]:.1f}, {arena.robot1.position[1]:.1f})", fill='white', font=font_small)
494
+ draw.text((200,130), f"Facing: {arena.robot1.facing}°", fill='white', font=font_small)
495
+ draw.rectangle([405,5,795,145], outline='blue', width=2)
496
+ draw.text((415,15), f"🤖 {arena.robot2.name}", fill='blue', font=font_large)
497
+ hp2 = arena.robot2.health / MAX_HEALTH
498
+ bw2 = int(350 * hp2)
499
+ draw.rectangle([415,45,415 + bw2,65], fill='blue')
500
+ draw.rectangle([415,45,765,65], outline='white', width=1)
501
+ draw.text((415,70), f"Health: {arena.robot2.health}/{MAX_HEALTH}", fill='white', font=font_medium)
502
+ e2 = arena.robot2.energy / MAX_ENERGY
503
+ be2 = int(350 * e2)
504
+ draw.rectangle([415,85,415 + be2,105], fill='yellow')
505
+ draw.rectangle([415,85,765,105], outline='white', width=1)
506
+ draw.text((415,110), f"Energy: {arena.robot2.energy}/{MAX_ENERGY}", fill='white', font=font_medium)
507
+ draw.text((415,130), f"Position: ({arena.robot2.position[0]:.1f}, {arena.robot2.position[1]:.1f})", fill='white', font=font_small)
508
+ draw.text((600,130), f"Facing: {arena.robot2.facing}°", fill='white', font=font_small)
509
+ draw.rectangle([5,155,795,295], outline='green', width=2)
510
+ draw.text((15,165), f"⚔️ BATTLE ARENA - ROUND {arena.round}", fill='yellow', font=font_large)
511
+ draw.text((15,195), f"Distance between robots: {arena.calculate_distance():.1f}", fill='white', font=font_medium)
512
+ draw.text((15,220), f"Current turn: {arena.robot1.name if arena.turn == 1 else arena.robot2.name}", fill='white', font=font_medium)
513
+ if arena.game_over and arena.winner:
514
+ draw.text((15,245), f"🏆 WINNER: {arena.winner.name}! 🏆", fill='green', font=font_large)
515
+ elif arena.game_over:
516
+ draw.text((15,245), "DRAW - Maximum rounds reached", fill='yellow', font=font_large)
517
+ return img
518
+
519
+ # --- Battle sequence generator (used by Gradio) ---
520
+ def battle_sequence(model_id1, model_id2, mode_choice="auto"):
521
+ """
522
+ mode_choice: "auto", "heuristic", "transformers"
523
+ - "auto": if model_id looks like HF then try transformers; else heuristic
524
+ """
525
+ # decide modes
526
+ def decide_mode(mid):
527
+ if mode_choice == "heuristic":
528
+ return "heuristic"
529
+ if mode_choice == "transformers":
530
+ return "transformers"
531
+ # auto
532
+ if TRANSFORMERS_AVAILABLE and "/" in mid: # naive HF id detection
533
+ return "transformers"
534
+ return "heuristic"
535
+
536
+ m1_mode = decide_mode(model_id1)
537
+ m2_mode = decide_mode(model_id2)
538
+
539
+ arena = BattleArena()
540
+ arena.initialize_battle(model_id1, model_id2, mode1=m1_mode, mode2=m2_mode)
541
+
542
+ battle_log = []
543
+ fig = render_arena_3d(arena)
544
+ status_img = create_status_image(arena)
545
+ yield fig, status_img, "🤖 Battle starting! Initializing..."
546
+
547
+ # Load transformer models if needed
548
+ try:
549
+ if arena.robot1.gen_mode == "transformers":
550
+ arena.robot1.load_model()
551
+ if arena.robot2.gen_mode == "transformers":
552
+ arena.robot2.load_model()
553
+ battle_log.append("✅ Models initialized (or heuristics ready).")
554
+ except Exception as e:
555
+ yield fig, status_img, f"❌ Model init error: {e}"
556
+ return
557
+
558
+ battle_log.append("⚔️ Let the battle begin!")
559
+
560
+ while not arena.game_over and arena.round <= MAX_ROUNDS:
561
+ current_robot = arena.robot1 if arena.turn == 1 else arena.robot2
562
+ opponent = arena.robot2 if arena.turn == 1 else arena.robot1
563
+ prompt, legal_actions = generate_action_prompt(arena, current_robot, opponent)
564
+ # legal actions as list
565
+ legal_list = legal_actions
566
+
567
+ # choose action based on mode
568
+ if current_robot.gen_mode == "transformers":
569
+ action = transformers_policy(arena, current_robot, opponent, prompt, legal_list)
570
+ else:
571
+ action = heuristic_policy(arena, current_robot, opponent)
572
+
573
+ # final sanity
574
+ action = validate_and_sanitize_action(action, legal_list)
575
+
576
+ message, damage = arena.execute_action(current_robot, opponent, action)
577
+ log_entry = f"Round {arena.round} - {current_robot.name}: {action}"
578
+ if message:
579
+ log_entry += f" - {message}"
580
+ if damage > 0:
581
+ log_entry += f" 💥(-{damage} HP)"
582
+ battle_log.append(log_entry)
583
+
584
+ if arena.check_game_over():
585
+ break
586
+
587
+ if arena.turn == 2:
588
+ arena.round += 1
589
+ arena.turn = 3 - arena.turn
590
+
591
+ fig = render_arena_3d(arena)
592
+ status_img = create_status_image(arena)
593
+ # yield last part of log
594
+ yield fig, status_img, "\n".join(battle_log[-8:])
595
+ time.sleep(0.6)
596
+
597
+ if arena.game_over:
598
+ conclusion = f"🏆 BATTLE OVER! {arena.winner.name} WINS! 🏆"
599
+ battle_log.append(conclusion)
600
+ save_battle_result(model_id1, model_id2, arena.winner.name, "KO", arena.round)
601
+ update_leaderboard()
602
+ else:
603
+ conclusion = "🤝 Battle ended in draw - maximum rounds reached"
604
+ battle_log.append(conclusion)
605
+ save_battle_result(model_id1, model_id2, "DRAW", "Max Rounds", arena.round)
606
+ update_leaderboard()
607
+
608
+ fig = render_arena_3d(arena)
609
+ status_img = create_status_image(arena)
610
+ yield fig, status_img, "\n".join(battle_log[-12:])
611
+
612
+ # --- File initialization ---
613
+ def initialize_files():
614
+ if not os.path.exists("battle_results.csv"):
615
+ with open("battle_results.csv", "w") as f:
616
+ f.write("Timestamp,Model1,Model2,Result,Termination,Rounds\n")
617
+ if not os.path.exists("battle_leaderboard.csv"):
618
+ df = get_leaderboard()
619
+ df.to_csv("battle_leaderboard.csv", index=False)
620
+
621
+ initialize_files()
622
+
623
+ # --- Gradio UI ---
624
+ title = """
625
+ <div align="center">
626
+ <p style="font-size: 32px;">🤖 LLM Battle Arena — No outlines</p>
627
+ <p style="font-size: 16px;">Scegli due "modelli" o usa la modalità heuristica. Se vuoi usare un modello HuggingFace, inserisci l'ID (es. 'gpt2' o 'facebook/opt-125m') e assicurati di avere 'transformers' installato.</p>
628
+ </div>
629
+ """
630
+
631
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
632
+ gr.Markdown(title)
633
+ with gr.Row():
634
+ with gr.Column():
635
+ model_id1 = gr.Textbox(label="🤖 Robot A Model ID", value="heuristic", placeholder="Inserisci HF model id o 'heuristic'")
636
+ model_id2 = gr.Textbox(label="🤖 Robot B Model ID", value="heuristic", placeholder="Inserisci HF model id o 'heuristic'")
637
+ mode_choice = gr.Radio(choices=["auto", "heuristic", "transformers"], value="auto", label="Mode (auto sceglie in base all'ID e disponibilità transformers)")
638
+ battle_btn = gr.Button("🎯 Start Battle!", variant="primary", size="lg")
639
+ with gr.Column():
640
+ arena_plot = gr.Plot(label="🎪 3D Battle Arena")
641
+ status_display = gr.Image(label="📊 Battle Status", height=300)
642
+ battle_log = gr.Textbox(label="📝 Battle Log", lines=6, max_lines=10)
643
+ with gr.Row():
644
+ gr.Markdown("### 🏆 Leaderboard")
645
+ leaderboard = gr.Dataframe(value=get_leaderboard, every=60, label="Model Rankings")
646
+ footer = """
647
+ <div align="center">
648
+ <p><em>Azioni: MOVE_FORWARD, MOVE_BACKWARD, TURN_LEFT, TURN_RIGHT, PUNCH, KICK, ENERGY_BLAST, BLOCK, CHARGE</em></p>
649
+ </div>
650
+ """
651
+ gr.Markdown(footer)
652
+
653
+ battle_btn.click(fn=battle_sequence, inputs=[model_id1, model_id2, mode_choice], outputs=[arena_plot, status_display, battle_log])
654
+
655
+ if __name__ == "__main__":
656
+ demo.launch(share=True)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ numpy
2
+ pandas
3
+ requests
4
+ tqdm
5
+ Pillow
6
+ plotly
7
+ gradio
8
+ gistyc
9
+ transformers