use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::{mpsc as std_mpsc, Arc, Once, OnceLock};
use manabrew_agent_interface::agent_impl::PromptAgent;
use manabrew_agent_interface::prompt::{AgentMessage, ClientToServerMessage};
use manabrew_protocol::deck_dto::Deck;
use crate::config::DeckSelection;
use forge_carddb::CardDatabase;
use manabot::BotResponder;
use manabrew_engine::agent::PlayerAgent;
use manabrew_engine::ids::PlayerId;
use manabrew_game_runtime::deck::prepare_players;
use manabrew_game_runtime::host_runtime::{
register_tokens_from_db, run_hosted_multiplayer_game, DEFAULT_MAX_TURNS,
};
use manabrew_game_runtime::mpsc_transport::MpscTransport as NodeTransport;
use memmap2::Mmap;
use rand::SeedableRng;
use tracing::{info, warn};
use super::HostedGameOver;
use crate::config::workspace_root;
pub fn run_hosted_engine_game(
game_id: String,
player_names: Vec<String>,
decks: Vec<Deck>,
commander_names: Vec<Option<String>>,
local_player_index: Option<usize>,
starting_life: i32,
remote_prompt_tx: std_mpsc::Sender<(usize, AgentMessage)>,
remote_response_rxs: Vec<(usize, std_mpsc::Receiver<ClientToServerMessage>)>,
game_over_tx: std_mpsc::Sender<HostedGameOver>,
) -> Result<(), String> {
let prepared_players = prepare_players(
&player_names,
&decks,
&commander_names,
get_card_db(),
starting_life,
);
let mut local_ai: Option<Box<dyn PlayerAgent>> = local_player_index.map(|player_index| {
Box::new(PromptAgent::new(
PlayerId(player_index as u32),
game_id.clone(),
BotResponder::default(),
)) as Box<dyn PlayerAgent>
});
let mut remote_rx_map: HashMap<usize, std_mpsc::Receiver<ClientToServerMessage>> =
remote_response_rxs.into_iter().collect();
let game_id_for_agents = game_id.clone();
let remote_prompt_tx_for_agents = remote_prompt_tx.clone();
let mut rng = rand::rngs::StdRng::from_entropy();
let abort_signal = Arc::new(AtomicBool::new(false));
run_hosted_multiplayer_game(
prepared_players,
abort_signal,
DEFAULT_MAX_TURNS,
&mut rng,
|game_loop| register_tokens_from_db(game_loop, get_token_db()),
|pid| {
if Some(pid.index()) == local_player_index {
local_ai
.take()
.expect("agent_factory called twice for the local seat")
} else {
let i = pid.index();
let response_rx = remote_rx_map
.remove(&i)
.expect("missing remote response receiver");
Box::new(PromptAgent::new(
pid,
game_id_for_agents.clone(),
NodeTransport::new_relay(i, remote_prompt_tx_for_agents.clone(), response_rx),
))
}
},
);
let _ = game_over_tx.send(HostedGameOver {
game_id,
messages: Vec::new(),
});
Ok(())
}
pub fn run_self_play(
seats: &[DeckSelection],
starting_life: i32,
seed: u64,
max_turns: u32,
) -> Result<(), String> {
let names: Vec<String> = (1..=seats.len())
.map(|i| format!("Self-Play {i}"))
.collect();
let decks: Vec<Deck> = seats.iter().map(|seat| seat.deck.clone()).collect();
let commander_names: Vec<Option<String>> = seats
.iter()
.map(|seat| seat.commander_name.clone())
.collect();
let prepared_players = prepare_players(
&names,
&decks,
&commander_names,
get_card_db(),
starting_life,
);
let game_id = "self-hosted-rust-self-play".to_string();
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let abort_signal = Arc::new(AtomicBool::new(false));
info!(
players = seats.len(),
starting_life, seed, max_turns, "rust self-play session started"
);
let outcome = run_hosted_multiplayer_game(
prepared_players,
abort_signal,
max_turns,
&mut rng,
|game_loop| register_tokens_from_db(game_loop, get_token_db()),
move |pid| {
Box::new(PromptAgent::new(
pid,
game_id.clone(),
BotResponder::default(),
)) as Box<dyn PlayerAgent>
},
);
if outcome.aborted {
return Err("rust self-play aborted".to_string());
}
match outcome.winner {
Some(winner) => {
info!(winner = winner.index(), "rust self-play reached game over");
Ok(())
}
None => Err(format!(
"rust self-play hit {max_turns} turns without a winner"
)),
}
}
static CARD_DB: OnceLock<CardDatabase> = OnceLock::new();
static TOKEN_DB: OnceLock<CardDatabase> = OnceLock::new();
static DB_INIT: Once = Once::new();
fn ensure_dbs_loaded() {
DB_INIT.call_once(|| {
let archive_path = cardset_archive_path();
info!(path = %archive_path.display(), "loading card + token databases from archive");
let file = std::fs::File::open(&archive_path).unwrap_or_else(|e| {
panic!(
"Cardset archive not found at {}: {e}. Run `cargo run -p forge-cardset-archive --features build --release --bin build-cardset-archive` to build it.",
archive_path.display()
)
});
let mmap = unsafe { Mmap::map(&file).expect("mmap cardset archive") };
let bundle =
CardDatabase::load_from_archive(&mmap).expect("load cardset archive");
info!(
cards_loaded = bundle.cards_result.loaded,
cards_failed = bundle.cards_result.failed,
tokens_loaded = bundle.tokens_result.loaded,
tokens_failed = bundle.tokens_result.failed,
"loaded archive"
);
for (file, error) in bundle.tokens_result.errors.iter().take(10) {
warn!(file, %error, "token parse error");
}
let _ = CARD_DB.set(bundle.cards);
let _ = TOKEN_DB.set(bundle.tokens);
});
}
fn get_card_db() -> &'static CardDatabase {
ensure_dbs_loaded();
CARD_DB.get().expect("card db must be initialized")
}
fn get_token_db() -> &'static CardDatabase {
ensure_dbs_loaded();
TOKEN_DB.get().expect("token db must be initialized")
}
fn cardset_archive_path() -> PathBuf {
env::var("CARDSET_ARCHIVE")
.map(PathBuf::from)
.unwrap_or_else(|_| workspace_root().join("src-tauri/resources/cardset.rkyv"))
}