use std::{
cell::RefCell,
env,
ffi::OsStr,
io::{BufReader, Read, Write},
path::{Path, PathBuf},
process::{Child, Command as ProcCommand, Stdio},
};
use super::{
error::AtariEnvError,
protocol::{self, Command, OBS_CHANNELS, OBS_HEIGHT, OBS_LEN, OBS_WIDTH, Response},
};
use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
pub const ENV_PYTHON: &str = "ATARI_PYTHON";
pub const ENV_WORKER_SCRIPT: &str = "ATARI_WORKER_SCRIPT";
pub const DEFAULT_WORKER_SCRIPT: &str = "envs/atari/ale_worker.py";
pub const PONG_ACTION_COUNT: usize = 6;
struct Transport {
reader: BufReader<Box<dyn Read + Send>>,
writer: Box<dyn Write + Send>,
child: Option<Child>,
}
pub struct AtariEnv {
io: RefCell<Transport>,
game_id: String,
seed: u64,
last_obs: Vec<f32>,
last_reward: f32,
last_terminated: bool,
last_truncated: bool,
last_lives: u32,
}
impl AtariEnv {
pub fn new(game_id: &str, seed: u64) -> Result<Self, AtariEnvError> {
let python = env::var(ENV_PYTHON).unwrap_or_else(|_| "python3".to_string());
let script = env::var(ENV_WORKER_SCRIPT)
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(DEFAULT_WORKER_SCRIPT));
Self::spawn_with(python, script, game_id, seed)
}
pub fn spawn_with(
python: impl AsRef<OsStr>,
script: impl AsRef<Path>,
game_id: &str,
seed: u64,
) -> Result<Self, AtariEnvError> {
let python_ref = python.as_ref();
let mut child = ProcCommand::new(python_ref)
.arg(script.as_ref())
.arg(game_id)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|source| AtariEnvError::MissingPython {
path: python_ref.to_string_lossy().into_owned(),
source,
})?;
let writer: Box<dyn Write + Send> =
Box::new(child.stdin.take().expect("child stdin was requested as piped"));
let reader: Box<dyn Read + Send> =
Box::new(child.stdout.take().expect("child stdout was requested as piped"));
Ok(Self::from_parts(reader, writer, Some(child), game_id, seed))
}
pub fn from_transport(
reader: Box<dyn Read + Send>,
writer: Box<dyn Write + Send>,
game_id: &str,
seed: u64,
) -> Self {
Self::from_parts(reader, writer, None, game_id, seed)
}
fn from_parts(
reader: Box<dyn Read + Send>,
writer: Box<dyn Write + Send>,
child: Option<Child>,
game_id: &str,
seed: u64,
) -> Self {
AtariEnv {
io: RefCell::new(Transport { reader: BufReader::new(reader), writer, child }),
game_id: game_id.to_string(),
seed,
last_obs: Vec::new(),
last_reward: 0.0,
last_terminated: false,
last_truncated: false,
last_lives: 0,
}
}
#[must_use]
pub fn game_id(&self) -> &str {
&self.game_id
}
#[must_use]
pub fn seed(&self) -> u64 {
self.seed
}
#[must_use]
pub fn lives(&self) -> u32 {
self.last_lives
}
fn exchange(&self, cmd: &Command) -> Result<Response, AtariEnvError> {
let mut io = self.io.borrow_mut();
protocol::write_command(&mut io.writer, cmd)?;
io.writer.flush()?;
Ok(protocol::read_response(&mut io.reader)?)
}
fn apply_obs(&mut self, resp: Response) -> Result<(), AtariEnvError> {
match resp {
Response::Obs { terminated, truncated, reward, lives, pixels } => {
if pixels.len() != OBS_LEN {
return Err(AtariEnvError::Protocol(format!(
"expected {OBS_LEN} observation elements ({OBS_HEIGHT}×{OBS_WIDTH}×{OBS_CHANNELS}), got {}",
pixels.len()
)));
}
self.last_obs = pixels;
self.last_reward = reward;
self.last_terminated = terminated;
self.last_truncated = truncated;
self.last_lives = lives;
Ok(())
}
Response::Error(msg) => Err(AtariEnvError::from_worker_message(msg)),
Response::State(_) => Err(AtariEnvError::Protocol(
"expected an OBS response but the worker sent STATE".to_string(),
)),
}
}
pub fn try_reset(&mut self) -> Result<(), AtariEnvError> {
let resp = self.exchange(&Command::Reset(self.seed))?;
self.apply_obs(resp)
}
pub fn try_step(&mut self, action: i64) -> Result<StepResult, AtariEnvError> {
let resp = self.exchange(&Command::Step(action as i32))?;
self.apply_obs(resp)?;
Ok(StepResult {
observation: self.last_obs.clone(),
reward: self.last_reward,
terminated: self.last_terminated,
truncated: self.last_truncated,
info: StepInfo::default(),
})
}
pub fn try_clone_state(&self) -> Result<Vec<u8>, AtariEnvError> {
match self.exchange(&Command::CloneState)? {
Response::State(bytes) => Ok(bytes),
Response::Error(msg) => Err(AtariEnvError::from_worker_message(msg)),
Response::Obs { .. } => Err(AtariEnvError::Protocol(
"expected a STATE response but the worker sent OBS".to_string(),
)),
}
}
pub fn try_restore_state(&mut self, state: &[u8]) -> Result<(), AtariEnvError> {
let resp = self.exchange(&Command::RestoreState(state.to_vec()))?;
self.apply_obs(resp)
}
pub fn try_close(&mut self) -> Result<(), AtariEnvError> {
{
let mut io = self.io.borrow_mut();
protocol::write_command(&mut io.writer, &Command::Close)?;
io.writer.flush()?;
if let Some(child) = io.child.as_mut() {
let _ = child.wait();
}
}
Ok(())
}
}
impl std::fmt::Debug for AtariEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let has_child = self.io.borrow().child.is_some();
f.debug_struct("AtariEnv")
.field("game_id", &self.game_id)
.field("seed", &self.seed)
.field("obs_len", &self.last_obs.len())
.field("owns_child", &has_child)
.finish()
}
}
impl Drop for AtariEnv {
fn drop(&mut self) {
let mut io = self.io.borrow_mut();
let _ = protocol::write_command(&mut io.writer, &Command::Close);
let _ = io.writer.flush();
if let Some(mut child) = io.child.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
impl Environment for AtariEnv {
type Action = i64;
type State = Vec<u8>;
fn reset(&mut self) {
self.try_reset().unwrap_or_else(|e| panic!("AtariEnv::reset failed: {e}"));
}
fn get_observation(&self) -> Vec<f32> {
self.last_obs.clone()
}
fn step(&mut self, action: i64) -> StepResult {
self.try_step(action).unwrap_or_else(|e| panic!("AtariEnv::step failed: {e}"))
}
fn observation_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![OBS_HEIGHT, OBS_WIDTH, OBS_CHANNELS], space_type: SpaceType::Box }
}
fn action_space(&self) -> SpaceInfo {
SpaceInfo {
shape: vec![PONG_ACTION_COUNT],
space_type: SpaceType::Discrete(PONG_ACTION_COUNT),
}
}
fn render(&self) -> Vec<u8> {
self.last_obs.iter().map(|&p| p.clamp(0.0, 255.0) as u8).collect()
}
fn close(&mut self) {
let _ = self.try_close();
}
fn clone_state(&self) -> Vec<u8> {
self.try_clone_state()
.unwrap_or_else(|e| panic!("AtariEnv::clone_state failed: {e}"))
}
fn restore_state(&mut self, state: &Vec<u8>) {
self.try_restore_state(state)
.unwrap_or_else(|e| panic!("AtariEnv::restore_state failed: {e}"));
}
}