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 }