Build and Push Docker Container / build-and-push (push) Successful in 10m12s
- Acknowledge start and end without waiting for persistence or dashboards. - Keep runtime creation and finished-game removal on the response path. - Serialize start, turn, and end bookkeeping per game in Quart tasks. - Capture turn thinking before scheduling deferred persistence. - Defer start metrics, logging, and stale-game pruning. - Run legacy history exports in a worker thread. - Record results and end metrics even when history export fails. - Drain pending bookkeeping before closing storage during shutdown. - Test sub-500 ms responses with slow backends and real SQLite. - Cover event ordering, export failures, and rejected user agents.
114 lines
4.2 KiB
Python
114 lines
4.2 KiB
Python
from typing import cast
|
|
import asyncio, json, time, os
|
|
|
|
from quart import Blueprint, request, jsonify
|
|
|
|
from quart_common.web.decorators import require_user_agent
|
|
from quart_common.web.logger import await_log
|
|
from snakes import DEFAULT_SNAKE_CONFIG
|
|
from server.GameBoard import GameBoard
|
|
from server.Files import read_file
|
|
|
|
lazy from server.Server import Server
|
|
|
|
def create_battlesnake_blueprint(server:'Server') -> Blueprint:
|
|
blueprint = Blueprint('battlesnake', __name__)
|
|
|
|
async def _override_snake_config_with_environment_variables(config:dict[str, str]) -> dict[str, str]:
|
|
print(config)
|
|
config['version'] = server.snake_version
|
|
|
|
for key in ('author', 'color', 'head', 'tail'):
|
|
value = os.environ.get(f'SNAKE_{key.upper()}')
|
|
if value is not None:
|
|
config[key] = value
|
|
|
|
version_override = os.environ.get('SNAKE_VERSION')
|
|
if version_override is not None:
|
|
config['version'] = version_override
|
|
|
|
return config
|
|
|
|
@blueprint.get('/')
|
|
async def on_info():
|
|
server.metrics_collector.record_http_request('info')
|
|
|
|
snake_config = cast(dict[str, str]|None, await read_file(server.config_file, json.load))
|
|
if not snake_config:
|
|
snake_json = await _override_snake_config_with_environment_variables(DEFAULT_SNAKE_CONFIG)
|
|
else:
|
|
snake_json = await _override_snake_config_with_environment_variables(snake_config)
|
|
|
|
await await_log(server.logger.info(f'INFO Snake: {snake_json}'))
|
|
return snake_json
|
|
|
|
@blueprint.post('/start')
|
|
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
|
async def on_start():
|
|
server.metrics_collector.record_http_request('start')
|
|
game_state = await request.get_json()
|
|
game_board = await server.game_runtime.create_game_board(game_state, publish_metrics=False)
|
|
active_count = len(server.game_runtime.running_games)
|
|
server.game_lifecycle.submit(
|
|
game_state['game']['id'],
|
|
lambda: server.game_lifecycle.started(game_state, game_board, active_count),
|
|
)
|
|
return 'ok'
|
|
|
|
@blueprint.post('/move')
|
|
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
|
async def on_move():
|
|
server.metrics_collector.record_http_request('move')
|
|
game_state = await request.get_json()
|
|
move_started = time.perf_counter()
|
|
|
|
game_id = game_state['game']['id']
|
|
timeout_ms = int(game_state.get('game', {}).get('timeout', 500))
|
|
budget_sec = max(0.05, (timeout_ms - 50) / 1000.0)
|
|
|
|
next_move = None
|
|
game_board = None
|
|
|
|
try:
|
|
async with asyncio.timeout(budget_sec):
|
|
game_board = cast(GameBoard, await server.game_runtime.get_game_board(game_state))
|
|
loop = asyncio.get_running_loop()
|
|
next_move = await loop.run_in_executor(None, game_board.snake_neat_make_a_move)
|
|
except TimeoutError:
|
|
await await_log(server.logger.warning(f'MOVE TIMEOUT: turn={game_state.get("turn")}, game={game_id}, returning fallback {next_move!r}'))
|
|
|
|
thinking = server.gameplay_tracking._extract_latest_snake_thinking(game_board)
|
|
server.game_lifecycle.submit(
|
|
game_id, lambda: server.game_lifecycle.turn(game_state, next_move, thinking),
|
|
)
|
|
elapsed_ms = (time.perf_counter() - move_started) * 1000.0
|
|
await server.metrics_collector.record_move(next_move, elapsed_ms)
|
|
|
|
if server.debug:
|
|
await await_log(server.logger.debug(f'TURN: {game_state['turn']:3}, MOVE: {next_move:5}'))
|
|
|
|
return {'move': next_move}
|
|
|
|
@blueprint.post('/end')
|
|
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
|
async def on_end():
|
|
server.metrics_collector.record_http_request('end')
|
|
game_state = await request.get_json()
|
|
game_id = game_state['game']['id']
|
|
game_board = server.game_runtime.running_games.get(game_id)
|
|
if game_board is not None:
|
|
game_board.read_game_data(game_state)
|
|
game_board.end_game(game_state)
|
|
await server.game_runtime.delete_game_board(game_state)
|
|
server.game_lifecycle.submit(
|
|
game_id, lambda: server.game_lifecycle.ended(game_state, game_board),
|
|
)
|
|
return 'ok'
|
|
|
|
# @blueprint.get('/cleanup')
|
|
# async def cleanup():
|
|
# results = server._cleanup_database()
|
|
# return jsonify(data=json.loads(results), status=200)
|
|
|
|
return blueprint
|