from typing import TYPE_CHECKING, 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 server.database import StorageLoader from snakes import DEFAULT_SNAKE_CONFIG from server.GameBoard import GameBoard from server.Files import read_file if TYPE_CHECKING: 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", abort_code=404) async def on_start(): server.metrics_collector.record_http_request('start') await server.game_runtime.prune_stale_games() game_state = await request.get_json() game_board = await server.game_runtime.create_game_board(game_state) await server.gameplay_tracking.record_gameplay_start(game_state, game_board) await await_log(server.logger.info(f'GAME START: {game_state['game']}')) return 'ok' @blueprint.post('/move') @require_user_agent("BattlesnakeEngine", 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}')) await server.gameplay_tracking.record_gameplay_turn(game_state, next_move, game_board) 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", abort_code=404) async def on_end(): server.metrics_collector.record_http_request('end') await server.game_runtime.prune_stale_games() game_state = await request.get_json() if server.store_game_state: game_board = cast(GameBoard, await server.game_runtime.get_game_board(game_state, end=True)) if server.check_tls_security: await game_board.save( StorageLoader.build(server.storage_type), file_path=os.path.join(server.data_path, 'data'), database=os.getenv('EDGEDB_DATABASE', None), tls_security=None, ) else: await game_board.save( StorageLoader.build(server.storage_type), file_path=os.path.join(server.data_path, 'data'), database=os.getenv('EDGEDB_DATABASE', None), ) await server.gameplay_tracking.record_gameplay_end(game_state) await server.dashboard_query.push_dashboard_games_update(game_state) await await_log(server.logger.info(f'GAME ENDED: Winner is {[x['name'] for x in game_state['board']['snakes']]}')) await server.game_runtime.delete_game_board(game_state) await server.metrics_collector.record_game_end(game_state) return 'ok' # @blueprint.get('/cleanup') # async def cleanup(): # results = server._cleanup_database() # return jsonify(data=json.loads(results), status=200) return blueprint