diff --git a/Cargo.lock b/Cargo.lock index afc300e..e6e6733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,7 @@ dependencies = [ "ahash", "hashbrown", "mino", + "serde", ] [[package]] diff --git a/fish/Cargo.toml b/fish/Cargo.toml index 75bcbb1..9d6adb7 100644 --- a/fish/Cargo.toml +++ b/fish/Cargo.toml @@ -5,10 +5,12 @@ version = "0.1.0" edition = "2021" [features] -default = [] +default = ["io"] +io = ["serde"] [dependencies] mino = { path = "../mino" } ahash = "0.8" hashbrown = "0.13" +serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/fish/src/io.rs b/fish/src/io.rs new file mode 100644 index 0000000..d253832 --- /dev/null +++ b/fish/src/io.rs @@ -0,0 +1,194 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; +use mino::srs::PieceType; +use mino::MatBuf; +use mino::{srs::Piece, Loc}; +use serde::{Deserialize, Serialize}; + +/// Deserializable description of an initial game state. This is intentionally similar to +/// the TBP "start" message. +#[derive(Clone, Debug, Deserialize)] +pub struct InputState { + #[serde(flatten)] + pub queue: InputQueue, + #[allow(dead_code)] + #[serde(flatten)] + _attack: AttackState, + // XXX(iitalics): TBP uses the terminology "board" but we use "matrix". + #[serde(rename = "board")] + pub matrix: InputMatrix, +} + +/// Deserializable description of the queue. +#[derive(Clone, Debug, Deserialize)] +pub struct InputQueue { + #[serde(deserialize_with = "deserialize_hold")] + pub hold: Option, + #[serde(deserialize_with = "deserialize_previews")] + #[serde(rename = "queue")] + pub previews: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[allow(dead_code)] +struct AttackState { + combo: u32, + back_to_back: u32, + // TODO: use this? +} + +// use FromStr to deserialize a PieceType + +fn deserialize_hold<'de, D>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::::deserialize(de)? + .map(|s| s.parse()) + .transpose() + .map_err(serde::de::Error::custom) +} + +fn deserialize_previews<'de, D>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Vec::::deserialize(de)? + .iter() + .map(|s| s.parse()) + .collect::, _>>() + .map_err(serde::de::Error::custom) +} + +/// Deserializable description of the game matrix. +#[derive(Clone, Debug)] +pub struct InputMatrix { + pub cells: Box<[[Option; 10]; 40]>, +} + +impl InputMatrix { + /// Converts this matrix to a [`MatBuf`], which discards color information but is + /// stored more efficiently. + pub fn to_mat(&self) -> MatBuf { + let mut mat = MatBuf::new(); + for (y, row) in self.cells.iter().enumerate() { + for (x, cell) in row.iter().enumerate() { + if cell.is_some() { + mat.set(x as i16, y as i16); + } + } + } + mat + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deserialize)] +#[repr(u8)] +pub enum Color { + I = 1, + J = 2, + L = 3, + O = 4, + S = 5, + T = 6, + Z = 7, + G = 8, +} + +impl<'de> serde::de::Deserialize<'de> for InputMatrix { + fn deserialize(de: D) -> Result + where + D: serde::Deserializer<'de>, + { + // XXX(iitalics): serde doesn't let you deserialize a [T; 40] so we have to go + // through Vec first and then check the length :| + let cells: Vec<_> = serde::de::Deserialize::deserialize(de)?; + let cells: [_; 40] = cells + .try_into() + .map_err(|_| serde::de::Error::custom("board must contain 40 rows"))?; + Ok(Self { + cells: Box::new(cells), + }) + } +} + +#[derive(Clone, Debug, Serialize, Default)] +pub struct OutputMoves { + pub moves: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct OutputMove { + pub location: OutputLocation, + pub spin: Spin, +} + +#[derive(Clone, Debug, Serialize)] +pub struct OutputLocation { + #[serde(serialize_with = "serialize_piece_type")] + #[serde(rename = "type")] + pub ty: PieceType, + #[serde(serialize_with = "serialize_loc")] + #[serde(flatten)] + pub location: Loc, +} + +impl From for OutputLocation { + fn from(pc: Piece) -> Self { + Self { + ty: pc.ty, + location: pc.loc, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Spin { + #[default] + None, + Mini, + Full, +} + +fn serialize_piece_type(ty: &PieceType, ser: S) -> Result +where + S: serde::Serializer, +{ + ty.as_char().serialize(ser) +} + +fn serialize_loc(loc: &Loc, ser: S) -> Result +where + S: serde::Serializer, +{ + use serde::ser::SerializeStruct; + let mut ser = ser.serialize_struct("Location", 3)?; + ser.serialize_field("x", &loc.x)?; + ser.serialize_field("y", &loc.y)?; + let r = ["north", "east", "south", "west"][loc.r as usize]; + ser.serialize_field("orientation", r)?; + ser.end() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_color_repr() { + assert_eq!( + core::mem::size_of::>(), + core::mem::size_of::() + ); + assert_eq!( + core::mem::size_of::<[Option; 10]>(), + core::mem::size_of::<[u8; 10]>() + ); + assert_eq!( + unsafe { core::mem::transmute::, u8>(None) }, + 0u8 + ); + } +} diff --git a/fish/src/lib.rs b/fish/src/lib.rs index f6deba4..f53dd73 100644 --- a/fish/src/lib.rs +++ b/fish/src/lib.rs @@ -4,4 +4,7 @@ extern crate alloc; pub mod find; +#[cfg(feature = "io")] +pub mod io; + pub use find::find_locations;