//! The main room for the snake game use std::collections::HashMap; use rand::Rng; use sdl2::{ event::Event, keyboard::Scancode, rect::Rect, render::{Canvas, TextureCreator}, video::{Window, WindowContext} }; use ycraft::{ collision::CollisionShape, obj::{ ControlObjectBehavior, Frame, GameObjectBehavior, GameObjectState, Sprite }, res::{Font, Image, Sound}, room::Room }; use crate::game::{ Img, Snd, Fnt, Spr, Rm, Data, BASE_MOVE_SPD, MOVE_SPD_INC }; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub enum Dir { Up, Down, Left, Right } #[derive(Clone)] struct SnakeHead { state: GameObjectState, move_spd: f64, inter_pos: (f64, f64), can_change_dir: bool, add_body_seg: bool, should_die: bool, play_eat_snd: bool } impl SnakeHead { pub fn new() -> Self { let pos = (640.0 / 2.0 + 32.0 + 32.0 / 2.0, 352.0 / 2.0); Self { state: GameObjectState { name: "head".to_string(), pos, collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) }, cur_spr: Spr::Head, sprs: HashMap::from([( Spr::Head, Sprite::new( vec![ Frame::new(Img::Snake, Rect::new(0, 0, 32, 32), (32, 32)) ], 0.0, (16, 16) ) )]), custom: Data::Head { dir: Dir::Right, lurch_propagation: 0 } }, move_spd: BASE_MOVE_SPD, inter_pos: pos, can_change_dir: true, add_body_seg: false, should_die: false, play_eat_snd: false } } } impl GameObjectBehavior for SnakeHead { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { let nw = SnakeHead::new(); self.state = nw.state; self.move_spd = nw.move_spd; self.inter_pos = nw.inter_pos; self.can_change_dir = nw.can_change_dir; self.add_body_seg = nw.add_body_seg; self.should_die = false; false } fn handle_sdl_event(&mut self, event: &Event) { match event { Event::KeyDown { scancode, .. } => if scancode.is_some() { if let Data::Head { ref mut dir, .. } = self.state.custom { if self.can_change_dir { // Keep how much you've moved in a dir when switching. // This keeps each increment the same time length. // It's like momentum let pos_dif = match *dir { Dir::Up | Dir::Down => (self.inter_pos.1 - self.state.pos.1).abs(), Dir::Left | Dir::Right => (self.inter_pos.0 - self.state.pos.0).abs() }; self.inter_pos = self.state.pos; match scancode.unwrap() { Scancode::Up => { self.can_change_dir = false; *dir = Dir::Up; self.inter_pos.1 -= pos_dif; }, Scancode::Down => { self.can_change_dir = false; *dir = Dir::Down; self.inter_pos.1 += pos_dif; }, Scancode::Left => { self.can_change_dir = false; *dir = Dir::Left; self.inter_pos.0 -= pos_dif; }, Scancode::Right => { self.can_change_dir = false; *dir = Dir::Right; self.inter_pos.0 += pos_dif; }, _ => {} } } } }, _ => {} } } fn update( &mut self, delta: f64, ctl_objs: &Vec>>, others: &Vec>>) -> ( Option, Vec>> ) { let mut score = 0; for obj in ctl_objs.iter() { if let Data::Score(sc) = obj.data() { score = sc; break; } } let mut added_objs: Vec>> = Vec::new(); if let Data::Head { ref mut dir, ref mut lurch_propagation } = self.state.custom { if *lurch_propagation == 0 { match dir { Dir::Up => { if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = 0.0; } self.inter_pos.1 -= delta * self.move_spd; if self.inter_pos.1.floor() < self.state.pos.1 - 32.0 { self.can_change_dir = true; self.state.pos.1 -= 32.0; *lurch_propagation = score; } }, Dir::Down => { if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = 180.0; } self.inter_pos.1 += delta * self.move_spd; if self.inter_pos.1.floor() > self.state.pos.1 + 32.0 { self.can_change_dir = true; self.state.pos.1 += 32.0; *lurch_propagation = score; } }, Dir::Left => { if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = 270.0; } self.inter_pos.0 -= delta * self.move_spd; if self.inter_pos.0.floor() < self.state.pos.0 - 32.0 { self.can_change_dir = true; self.state.pos.0 -= 32.0; *lurch_propagation = score; } }, Dir::Right => { if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = 90.0; } self.inter_pos.0 += delta * self.move_spd; if self.inter_pos.0.floor() > self.state.pos.0 + 32.0 { self.can_change_dir = true; self.state.pos.0 += 32.0; *lurch_propagation = score; } } } } else { *lurch_propagation -= 1; } if self.add_body_seg { let mut max_body = -1; let mut max_body_pos = (0.0, 0.0); for other in others.iter() { if let Data::Body { index, .. } = other.state().custom { if index > max_body { max_body = index; max_body_pos = other.state().pos; } } } added_objs.push(Box::new(SnakeBody::new(max_body + 1, max_body_pos))); self.add_body_seg = false; self.move_spd += MOVE_SPD_INC; } if self.state.pos.0 < 32.0 || self.state.pos.1 < 32.0 || self.state.pos.0 > 640.0 - 32.0 || self.state.pos.1 > 360.0 - 32.0 { return (Some(Rm::Dead), vec![]); } if self.should_die { return (Some(Rm::Dead), vec![]); } } (None, added_objs) } fn on_collision( &mut self, other: &Box>) { match other.state().custom { Data::Mouse => { self.add_body_seg = true; self.play_eat_snd = true; }, Data::Tail | Data::Body { .. } => { if let Data::Head { lurch_propagation, .. } = self.state.custom { if lurch_propagation == 0 { self.should_die = true; } } } _ => {} } } fn render( &mut self, cnv: &mut Canvas, imgs: &HashMap, snds: &HashMap, _fonts: &HashMap, _creator: &TextureCreator, elapsed: f64) -> Result<(), String> { if self.play_eat_snd { snds[&Snd::Bite].play()?; self.play_eat_snd = false; } // Default render let mut state = self.state().clone(); let GameObjectState { ref mut sprs, ref mut cur_spr, pos, .. } = state; if let Some(spr) = sprs.get_mut(cur_spr) { spr.update(elapsed); spr.render(cnv, imgs, (pos.0 as i32, pos.1 as i32))?; } self.set_state(&state); Ok(()) } } #[derive(Clone)] struct SnakeBody { state: GameObjectState, last_dir: Dir, last_pos: (f64, f64), def_pos: (f64, f64) } impl SnakeBody { pub fn new(index: isize, def_pos: (f64, f64)) -> Self { Self { state: GameObjectState { name: format!("snake_body_{}", index), pos: def_pos, collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) }, cur_spr: Spr::Body, sprs: HashMap::from([( Spr::Body, Sprite::new( vec![ Frame::new(Img::Snake, Rect::new(32, 0, 32, 32), (32, 32)) ], 0.0, (16, 16) ) )]), custom: Data::Body { index, dir: Dir::Right } }, last_dir: Dir::Right, last_pos: def_pos, def_pos } } } impl GameObjectBehavior for SnakeBody { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { if let Data::Body { index, .. } = self.state.custom { if index > 1 { true } else { let nw = SnakeBody::new(index, self.def_pos); self.state = nw.state; self.last_dir = nw.last_dir; self.last_pos = nw.last_pos; false } } else { true } } fn update( &mut self, _delta: f64, _ctl_objs: &Vec>>, others: &Vec>>) -> ( Option, Vec>> ) { if let Data::Body { ref mut index, ref mut dir } = self.state.custom { let parent_id = if *index == 0 { "head".to_string() } else { format!("snake_body_{}", *index - 1) }; for other in others.iter() { if *other.state().name == parent_id && other.state().pos != self.last_pos { self.state.pos = self.last_pos; *dir = self.last_dir; self.last_pos = other.state().pos; if *index == 0 { if let Data::Head { dir: other_dir, .. } = other.state().custom { self.last_dir = other_dir; } } else { if let Data::Body { dir: other_dir, .. } = other.state().custom { self.last_dir = other_dir; } } } } if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = match dir { Dir::Up => 0.0, Dir::Down => 180.0, Dir::Left => 270.0, Dir::Right => 90.0 }; } } (None, vec![]) } } #[derive(Clone)] struct SnakeTail { state: GameObjectState, dir: Dir, last_dir: Dir, last_pos: (f64, f64), } impl SnakeTail { pub fn new() -> Self { Self { state: GameObjectState { name: "snake_tail".to_string(), pos: (640.0 / 2.0 - 32.0 - 32.0 / 2.0, 352.0 / 2.0), cur_spr: Spr::Tail, sprs: HashMap::from([( Spr::Tail, Sprite::new( vec![ Frame::new(Img::Snake, Rect::new(0, 32, 32, 32), (32, 32)) ], 0.0, (16, 16) ) )]), collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) }, custom: Data::Tail }, dir: Dir::Right, last_dir: Dir::Right, last_pos: (640.0 / 2.0 - 32.0 - 32.0 / 2.0, 352.0 / 2.0), } } } impl GameObjectBehavior for SnakeTail { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { let nw = SnakeTail::new(); self.state = nw.state; self.last_pos = nw.last_pos; self.dir = nw.dir; self.last_dir = nw.dir; false } fn update( &mut self, _delta: f64, _ctl_objs: &Vec>>, others: &Vec>>) -> ( Option, Vec>> ) { let mut max_body = -1; for other in others.iter() { if let Data::Body { index, .. } = other.state().custom { if index > max_body { max_body = index; } } } for other in others.iter() { if let Data::Body { index, dir: other_dir} = other.state().custom { if index == max_body && self.last_pos != other.state().pos { self.dir = self.last_dir; self.state.pos = self.last_pos; self.last_dir = other_dir; self.last_pos = other.state().pos; } } } if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.angle = match self.dir { Dir::Up => 0.0, Dir::Down => 180.0, Dir::Left => 270.0, Dir::Right => 90.0 }; } (None, vec![]) } } #[derive(Clone)] struct Mouse { state: GameObjectState } impl Mouse { pub fn new() -> Self { Self { state: GameObjectState { name: "mouse".to_string(), pos: Self::random_mouse_pos(), cur_spr: Spr::Mouse, sprs: HashMap::from([( Spr::Mouse, Sprite::new( vec![ Frame::new(Img::Mouse, Rect::new(0, 0, 32, 32), (32, 32)) ], 0.0, (16, 16) ) )]), collider: CollisionShape::Circle { center: (0, 0), radius: 15 }, custom: Data::Mouse } } } fn random_mouse_pos() -> (f64, f64) { let mut rng = rand::thread_rng(); ( (rng.gen_range(32.0..640.0 - 96.0) / 32.0 as f64).floor() * 32.0 + 16.0, (rng.gen_range(32.0..360.0 - 96.0) / 32.0 as f64).floor() * 32.0 + 16.0 ) } } impl GameObjectBehavior for Mouse { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { let nw = Mouse::new(); self.state = nw.state; false } fn on_collision( &mut self, other: &Box>) { if let Data::Head { .. } = other.state().custom { self.on_reset(); } } } #[derive(Clone)] struct Board { state: GameObjectState } impl Board { pub fn new() -> Self { Self { state: GameObjectState { name: "board".to_string(), pos: (0.0, 0.0), collider: CollisionShape::Rect { center: (320, 180), size: (640, 480) }, cur_spr: Spr::Board, sprs: HashMap::from([( Spr::Board, Sprite::new( vec![Frame::new( Img::Board, Rect::new(0, 0, 640, 360), (640, 360) )], 0.0, (0, 0) ) )]), custom: Data::Board } } } } impl GameObjectBehavior for Board { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { false } } pub fn play() -> Room { Room::new( vec![ Box::new(Board::new()), Box::new(SnakeHead::new()), Box::new(SnakeBody::new(0, (640.0 / 2.0 + 32.0 / 2.0, 352.0 / 2.0))), Box::new(SnakeBody::new(1, (640.0 / 2.0 - 32.0 / 2.0, 352.0 / 2.0))), Box::new(SnakeTail::new()), Box::new(Mouse::new()) ], false ) }