feat: add Zenith strategy and move latency tracing
Build and Push Docker Container / build-and-push (push) Successful in 2m18s

- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
This commit is contained in:
2026-11-05 01:11:31 +01:00
parent 39a7a53873
commit a2414f0b3d
26 changed files with 758 additions and 45 deletions
+21
View File
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import test from 'node:test';
const source = readFileSync(new URL('../../templates/files/js/Snake.js', import.meta.url), 'utf8');
const SnakeUtils = vm.runInNewContext(`${source}\nSnakeUtils`);
test('empty or missing head/tail uses the default SVG', () => {
for (const kind of ['heads', 'tails']) {
for (const value of [undefined, null, '', ' ']) {
assert.equal(SnakeUtils.buildCustomizationIconUrl(kind, value), `/dashboard/customizations/${kind}/default.svg`);
}
}
});
test('explicit customizations and invalid-name validation are preserved', () => {
assert.equal(SnakeUtils.buildCustomizationIconUrl('tails', 'ghost'), '/dashboard/customizations/tails/ghost.svg');
assert.equal(SnakeUtils.buildCustomizationIconUrl('heads', 'crystal-power'), '/dashboard/customizations/heads/crystal-power.svg');
assert.equal(SnakeUtils.buildCustomizationIconUrl('tails', '../ghost'), null);
});
@@ -0,0 +1,84 @@
import unittest
from time import perf_counter
from snakes import SnakeBuilder, get_snake_version
from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol
from snakes.strategies.zenith import ZenithBattleSnake_ClaudeOpus5_5
LOSS = -100_000.0
WIN = 100_000.0
class TestZenithRegistration(unittest.TestCase):
def test_api_name_and_version_are_exposed(self):
snake = ZenithBattleSnake_ClaudeOpus5_5()
self.assertEqual(snake.name, "ZenithBattleSnake")
self.assertEqual(snake.version, "1.1.0")
self.assertEqual(get_snake_version("ZenithBattleSnake_ClaudeOpus5_5"), "1.1.0")
self.assertIsInstance(snake, PrismBattleSnake_GPT_5_6_Sol)
self.assertIsInstance(
SnakeBuilder.build("ZenithBattleSnake_ClaudeOpus5_5"),
ZenithBattleSnake_ClaudeOpus5_5,
)
class TestZenithVerdicts(unittest.TestCase):
def test_proven_loss_is_vetoed_for_best_surviving_heuristic(self):
scores = {"up": 300.0, "left": 120.0, "right": 90.0}
verdicts = {"up": (LOSS, 3), "left": (-40.0, 3), "right": (25.0, 3)}
self.assertEqual(ZenithBattleSnake_ClaudeOpus5_5._apply_verdicts("up", scores, verdicts), "left")
def test_proven_win_overrides_heuristic(self):
scores = {"up": 300.0, "left": 120.0}
verdicts = {"up": (15.0, 2), "left": (WIN, 2)}
self.assertEqual(ZenithBattleSnake_ClaudeOpus5_5._apply_verdicts("up", scores, verdicts), "left")
def test_keeps_choice_when_every_move_loses(self):
scores = {"up": 300.0, "left": 120.0}
verdicts = {"up": (LOSS, 2), "left": (LOSS - 1, 2)}
self.assertEqual(ZenithBattleSnake_ClaudeOpus5_5._apply_verdicts("up", scores, verdicts), "up")
def test_keeps_choice_without_verdict(self):
scores = {"up": 300.0, "left": 120.0}
self.assertEqual(ZenithBattleSnake_ClaudeOpus5_5._apply_verdicts("up", scores, {}), "up")
def test_verdict_search_resolves_every_candidate(self):
snake = ZenithBattleSnake_ClaudeOpus5_5()
# Longer enemy can also reach (2, 5): stepping there loses the head-to-head.
my_body = [{"x": 1, "y": 5}, {"x": 0, "y": 5}]
enemy = {"body": [{"x": 3, "y": 5}, {"x": 4, "y": 5}, {"x": 5, "y": 5}], "health": 100}
safe_moves = {"up": {"x": 1, "y": 6}, "right": {"x": 2, "y": 5}}
verdicts = snake._duel_verdicts(
safe_moves=safe_moves, my_body=my_body, enemy=enemy, my_health=100,
food_set=set(), hazard_set=set(), hazard_damage=15, hazard_count={},
previous_hazard_set=set(), width=11, height=11,
deadline=perf_counter() + 1.0,
)
self.assertEqual(set(verdicts), {"up", "right"})
self.assertLessEqual(verdicts["right"][0], LOSS / 2)
self.assertGreater(verdicts["up"][0], LOSS / 2)
self.assertEqual(
snake._apply_verdicts("right", {"right": 50.0, "up": 10.0}, verdicts), "up",
)
def test_verdict_search_stops_when_out_of_time(self):
snake = ZenithBattleSnake_ClaudeOpus5_5()
verdicts = snake._duel_verdicts(
safe_moves={"up": {"x": 0, "y": 2}}, my_body=[{"x": 0, "y": 1}],
enemy={"body": [{"x": 4, "y": 4}]}, my_health=100,
food_set=set(), hazard_set=set(), hazard_damage=15, hazard_count={},
previous_hazard_set=set(), width=5, height=5,
deadline=perf_counter() - 0.001,
)
self.assertEqual(verdicts, {})
if __name__ == "__main__":
unittest.main()
+44
View File
@@ -0,0 +1,44 @@
import unittest
from time import perf_counter
from snakes.engine.health_routes import health_route_cost
from snakes.engine.bitboard import BitBoard
from snakes.engine.survival_search import CompactSurvivalSearch
from server.services.move_safety import fallback_move, run_budgeted_move
from unittest.mock import Mock
class TestSurvivalCorrections(unittest.TestCase):
def test_food_resets_health_before_elimination(self):
self.assertEqual(health_route_cost((0, 0), {(1, 0)}, {(1, 0)}, {}, 14, set(), 2, 1), 0)
def test_food_does_not_make_body_passable(self):
self.assertIsNone(health_route_cost((0, 0), {(1, 0)}, set(), {}, 14, {(1, 0)}, 2, 1))
def test_hazard_escape_charges_every_step(self):
self.assertEqual(health_route_cost((0, 0), set(), {(0, 0), (1, 0)}, {}, 14, set(), 3, 1, refuge=True), 16)
def test_long_constrictor_can_survive_in_small_free_region(self):
bb = BitBoard(5, 2)
body = (0, 5, 6, 7, 8, 9)
search = CompactSurvivalSearch(bb, set(), True, None, 2)
self.assertGreater(search._evaluate(body, ()), search.DEATH)
standard = CompactSurvivalSearch(bb, set(), False, None, 2)
self.assertEqual(standard._evaluate(body, ()), standard.DEATH)
def test_expired_queued_job_never_starts_strategy(self):
board = Mock(timeout=500)
self.assertEqual(run_budgeted_move(board, perf_counter()-1, 'left'), 'left')
board.snake_neat_make_a_move.assert_not_called()
def test_worker_deducts_queue_time_and_restores_timeout(self):
board = Mock(timeout=500)
observed = []
board.snake_neat_make_a_move.side_effect = lambda: observed.append(board.timeout) or 'right'
self.assertEqual(run_budgeted_move(board, perf_counter()+0.2, 'left'), 'right')
self.assertLessEqual(observed[0], 200)
self.assertEqual(board.timeout, 500)
def test_fallback_avoids_body_and_wall(self):
mine = {'id':'me','head':{'x':0,'y':0},'body':[{'x':0,'y':0},{'x':1,'y':0}]}
state = {'you':mine,'board':{'width':3,'height':3,'snakes':[mine]}}
self.assertEqual(fallback_move(state), 'up')
+52
View File
@@ -135,6 +135,58 @@ class TestBattlesnakeLifecycle(unittest.IsolatedAsyncioTestCase):
self.assertNotEqual(replay['game']['status'], 'running')
print(f'Real SQLite HTTP timings: {timings}')
async def test_timed_out_move_returns_valid_fallback(self):
server = self.server
server.logger = Mock()
server.logger.warning.return_value = None
server.game_lifecycle.turn = AsyncMock()
server.metrics_collector.record_move = AsyncMock()
state = deepcopy(self.state)
state['game']['timeout'] = 100
async def stalled_board(*args):
await asyncio.sleep(0.2)
server.game_runtime.get_game_board = stalled_board
response = await server.app.test_client().post('/move', json=state, headers=self.headers)
self.assertEqual(response.status_code, 200)
self.assertIn((await response.get_json())['move'], ('up', 'down', 'left', 'right'))
await server.game_lifecycle.drain()
async def test_slow_metrics_publish_is_off_response_path_and_traced(self):
server = self.server
gate = asyncio.Event()
snapshots = []
async def slow_publish():
await gate.wait()
await asyncio.sleep(0.1)
async def capture(state, move, thinking):
snapshots.append(thinking)
server.metrics_collector.publish_move_metrics = slow_publish
server.gameplay_tracking.record_gameplay_turn_snapshot = capture
server.dashboard_query.push_dashboard_game_replay_update = AsyncMock()
client = server.app.test_client()
started = perf_counter()
response = await asyncio.wait_for(client.post('/move', json=self.state, headers=self.headers), 0.5)
elapsed = (perf_counter() - started) * 1000
self.assertEqual(response.status_code, 200)
self.assertLess(elapsed, 500)
trace_id = response.headers['X-Move-Trace-ID']
self.assertEqual(len(trace_id), 32)
self.assertEqual(snapshots, [])
gate.set()
await server.game_lifecycle.drain()
trace = snapshots[0]['move_trace']
self.assertEqual(trace['trace_id'], trace_id)
self.assertGreaterEqual(trace['metrics_publish_ms'], 90)
self.assertIn('executor_queue_ms', trace)
self.assertIn('strategy_ms', trace)
self.assertIn('response_encode_ms', trace)
self.assertFalse(trace['timed_out'])
async def test_invalid_agent_does_not_schedule_work(self):
response = await self.server.app.test_client().post('/start', json=self.state)
self.assertEqual(response.status_code, 404)
+2 -1
View File
@@ -42,7 +42,8 @@ class TestCodec(unittest.TestCase):
for move in ("up", "down", "left", "right", None):
self.assertEqual(decode_move(encode_move(move)), move)
self.assertEqual(decode_latency(encode_latency("77")), "77")
self.assertIsNone(encode_latency(""))
self.assertEqual(decode_latency(encode_latency("")), "")
self.assertIsNone(decode_latency(encode_latency(None)))
self.assertIsNone(encode_latency("timeout"))
def test_epoch_microseconds_roundtrip(self):
+16
View File
@@ -16,6 +16,22 @@ class TestDashboardStaticAssets(unittest.IsolatedAsyncioTestCase):
gameplay_db_enabled=False,
)
async def test_dashboard_uses_arena_game_url(self):
from unittest.mock import patch
with patch.dict(os.environ, {}, clear=True):
response = await self.server.app.test_client().get('/dashboard')
self.assertEqual(response.status_code, 200)
html = await response.get_data(as_text=True)
self.assertIn('https://arena.battlesnake.com/games', html)
self.assertNotIn('https://play.battlesnake.com/game', html)
def test_gameboard_uses_arena_game_url(self):
from types import SimpleNamespace
from server.GameBoard import GameBoard
game = SimpleNamespace(id='example-game')
self.assertEqual(GameBoard._get_game_url(game, False), 'https://arena.battlesnake.com/games/example-game')
self.assertIsNone(GameBoard._get_game_url(game, True))
def _static_url(self):
return self.server.app.jinja_env.globals['static_url']
+1 -1
View File
@@ -124,7 +124,7 @@ class TestLegacyMigration(unittest.TestCase):
self.assertEqual((me["snake_id"], me["snake_name"], me["is_you"]), ("me", "Me", True))
self.assertEqual((me["inferred_move"], me["latency"], me["health"], me["length"]), ("left", "12", 98, 3))
enemy = turns[2]["snakes"][1]
self.assertEqual((enemy["inferred_move"], enemy["latency"]), ("down", None))
self.assertEqual((enemy["inferred_move"], enemy["latency"]), ("down", ""))
with closing(sqlite3.connect(destination)) as connection:
self.assertEqual(connection.execute("SELECT turn_count FROM game_replays").fetchone()[0], 3)
+29
View File
@@ -0,0 +1,29 @@
import unittest
from unittest.mock import Mock, patch
from server.services.move_trace import MoveTrace
class TestMoveTrace(unittest.TestCase):
def test_snapshot_is_independent_and_stage_survives_exception(self):
trace = MoveTrace('test-game', 1)
with self.assertRaises(ValueError):
with trace.stage('test'):
raise ValueError()
snapshot = trace.snapshot()
snapshot['test_ms'] = -1
self.assertGreaterEqual(trace.snapshot()['test_ms'], 0)
def test_timeout_logged_even_when_sampling_disabled(self):
trace = MoveTrace('test-game', 2)
trace.set(timed_out=True)
logger = Mock()
with patch.dict('os.environ', {'MOVE_TRACE_SAMPLE_EVERY':'0'}):
trace.log(logger)
logger.info.assert_called_once()
self.assertIn(trace.id, logger.info.call_args.args[1])
def test_fast_requests_not_logged_when_disabled(self):
trace = MoveTrace('test-game', 2)
logger = Mock()
with patch.dict('os.environ', {'MOVE_TRACE_SAMPLE_EVERY':'0'}):
trace.log(logger)
logger.info.assert_not_called()