use std::io::{self, Read, Write};
pub const TAG_RESET: u8 = 0x01;
pub const TAG_STEP: u8 = 0x02;
pub const TAG_CLONE_STATE: u8 = 0x03;
pub const TAG_RESTORE_STATE: u8 = 0x04;
pub const TAG_CLOSE: u8 = 0x05;
pub const TAG_OBS: u8 = 0x81;
pub const TAG_STATE: u8 = 0x82;
pub const TAG_ERROR: u8 = 0x83;
pub const OBS_HEIGHT: usize = 210;
pub const OBS_WIDTH: usize = 160;
pub const OBS_CHANNELS: usize = 3;
pub const OBS_LEN: usize = OBS_HEIGHT * OBS_WIDTH * OBS_CHANNELS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Reset(u64),
Step(i32),
CloneState,
RestoreState(Vec<u8>),
Close,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Response {
Obs {
terminated: bool,
truncated: bool,
reward: f32,
lives: u32,
pixels: Vec<f32>,
},
State(Vec<u8>),
Error(String),
}
fn invalid_data(msg: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.into())
}
fn frame(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + payload.len());
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
out
}
#[must_use]
pub fn encode_command(cmd: &Command) -> Vec<u8> {
frame(&command_payload(cmd))
}
fn command_payload(cmd: &Command) -> Vec<u8> {
match cmd {
Command::Reset(seed) => {
let mut p = Vec::with_capacity(9);
p.push(TAG_RESET);
p.extend_from_slice(&seed.to_le_bytes());
p
}
Command::Step(action) => {
let mut p = Vec::with_capacity(5);
p.push(TAG_STEP);
p.extend_from_slice(&action.to_le_bytes());
p
}
Command::CloneState => vec![TAG_CLONE_STATE],
Command::RestoreState(bytes) => {
let mut p = Vec::with_capacity(1 + bytes.len());
p.push(TAG_RESTORE_STATE);
p.extend_from_slice(bytes);
p
}
Command::Close => vec![TAG_CLOSE],
}
}
pub fn decode_command(payload: &[u8]) -> io::Result<Command> {
let (&tag, rest) =
payload.split_first().ok_or_else(|| invalid_data("empty command payload"))?;
match tag {
TAG_RESET => {
let bytes: [u8; 8] =
rest.try_into().map_err(|_| invalid_data("RESET payload must be 8 bytes"))?;
Ok(Command::Reset(u64::from_le_bytes(bytes)))
}
TAG_STEP => {
let bytes: [u8; 4] =
rest.try_into().map_err(|_| invalid_data("STEP payload must be 4 bytes"))?;
Ok(Command::Step(i32::from_le_bytes(bytes)))
}
TAG_CLONE_STATE => Ok(Command::CloneState),
TAG_RESTORE_STATE => Ok(Command::RestoreState(rest.to_vec())),
TAG_CLOSE => Ok(Command::Close),
other => Err(invalid_data(format!("unknown command tag {other:#04x}"))),
}
}
#[must_use]
pub fn encode_response(resp: &Response) -> Vec<u8> {
frame(&response_payload(resp))
}
fn response_payload(resp: &Response) -> Vec<u8> {
match resp {
Response::Obs { terminated, truncated, reward, lives, pixels } => {
let mut p = Vec::with_capacity(11 + pixels.len() * 4);
p.push(TAG_OBS);
p.push(u8::from(*terminated));
p.push(u8::from(*truncated));
p.extend_from_slice(&reward.to_le_bytes());
p.extend_from_slice(&lives.to_le_bytes());
for px in pixels {
p.extend_from_slice(&px.to_le_bytes());
}
p
}
Response::State(bytes) => {
let mut p = Vec::with_capacity(1 + bytes.len());
p.push(TAG_STATE);
p.extend_from_slice(bytes);
p
}
Response::Error(msg) => {
let mut p = Vec::with_capacity(1 + msg.len());
p.push(TAG_ERROR);
p.extend_from_slice(msg.as_bytes());
p
}
}
}
pub fn decode_response(payload: &[u8]) -> io::Result<Response> {
let (&tag, rest) =
payload.split_first().ok_or_else(|| invalid_data("empty response payload"))?;
match tag {
TAG_OBS => {
if rest.len() < 10 {
return Err(invalid_data("OBS payload shorter than its 10-byte header"));
}
let terminated = rest[0] != 0;
let truncated = rest[1] != 0;
let reward = f32::from_le_bytes(rest[2..6].try_into().unwrap());
let lives = u32::from_le_bytes(rest[6..10].try_into().unwrap());
let pixel_bytes = &rest[10..];
if !pixel_bytes.len().is_multiple_of(4) {
return Err(invalid_data("OBS pixel byte count is not a multiple of 4"));
}
let (chunks, _rest) = pixel_bytes.as_chunks::<4>();
let pixels = chunks.iter().map(|c| f32::from_le_bytes(*c)).collect();
Ok(Response::Obs { terminated, truncated, reward, lives, pixels })
}
TAG_STATE => Ok(Response::State(rest.to_vec())),
TAG_ERROR => Ok(Response::Error(String::from_utf8_lossy(rest).into_owned())),
other => Err(invalid_data(format!("unknown response tag {other:#04x}"))),
}
}
fn read_frame<R: Read>(reader: &mut R) -> io::Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
reader.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
let mut payload = vec![0u8; len];
reader.read_exact(&mut payload)?;
Ok(payload)
}
pub fn write_command<W: Write>(writer: &mut W, cmd: &Command) -> io::Result<()> {
writer.write_all(&encode_command(cmd))
}
pub fn read_command<R: Read>(reader: &mut R) -> io::Result<Command> {
let payload = read_frame(reader)?;
decode_command(&payload)
}
pub fn write_response<W: Write>(writer: &mut W, resp: &Response) -> io::Result<()> {
writer.write_all(&encode_response(resp))
}
pub fn read_response<R: Read>(reader: &mut R) -> io::Result<Response> {
let payload = read_frame(reader)?;
decode_response(&payload)
}
#[cfg(test)]
mod tests {
use super::*;
fn payload_of(frame: &[u8]) -> &[u8] {
let len = u32::from_le_bytes(frame[0..4].try_into().unwrap()) as usize;
assert_eq!(len, frame.len() - 4, "length prefix must match payload length");
&frame[4..]
}
#[test]
fn codec_reset_round_trip() {
let frame = encode_command(&Command::Reset(42));
let decoded = decode_command(payload_of(&frame)).unwrap();
assert_eq!(decoded, Command::Reset(42));
}
#[test]
fn codec_step_round_trip() {
let frame = encode_command(&Command::Step(3));
let decoded = decode_command(payload_of(&frame)).unwrap();
assert_eq!(decoded, Command::Step(3));
let frame = encode_command(&Command::Step(-7));
assert_eq!(decode_command(payload_of(&frame)).unwrap(), Command::Step(-7));
}
#[test]
fn codec_obs_response_round_trip() {
let pixels: Vec<f32> = (0..OBS_LEN).map(|i| (i % 256) as f32).collect();
let resp =
Response::Obs { terminated: false, truncated: true, reward: 1.0, lives: 3, pixels };
let frame = encode_response(&resp);
let decoded = decode_response(payload_of(&frame)).unwrap();
match decoded {
Response::Obs { terminated, truncated, reward, lives, pixels } => {
assert!(!terminated);
assert!(truncated);
assert_eq!(reward, 1.0);
assert_eq!(lives, 3);
assert_eq!(pixels.len(), OBS_LEN);
assert_eq!(pixels[1], 1.0);
assert_eq!(pixels[255], 255.0);
}
other => panic!("expected Obs, got {other:?}"),
}
}
#[test]
fn codec_state_round_trip() {
let blob = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x7F];
let frame = encode_response(&Response::State(blob.clone()));
let decoded = decode_response(payload_of(&frame)).unwrap();
assert_eq!(decoded, Response::State(blob.clone()));
let frame = encode_command(&Command::RestoreState(blob.clone()));
assert_eq!(decode_command(payload_of(&frame)).unwrap(), Command::RestoreState(blob));
}
#[test]
fn codec_error_response() {
let resp = Response::Error("missing ROM".to_string());
let frame = encode_response(&resp);
let decoded = decode_response(payload_of(&frame)).unwrap();
assert_eq!(decoded, Response::Error("missing ROM".to_string()));
}
#[test]
fn read_write_stream_round_trip() {
let mut buf: Vec<u8> = Vec::new();
write_command(&mut buf, &Command::Reset(7)).unwrap();
write_command(&mut buf, &Command::CloneState).unwrap();
let mut cursor = std::io::Cursor::new(buf);
assert_eq!(read_command(&mut cursor).unwrap(), Command::Reset(7));
assert_eq!(read_command(&mut cursor).unwrap(), Command::CloneState);
}
#[test]
fn decode_rejects_unknown_and_short_frames() {
assert!(decode_command(&[]).is_err());
assert!(decode_command(&[0xEE]).is_err());
assert!(decode_command(&[TAG_RESET, 1, 2]).is_err()); assert!(decode_response(&[TAG_OBS, 0, 0, 0]).is_err()); }
}