snake/
play.rs

1//! The main room for the snake game
2
3use std::collections::HashMap;
4use rand::Rng;
5use sdl2::{
6    event::Event,
7    keyboard::Scancode,
8    rect::Rect, render::{Canvas, TextureCreator}, video::{Window, WindowContext}
9};
10use ycraft::{
11    collision::CollisionShape,
12    obj::{
13        ControlObjectBehavior, Frame, GameObjectBehavior, GameObjectState, Sprite
14    }, res::{Font, Image, Sound}, room::Room
15};
16use crate::game::{
17    Img, Snd, Fnt, Spr, Rm, Data, BASE_MOVE_SPD, MOVE_SPD_INC
18};
19
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
21pub enum Dir {
22    Up,
23    Down,
24    Left,
25    Right
26}
27
28#[derive(Clone)]
29struct SnakeHead {
30    state: GameObjectState<Img, Spr, Data>,
31    move_spd: f64,
32    inter_pos: (f64, f64),
33    can_change_dir: bool,
34    add_body_seg: bool,
35    should_die: bool,
36    play_eat_snd: bool
37}
38
39impl SnakeHead {
40    pub fn new() -> Self {
41        let pos = (640.0 / 2.0 + 32.0 + 32.0 / 2.0, 352.0 / 2.0);
42        Self {
43            state: GameObjectState {
44                name: "head".to_string(),
45                pos,
46                collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) },
47                cur_spr: Spr::Head,
48                sprs: HashMap::from([(
49                    Spr::Head,
50                    Sprite::new(
51                        vec![ Frame::new(Img::Snake, Rect::new(0, 0, 32, 32), (32, 32)) ],
52                        0.0, (16, 16)
53                    )
54                )]), custom: Data::Head {
55                    dir: Dir::Right,
56                    lurch_propagation: 0
57                }
58            }, move_spd: BASE_MOVE_SPD,
59            inter_pos: pos,
60            can_change_dir: true,
61            add_body_seg: false,
62            should_die: false,
63            play_eat_snd: false
64        }
65    }
66}
67
68impl GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data> for SnakeHead {
69    fn state(&self) -> GameObjectState<Img, Spr, Data> {
70        self.state.clone()
71    }
72
73    fn set_state(&mut self, new_state: &GameObjectState<Img, Spr, Data>) {
74        self.state = new_state.clone();
75    }
76
77    fn on_reset(&mut self) -> bool {
78        let nw = SnakeHead::new();
79        self.state = nw.state;
80        self.move_spd = nw.move_spd;
81        self.inter_pos = nw.inter_pos;
82        self.can_change_dir = nw.can_change_dir;
83        self.add_body_seg = nw.add_body_seg;
84        self.should_die = false;
85        false
86    }
87
88    fn handle_sdl_event(&mut self, event: &Event) {
89        match event {
90            Event::KeyDown { scancode, .. } => if scancode.is_some() {
91                if let Data::Head { ref mut dir, .. } = self.state.custom {
92                    if self.can_change_dir {
93                        // Keep how much you've moved in a dir when switching.
94                        // This keeps each increment the same time length.
95                        // It's like momentum
96                        let pos_dif = match *dir {
97                            Dir::Up | Dir::Down => (self.inter_pos.1 - self.state.pos.1).abs(),
98                            Dir::Left | Dir::Right => (self.inter_pos.0 - self.state.pos.0).abs()
99                        };
100                        self.inter_pos = self.state.pos;
101                        match scancode.unwrap() {
102                            Scancode::Up => {
103                                self.can_change_dir = false;
104                                *dir = Dir::Up;
105                                self.inter_pos.1 -= pos_dif;
106                            }, Scancode::Down => {
107                                self.can_change_dir = false;
108                                *dir = Dir::Down;
109                                self.inter_pos.1 += pos_dif;
110                            }, Scancode::Left => {
111                                self.can_change_dir = false;
112                                *dir = Dir::Left;
113                                self.inter_pos.0 -= pos_dif;
114                            }, Scancode::Right => {
115                                self.can_change_dir = false;
116                                *dir = Dir::Right;
117                                self.inter_pos.0 += pos_dif;
118                            }, _ => {}
119                        }
120                    }
121                }
122            }, _ => {}
123        }
124    }
125
126    fn update(
127            &mut self, delta: f64,
128            ctl_objs: &Vec<Box<dyn ControlObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>,
129            others: &Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>) -> (
130                Option<Rm>,
131                Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>
132            ) {
133        let mut score = 0;
134        for obj in ctl_objs.iter() {
135            if let Data::Score(sc) = obj.data() {
136                score = sc;
137                break;
138            }
139        }
140        let mut added_objs: Vec<Box<dyn GameObjectBehavior<_, _, _, _, _, _>>> = Vec::new();
141        if let Data::Head { ref mut dir, ref mut lurch_propagation } =
142                self.state.custom {
143            if *lurch_propagation == 0 {
144                match dir {
145                    Dir::Up => {
146                        if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
147                            spr.angle = 0.0;
148                        }
149                        self.inter_pos.1 -= delta * self.move_spd;
150                        if self.inter_pos.1.floor() < self.state.pos.1 - 32.0 {
151                            self.can_change_dir = true;
152                            self.state.pos.1 -= 32.0;
153                            *lurch_propagation = score;
154                        }
155                    }, Dir::Down => {
156                        if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
157                            spr.angle = 180.0;
158                        }
159                        self.inter_pos.1 += delta * self.move_spd;
160                        if self.inter_pos.1.floor() > self.state.pos.1 + 32.0 {
161                            self.can_change_dir = true;
162                            self.state.pos.1 += 32.0;
163                            *lurch_propagation = score;
164                        }
165                    }, Dir::Left => {
166                        if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
167                            spr.angle = 270.0;
168                        }
169                        self.inter_pos.0 -= delta * self.move_spd;
170                        if self.inter_pos.0.floor() < self.state.pos.0 - 32.0 {
171                            self.can_change_dir = true;
172                            self.state.pos.0 -= 32.0;
173                            *lurch_propagation = score;
174                        }
175                    }, Dir::Right => {
176                        if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
177                            spr.angle = 90.0;
178                        }
179                        self.inter_pos.0 += delta * self.move_spd;
180                        if self.inter_pos.0.floor() > self.state.pos.0 + 32.0 {
181                            self.can_change_dir = true;
182                            self.state.pos.0 += 32.0;
183                            *lurch_propagation = score;
184                        }
185                    }
186                }
187            } else {
188                *lurch_propagation -= 1;
189            }
190
191            if self.add_body_seg {
192                let mut max_body = -1;
193                let mut max_body_pos = (0.0, 0.0);
194                for other in others.iter() {
195                    if let Data::Body { index, .. } = other.state().custom {
196                        if index > max_body {
197                            max_body = index;
198                            max_body_pos = other.state().pos;
199                        }
200                    }
201                }
202                added_objs.push(Box::new(SnakeBody::new(max_body + 1, max_body_pos)));
203                self.add_body_seg = false;
204                self.move_spd += MOVE_SPD_INC;
205            }
206
207            if self.state.pos.0 < 32.0 || self.state.pos.1 < 32.0
208                    || self.state.pos.0 > 640.0 - 32.0 || self.state.pos.1 > 360.0 - 32.0 {
209                return (Some(Rm::Dead), vec![]);
210            }
211
212            if self.should_die {
213                return (Some(Rm::Dead), vec![]);
214            }
215        }
216        (None, added_objs)
217    }
218
219    fn on_collision(
220            &mut self,
221            other: &Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>) {
222        match other.state().custom {
223            Data::Mouse => {
224                self.add_body_seg = true;
225                self.play_eat_snd = true;
226            }, Data::Tail | Data::Body { .. } => {
227                if let Data::Head { lurch_propagation, .. } = self.state.custom {
228                    if lurch_propagation == 0 {
229                        self.should_die = true;
230                    }
231                }
232            }
233            _ => {}
234        }
235    }
236
237    fn render(
238                &mut self, cnv: &mut Canvas<Window>,
239                imgs: &HashMap<Img, Image>, snds: &HashMap<Snd, Sound>,
240                _fonts: &HashMap<Fnt, Font>, _creator: &TextureCreator<WindowContext>,
241                elapsed: f64) -> Result<(), String> {
242        if self.play_eat_snd {
243            snds[&Snd::Bite].play()?;
244            self.play_eat_snd = false;
245        }
246
247        // Default render
248        let mut state = self.state().clone();
249        let GameObjectState { ref mut sprs, ref mut cur_spr, pos, .. } = state;
250        if let Some(spr) = sprs.get_mut(cur_spr) {
251            spr.update(elapsed);
252            spr.render(cnv, imgs, (pos.0 as i32, pos.1 as i32))?;
253        }
254        self.set_state(&state);
255        Ok(())
256    }
257}
258
259#[derive(Clone)]
260struct SnakeBody {
261    state: GameObjectState<Img, Spr, Data>,
262    last_dir: Dir,
263    last_pos: (f64, f64),
264    def_pos: (f64, f64)
265}
266
267impl SnakeBody {
268    pub fn new(index: isize, def_pos: (f64, f64)) -> Self {
269        Self {
270            state: GameObjectState {
271                name: format!("snake_body_{}", index),
272                pos: def_pos,
273                collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) },
274                cur_spr: Spr::Body,
275                sprs: HashMap::from([(
276                    Spr::Body,
277                    Sprite::new(
278                        vec![ Frame::new(Img::Snake, Rect::new(32, 0, 32, 32), (32, 32)) ],
279                        0.0, (16, 16)
280                    )
281                )]), custom: Data::Body {
282                    index,
283                    dir: Dir::Right
284                }
285            }, last_dir: Dir::Right,
286            last_pos: def_pos,
287            def_pos
288        }
289    }
290}
291
292impl GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data> for SnakeBody {
293    fn state(&self) -> GameObjectState<Img, Spr, Data> {
294        self.state.clone()
295    }
296
297    fn set_state(&mut self, new_state: &GameObjectState<Img, Spr, Data>) {
298        self.state = new_state.clone();
299    }
300
301    fn on_reset(&mut self) -> bool {
302        if let Data::Body { index, .. } = self.state.custom {
303            if index > 1 {
304                true
305            } else {
306                let nw = SnakeBody::new(index, self.def_pos);
307                self.state = nw.state;
308                self.last_dir = nw.last_dir;
309                self.last_pos = nw.last_pos;
310                false
311            }
312        } else {
313            true
314        }
315    }
316
317    fn update(
318            &mut self, _delta: f64,
319            _ctl_objs: &Vec<Box<dyn ControlObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>,
320            others: &Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>) -> (
321                Option<Rm>,
322                Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>
323            ) {
324        if let Data::Body { ref mut index, ref mut dir } = self.state.custom {
325            let parent_id = if *index == 0 {
326                "head".to_string()
327            } else {
328                format!("snake_body_{}", *index - 1)
329            };
330            for other in others.iter() {
331                if *other.state().name == parent_id && other.state().pos != self.last_pos {
332                    self.state.pos = self.last_pos;
333                    *dir = self.last_dir;
334                    self.last_pos = other.state().pos;
335                    if *index == 0 {
336                        if let Data::Head { dir: other_dir, .. } = other.state().custom {
337                            self.last_dir = other_dir;
338                        }
339                    } else {
340                        if let Data::Body { dir: other_dir, .. } = other.state().custom {
341                            self.last_dir = other_dir;
342                        }
343                    }
344                }
345            }
346            if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
347                spr.angle = match dir {
348                    Dir::Up => 0.0,
349                    Dir::Down => 180.0,
350                    Dir::Left => 270.0,
351                    Dir::Right => 90.0
352                };
353            }
354        }
355        (None, vec![])
356    }
357}
358
359#[derive(Clone)]
360struct SnakeTail {
361    state: GameObjectState<Img, Spr, Data>,
362    dir: Dir,
363    last_dir: Dir,
364    last_pos: (f64, f64),
365}
366
367impl SnakeTail {
368    pub fn new() -> Self {
369        Self {
370            state: GameObjectState {
371                name: "snake_tail".to_string(),
372                pos: (640.0 / 2.0 - 32.0 - 32.0 / 2.0, 352.0 / 2.0),
373                cur_spr: Spr::Tail,
374                sprs: HashMap::from([(
375                    Spr::Tail,
376                    Sprite::new(
377                        vec![ Frame::new(Img::Snake, Rect::new(0, 32, 32, 32), (32, 32)) ],
378                        0.0, (16, 16)
379                    )
380                )]), collider: CollisionShape::Rect { center: (0, 0), size: (31, 31) },
381                custom: Data::Tail
382            }, dir: Dir::Right,
383            last_dir: Dir::Right,
384            last_pos: (640.0 / 2.0 - 32.0 - 32.0 / 2.0, 352.0 / 2.0),
385        }
386    }
387}
388
389impl GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data> for SnakeTail {
390    fn state(&self) -> GameObjectState<Img, Spr, Data> {
391        self.state.clone()
392    }
393
394    fn set_state(&mut self, new_state: &GameObjectState<Img, Spr, Data>) {
395        self.state = new_state.clone();
396    }
397
398    fn on_reset(&mut self) -> bool {
399        let nw = SnakeTail::new();
400        self.state = nw.state;
401        self.last_pos = nw.last_pos;
402        self.dir = nw.dir;
403        self.last_dir = nw.dir;
404        false
405    }
406
407    fn update(
408            &mut self, _delta: f64,
409            _ctl_objs: &Vec<Box<dyn ControlObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>,
410            others: &Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>) -> (
411                Option<Rm>, Vec<Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>>
412            ) {
413        let mut max_body = -1;
414        for other in others.iter() {
415            if let Data::Body { index, .. } = other.state().custom {
416                if index > max_body {
417                    max_body = index;
418                }
419            }
420        }
421        for other in others.iter() {
422            if let Data::Body { index, dir: other_dir} = other.state().custom {
423                if index == max_body && self.last_pos != other.state().pos {
424                    self.dir = self.last_dir;
425                    self.state.pos = self.last_pos;
426                    self.last_dir = other_dir;
427                    self.last_pos = other.state().pos;
428                }
429            }
430        }
431        if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) {
432            spr.angle = match self.dir {
433                Dir::Up => 0.0,
434                Dir::Down => 180.0,
435                Dir::Left => 270.0,
436                Dir::Right => 90.0
437            };
438        }
439        (None, vec![])
440    }
441}
442
443#[derive(Clone)]
444struct Mouse {
445    state: GameObjectState<Img, Spr, Data>
446}
447
448impl Mouse {
449    pub fn new() -> Self {
450        Self {
451            state: GameObjectState {
452                name: "mouse".to_string(),
453                pos: Self::random_mouse_pos(),
454                cur_spr: Spr::Mouse,
455                sprs: HashMap::from([(
456                    Spr::Mouse,
457                    Sprite::new(
458                        vec![ Frame::new(Img::Mouse, Rect::new(0, 0, 32, 32), (32, 32)) ],
459                        0.0, (16, 16)
460                    )
461                )]), collider: CollisionShape::Circle { center: (0, 0), radius: 15 },
462                custom: Data::Mouse
463            }
464        }
465    }
466
467    fn random_mouse_pos() -> (f64, f64) {
468        let mut rng = rand::thread_rng();
469        (
470            (rng.gen_range(32.0..640.0 - 96.0) / 32.0 as f64).floor() * 32.0 + 16.0,
471            (rng.gen_range(32.0..360.0 - 96.0) / 32.0 as f64).floor() * 32.0 + 16.0
472        )
473    }
474}
475
476impl GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data> for Mouse {
477    fn state(&self) -> GameObjectState<Img, Spr, Data> {
478        self.state.clone()
479    }
480
481    fn set_state(&mut self, new_state: &GameObjectState<Img, Spr, Data>) {
482        self.state = new_state.clone();
483    }
484
485    fn on_reset(&mut self) -> bool {
486        let nw = Mouse::new();
487        self.state = nw.state;
488        false
489    }
490
491    fn on_collision(
492            &mut self,
493            other: &Box<dyn GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data>>) {
494        if let Data::Head { .. } = other.state().custom {
495            self.on_reset();
496        }
497    }
498}
499
500#[derive(Clone)]
501struct Board {
502    state: GameObjectState<Img, Spr, Data>
503}
504
505impl Board {
506    pub fn new() -> Self {
507        Self {
508            state: GameObjectState {
509                name: "board".to_string(),
510                pos: (0.0, 0.0),
511                collider: CollisionShape::Rect { center: (320, 180), size: (640, 480) },
512                cur_spr: Spr::Board,
513                sprs: HashMap::from([(
514                    Spr::Board,
515                    Sprite::new(
516                        vec![Frame::new(
517                            Img::Board, Rect::new(0, 0, 640, 360), (640, 360)
518                        )], 0.0, (0, 0)
519                    )
520                )]), custom: Data::Board
521            }
522        }
523    }
524}
525
526impl GameObjectBehavior<Img, Snd, Fnt, Spr, Rm, Data> for Board {
527    fn state(&self) -> GameObjectState<Img, Spr, Data> {
528        self.state.clone()
529    }
530
531    fn set_state(&mut self, new_state: &GameObjectState<Img, Spr, Data>) {
532        self.state = new_state.clone();
533    }
534
535    fn on_reset(&mut self) -> bool {
536        false
537    }
538}
539
540pub fn play() -> Room<Img, Snd, Fnt, Spr, Rm, Data> {
541    Room::new(
542        vec![
543            Box::new(Board::new()),
544            Box::new(SnakeHead::new()),
545            Box::new(SnakeBody::new(0, (640.0 / 2.0 + 32.0 / 2.0, 352.0 / 2.0))),
546            Box::new(SnakeBody::new(1, (640.0 / 2.0 - 32.0 / 2.0, 352.0 / 2.0))),
547            Box::new(SnakeTail::new()),
548            Box::new(Mouse::new())
549        ], false
550    )
551}
552