ai-code-battle/bots/assassin/src/main.rs
jedarden 6c1f031071 feat(config): add season_id + rules_version to Config per §4.2
- SeasonID and RulesVersion already present in engine/types.go Config struct
- Worker already populates from active season row via DB join
- Config embedded in VisibleState sent to bots each turn (including turn 0)
- All starter kits (go, python, rust, java, csharp) already expose and log fields
- Add season_id/rules_version logging to JavaScript starter on turn 0
- TypeScript Config interface already includes season_id and rules_version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 18:09:26 -04:00

152 lines
4.4 KiB
Rust

//! AssassinBot - Decapitation archetype. All units rush the enemy core.
//!
//! Ignores enemy units and economy; pushes straight for the enemy core.
//! No perimeter defense — commits fully.
mod game;
mod strategy;
use axum::{
extract::State,
http::{HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use game::{GameState, MoveResponse};
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use std::env;
use std::sync::Arc;
use strategy::AssassinStrategy;
use tokio::sync::Mutex;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
type HmacSha256 = Hmac<Sha256>;
struct BotState {
secret: String,
strategy: AssassinStrategy,
}
#[tokio::main]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::INFO)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
let port = env::var("BOT_PORT").unwrap_or_else(|_| "8082".to_string());
let secret = env::var("BOT_SECRET").expect("BOT_SECRET environment variable is required");
let state = Arc::new(Mutex::new(BotState {
secret,
strategy: AssassinStrategy::new(),
}));
let app = Router::new()
.route("/turn", post(handle_turn))
.route("/health", get(handle_health))
.with_state(state);
let addr = format!("0.0.0.0:{}", port);
info!("AssassinBot starting on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn handle_turn(
State(state): State<Arc<Mutex<BotState>>>,
headers: HeaderMap,
body: String,
) -> Result<impl IntoResponse, StatusCode> {
let match_id = headers
.get("X-ACB-Match-Id")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let turn_str = headers
.get("X-ACB-Turn")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let timestamp = headers
.get("X-ACB-Timestamp")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let signature = headers
.get("X-ACB-Signature")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let mut state = state.lock().await;
if !verify_signature(&state.secret, match_id, turn_str, timestamp, &body, signature) {
return Err(StatusCode::UNAUTHORIZED);
}
let game_state: GameState = serde_json::from_str(&body).map_err(|_| StatusCode::BAD_REQUEST)?;
let moves = state.strategy.compute_moves(&game_state);
let turn: u32 = turn_str.parse().unwrap_or(0);
info!("Turn {}: {} moves computed", turn, moves.len());
let response = MoveResponse { moves };
let response_body = serde_json::to_string(&response).unwrap();
let response_sig = sign_response(&state.secret, match_id, turn, &response_body);
let mut resp_headers = HeaderMap::new();
resp_headers.insert("X-ACB-Signature", HeaderValue::from_str(&response_sig).unwrap());
Ok((resp_headers, Json(response)))
}
async fn handle_health() -> &'static str {
"OK"
}
fn verify_signature(
secret: &str,
match_id: &str,
turn: &str,
timestamp: &str,
body: &str,
signature: &str,
) -> bool {
let body_hash = sha2::Sha256::digest(body.as_bytes());
let body_hash_hex = hex::encode(body_hash);
let signing_string = format!("{}.{}.{}.{}", match_id, turn, timestamp, body_hash_hex);
let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
Ok(m) => m,
Err(_) => return false,
};
mac.update(signing_string.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
hmac_equal(signature, &expected)
}
fn sign_response(secret: &str, match_id: &str, turn: u32, body: &str) -> String {
let body_hash = sha2::Sha256::digest(body.as_bytes());
let body_hash_hex = hex::encode(body_hash);
let signing_string = format!("{}.{}.{}", match_id, turn, body_hash_hex);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signing_string.as_bytes());
hex::encode(mac.finalize().into_bytes())
}
fn hmac_equal(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.as_bytes()
.iter()
.zip(b.as_bytes().iter())
.fold(0, |acc, (x, y)| acc | (x ^ y))
== 0
}