library

Online co-op book organization video game
git clone https://git.bradleygannon.com/library.git
Log | Files | Refs

game.rs (16025B)


      1 use std::{
      2     collections::HashMap,
      3     fmt::Display,
      4     io::Write,
      5     time::{Duration, SystemTime},
      6 };
      7 
      8 use anyhow::{Result, anyhow};
      9 use lazy_static::lazy_static;
     10 use macroquad::{
     11     math::{Rect, Vec2},
     12     rand::{rand, srand},
     13 };
     14 use serde::{Deserialize, Serialize};
     15 use uuid::Uuid;
     16 
     17 lazy_static! {
     18     pub static ref BOOKS: BookDatabase =
     19         BookDatabase::from_csv(include_str!("../assets/books.csv"))
     20             .expect("Failed to parse book database");
     21 }
     22 
     23 pub const TICK_DURATION: Duration = Duration::from_micros(16_667);
     24 
     25 pub type BookId = Uuid;
     26 pub type Players = HashMap<Uuid, Player>;
     27 
     28 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
     29 pub struct GameState {
     30     timestamp: SystemTime,
     31     players: Players,
     32     bookshelves: Vec<Bookshelf>,
     33     floor_books: Vec<FloorBook>,
     34 }
     35 
     36 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
     37 pub enum Update {
     38     InsertPlayer { id: Uuid, player: Player },
     39     RemovePlayer { player_id: Uuid },
     40 }
     41 
     42 /// Equivalent to [macroquad::math::Rect::overlaps], except using strict comparison (i.e., `<` and
     43 /// `>` instead of `<=` and `>=`)
     44 fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool {
     45     a.left() < b.right() && a.right() > b.left() && a.top() < b.bottom() && a.bottom() > b.top()
     46 }
     47 
     48 impl GameState {
     49     pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.;
     50 
     51     pub fn update(&mut self, update: &Update) {
     52         match update {
     53             Update::InsertPlayer { id, player } => self.players.insert(*id, player.clone()),
     54             Update::RemovePlayer { player_id } => self.players.remove(player_id),
     55         };
     56     }
     57     pub fn apply(&mut self, action: &Action) {
     58         match action.kind() {
     59             ActionKind::Join { name } => {
     60                 self.players.insert(action.player_id, Player::new(name));
     61             }
     62             ActionKind::Move { new_position } => {
     63                 if let Some(player) = self.players.get_mut(&action.player_id)
     64                     && let Ok(dt) = action.timestamp.duration_since(player.timestamp)
     65                 {
     66                     let velocity = (new_position - player.position) / dt.as_secs_f32();
     67                     let limited_velocity = if velocity.length() > GameState::MAX_PLAYER_SPEED_PX_S {
     68                         velocity.normalize() * GameState::MAX_PLAYER_SPEED_PX_S
     69                     } else {
     70                         velocity
     71                     };
     72                     player.position += limited_velocity * dt.as_secs_f32();
     73 
     74                     // Handle collisions with bookshelves
     75                     let player_collider = player.collider.rect().offset(player.position);
     76                     for bookshelf in self.bookshelves.iter() {
     77                         // If the player's movement would cause their collider to intersect with a
     78                         // bookshelf's collider, then compute the minimum axis-aligned motion
     79                         // required to make them touch but not intersect
     80                         let bookshelf_collider: Rect = bookshelf.bounds.into();
     81                         let (a, b) = (bookshelf_collider, player_collider);
     82                         if rects_strictly_overlap(&a, &b) {
     83                             // Compute the required motion in each possible direction and select the
     84                             // smallest among them
     85                             let up = Vec2::new(0., a.top() - b.bottom());
     86                             let down = Vec2::new(0., a.bottom() - b.top());
     87                             let left = Vec2::new(a.left() - b.right(), 0.);
     88                             let right = Vec2::new(a.right() - b.left(), 0.);
     89                             let min_y = if up.y.abs() < down.y.abs() { up } else { down };
     90                             let min_x = if right.x.abs() < left.x.abs() {
     91                                 right
     92                             } else {
     93                                 left
     94                             };
     95                             let min = if min_x.x.abs() < min_y.y.abs() {
     96                                 min_x
     97                             } else {
     98                                 min_y
     99                             };
    100                             player.position += min;
    101                         }
    102                     }
    103                 }
    104             }
    105         }
    106     }
    107     pub fn interpolate(
    108         old: &Self,
    109         new: &Self,
    110         basis: SystemTime,
    111         local_player_id: Option<Uuid>,
    112     ) -> GameState {
    113         let mut output = GameState::default();
    114         for (new_id, new_player) in new.players.iter() {
    115             if let Some(old_player) = old.players.get(new_id) {
    116                 if let Some(ref lpid) = local_player_id
    117                     && new_id == lpid
    118                 {
    119                     output.players.insert(*new_id, new_player.clone());
    120                 } else {
    121                     let span = new.timestamp.duration_since(old.timestamp);
    122                     let fraction = SystemTime::now().duration_since(basis);
    123                     let t = match (fraction, span) {
    124                         (Ok(f), Ok(s)) => (f.as_secs_f32() / s.as_secs_f32()).clamp(0., 1.),
    125                         _ => return new.clone(),
    126                     };
    127                     let interpolated_player = Player {
    128                         position: (new_player.position() - old_player.position()) * t
    129                             + old_player.position(),
    130                         ..new_player.clone()
    131                     };
    132                     output.players.insert(*new_id, interpolated_player);
    133                 }
    134             } else {
    135                 output.players.insert(*new_id, new_player.clone());
    136             }
    137         }
    138         output
    139     }
    140     pub fn get_player(&self, id: &Uuid) -> Option<&Player> {
    141         self.players.get(id)
    142     }
    143     pub fn get_player_mut(&mut self, id: &Uuid) -> Option<&mut Player> {
    144         self.players.get_mut(id)
    145     }
    146     pub fn players(&self) -> &HashMap<Uuid, Player> {
    147         &self.players
    148     }
    149     pub fn insert_player(&mut self, id: Uuid, player: Player) -> Option<Player> {
    150         self.players.insert(id, player)
    151     }
    152     pub fn remove_player(&mut self, id: &Uuid) -> Option<Player> {
    153         self.players.remove(id)
    154     }
    155     pub fn set_timestamp(&mut self, timestamp: SystemTime) {
    156         self.timestamp = timestamp;
    157     }
    158     pub fn get_timestamp(&self) -> SystemTime {
    159         self.timestamp
    160     }
    161     pub fn update_timestamp(&mut self) {
    162         self.set_timestamp(SystemTime::now());
    163     }
    164     pub fn bookshelves(&self) -> &Vec<Bookshelf> {
    165         self.bookshelves.as_ref()
    166     }
    167     pub fn floor_books(&self) -> &Vec<FloorBook> {
    168         self.floor_books.as_ref()
    169     }
    170 }
    171 
    172 impl Default for GameState {
    173     fn default() -> Self {
    174         Self {
    175             timestamp: SystemTime::now(),
    176             players: HashMap::with_capacity(32),
    177             bookshelves: vec![
    178                 Bookshelf {
    179                     bounds: RectSerde {
    180                         x: -200.,
    181                         y: -100.,
    182                         w: 100.,
    183                         h: 500.,
    184                     },
    185                 },
    186                 Bookshelf {
    187                     bounds: RectSerde {
    188                         x: -400.,
    189                         y: -100.,
    190                         w: 100.,
    191                         h: 500.,
    192                     },
    193                 },
    194             ],
    195             floor_books: {
    196                 srand(0);
    197                 BOOKS
    198                     .books
    199                     .keys()
    200                     .map(|&id| FloorBook {
    201                         id,
    202                         position: Vec2::new((rand() % 750) as f32, (rand() % 750) as f32),
    203                         collider: Rect::new(0., 0., 30., 23.6).into(),
    204                     })
    205                     .collect()
    206             },
    207         }
    208     }
    209 }
    210 
    211 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    212 pub struct Player {
    213     name: String,
    214     position: Vec2,
    215     sprite_index: SpriteIndex,
    216     collider: RectSerde,
    217     timestamp: SystemTime,
    218 }
    219 
    220 impl Player {
    221     fn new(name: String) -> Self {
    222         Player {
    223             name,
    224             position: Vec2::ZERO,
    225             sprite_index: SpriteIndex::Meeple,
    226             collider: RectSerde::new(0., 0., 43.4, 40.2),
    227             timestamp: SystemTime::now(),
    228         }
    229     }
    230     pub fn name(&self) -> String {
    231         self.name.clone()
    232     }
    233     pub fn position(&self) -> Vec2 {
    234         self.position
    235     }
    236     pub fn sprite_index(&self) -> SpriteIndex {
    237         self.sprite_index
    238     }
    239     pub fn collider(&self) -> RectSerde {
    240         self.collider
    241     }
    242 }
    243 
    244 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
    245 pub struct RectSerde {
    246     x: f32,
    247     y: f32,
    248     w: f32,
    249     h: f32,
    250 }
    251 
    252 impl RectSerde {
    253     pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
    254         RectSerde { x, y, w, h }
    255     }
    256     pub fn rect(&self) -> Rect {
    257         (*self).into()
    258     }
    259 }
    260 
    261 impl From<Rect> for RectSerde {
    262     fn from(value: Rect) -> Self {
    263         RectSerde {
    264             x: value.x,
    265             y: value.y,
    266             w: value.w,
    267             h: value.h,
    268         }
    269     }
    270 }
    271 
    272 impl From<RectSerde> for Rect {
    273     fn from(value: RectSerde) -> Self {
    274         Rect::new(value.x, value.y, value.w, value.h)
    275     }
    276 }
    277 
    278 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
    279 pub struct Bookshelf {
    280     bounds: RectSerde,
    281 }
    282 
    283 impl Bookshelf {
    284     pub fn rect(&self) -> Rect {
    285         self.bounds.into()
    286     }
    287 }
    288 
    289 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
    290 pub struct FloorBook {
    291     id: BookId,
    292     position: Vec2,
    293     collider: RectSerde,
    294 }
    295 
    296 impl FloorBook {
    297     pub fn id(&self) -> BookId {
    298         self.id
    299     }
    300     pub fn position(&self) -> Vec2 {
    301         self.position
    302     }
    303     pub fn collider(&self) -> RectSerde {
    304         self.collider
    305     }
    306 }
    307 
    308 pub struct BookDatabase {
    309     books: HashMap<BookId, BookMetadata>,
    310 }
    311 
    312 fn _csv_escape(input: &String) -> String {
    313     if input.contains(',') {
    314         format!("\"{input}\"")
    315     } else {
    316         input.clone()
    317     }
    318 }
    319 
    320 impl BookDatabase {
    321     const CSV_HEADER: &str = "id,title,author,publish_year";
    322 
    323     fn new() -> Self {
    324         BookDatabase {
    325             books: HashMap::new(),
    326         }
    327     }
    328     fn _to_csv(&self, output: &mut impl Write) -> Result<()> {
    329         writeln!(output, "{}", BookDatabase::CSV_HEADER)?;
    330         for (id, metadata) in self.books.iter() {
    331             let BookMetadata {
    332                 title,
    333                 author,
    334                 publish_year,
    335             } = metadata;
    336             writeln!(
    337                 output,
    338                 "{},{},{},{}",
    339                 id,
    340                 _csv_escape(title),
    341                 _csv_escape(author),
    342                 _csv_escape(publish_year),
    343             )?;
    344         }
    345         Ok(())
    346     }
    347     fn from_csv(input: &str) -> Result<Self> {
    348         let mut output = BookDatabase::new();
    349         for (line_index, line) in input.lines().enumerate() {
    350             if line_index == 0 {
    351                 if line != BookDatabase::CSV_HEADER {
    352                     return Err(anyhow!(
    353                         "Invalid CSV header: expected '{}' but got '{}'",
    354                         BookDatabase::CSV_HEADER,
    355                         line
    356                     ));
    357                 }
    358                 continue;
    359             }
    360             let mut fields = vec![];
    361             #[derive(Clone, Copy, Debug)]
    362             enum State {
    363                 Start,
    364                 FieldData,
    365                 Quote,
    366                 FieldEnd,
    367             }
    368             let mut state = State::Start;
    369             let mut new_field = String::new();
    370             let mut quoted_field = false;
    371             for c in line.chars() {
    372                 match (c, state) {
    373                     ('"', State::Start | State::FieldEnd) => {
    374                         quoted_field = true;
    375                         state = State::FieldData;
    376                     }
    377                     ('"', State::FieldData) => state = State::Quote,
    378                     ('"', State::Quote) => {
    379                         if quoted_field {
    380                             new_field.push('"');
    381                         }
    382                     }
    383                     (',', State::Start | State::FieldEnd) => state = State::FieldEnd,
    384                     (',', State::FieldData) => {
    385                         if quoted_field {
    386                             new_field.push(',');
    387                         } else {
    388                             state = State::FieldEnd;
    389                         }
    390                     }
    391                     (',', State::Quote) => {
    392                         if quoted_field {
    393                             state = State::FieldEnd;
    394                         }
    395                     }
    396                     (d, _) => {
    397                         new_field.push(d);
    398                         state = State::FieldData;
    399                     }
    400                 }
    401                 if let State::FieldEnd = state {
    402                     fields.push(new_field.clone());
    403                     new_field.clear();
    404                     quoted_field = false;
    405                 }
    406             }
    407             fields.push(new_field);
    408             if fields.len() != 4 {
    409                 return Err(anyhow!(
    410                     "Error on line {}: expected 4 fields but got {}",
    411                     line_index + 1,
    412                     fields.len()
    413                 ));
    414             }
    415             let id = fields[0].parse()?;
    416             let title = fields[1].clone();
    417             let author = fields[2].clone();
    418             let publish_year = fields[3].clone();
    419             output.books.insert(
    420                 id,
    421                 BookMetadata {
    422                     title,
    423                     author,
    424                     publish_year,
    425                 },
    426             );
    427         }
    428         Ok(output)
    429     }
    430     pub fn get(&self, k: &BookId) -> Option<&BookMetadata> {
    431         self.books.get(k)
    432     }
    433 }
    434 
    435 #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
    436 pub struct BookMetadata {
    437     title: String,
    438     author: String,
    439     publish_year: String,
    440 }
    441 
    442 impl Display for BookMetadata {
    443     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    444         write!(
    445             f,
    446             "{} by {} ({})",
    447             self.title, self.author, self.publish_year
    448         )
    449     }
    450 }
    451 
    452 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    453 pub struct Action {
    454     sequence_number: u32,
    455     timestamp: SystemTime,
    456     player_id: Uuid,
    457     kind: ActionKind,
    458 }
    459 
    460 impl Action {
    461     pub fn new(sequence_number: u32, player_id: Uuid, kind: ActionKind) -> Self {
    462         Action {
    463             sequence_number,
    464             timestamp: SystemTime::now(),
    465             player_id,
    466             kind,
    467         }
    468     }
    469     pub fn sequence_number(&self) -> u32 {
    470         self.sequence_number
    471     }
    472     pub fn player_id(&self) -> Uuid {
    473         self.player_id
    474     }
    475     pub fn kind(&self) -> ActionKind {
    476         self.kind.clone()
    477     }
    478 }
    479 
    480 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    481 pub enum ActionKind {
    482     Join { name: String },
    483     Move { new_position: Vec2 },
    484 }
    485 
    486 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
    487 pub enum SpriteIndex {
    488     Meeple,
    489     FloorBook,
    490 }
    491 
    492 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    493 pub struct Response {
    494     timestamp: SystemTime,
    495     sequence_number: u32,
    496     kind: ResponseKind,
    497 }
    498 
    499 impl Response {
    500     pub fn new(timestamp: SystemTime, sequence_number: u32, kind: ResponseKind) -> Self {
    501         Response {
    502             timestamp,
    503             sequence_number,
    504             kind,
    505         }
    506     }
    507     pub fn timestamp(&self) -> SystemTime {
    508         self.timestamp
    509     }
    510     pub fn sequence_number(&self) -> u32 {
    511         self.sequence_number
    512     }
    513     pub fn kind(&self) -> ResponseKind {
    514         self.kind.clone()
    515     }
    516 }
    517 
    518 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    519 pub enum ResponseKind {
    520     SetId { id: Uuid },
    521     UpdateGameState { updates: Vec<Update> },
    522     ReplaceGameState { new_game_state: GameState },
    523 }