cli_2048/
lib.rs

1use rand::Rng;
2use phf::phf_map;
3use std::fmt;
4
5/// Holds the game state.
6pub struct Grid {
7    //rows contain cols
8    rows: Vec<Vec<u8>>,
9    pipes: &'static PipeMap,
10}
11
12impl Default for Grid {
13    fn default() -> Self {
14        Grid {
15            rows: vec![vec![0; 4]; 4],
16            pipes: &PIPEMAP_THICK,
17        }
18    }
19}
20
21impl Grid {
22    /// Creates a new grid with the given size and adds two numbers on random positions.
23    /// The size must be greater than 0 and greater than 1 in at least one dimension.
24    /// # Examples
25    ///
26    /// ```
27    /// use cli_2048::Grid;
28    /// 
29    /// //Create a 4x4 grid
30    /// let grid = Grid::new(4, 4);
31    ///
32    /// ```
33    pub fn new(x_size: usize, y_size: usize) -> Grid {
34        if x_size < 1 || y_size < 1 || x_size < 2 && y_size < 2 {
35            panic!("Grid size cannot be 0");
36        }
37        let grid = Grid {
38            rows: vec![vec![0; x_size]; y_size],
39            ..Default::default()
40        };
41        //add two starting numbers
42        grid.add_random_number().unwrap().add_random_number().unwrap()
43    }
44
45    /// Creates a new grid from a predefined grid.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use cli_2048::Grid;
51    /// 
52    /// //Create a 4x4 vector
53    /// let rows = vec![vec![0; 4]; 4];
54    /// //Create a grid from the vector
55    /// let grid = Grid::from_rows(rows);
56    ///
57    /// ```
58    pub fn from_rows(rows: Vec<Vec<u8>>) -> Grid {
59        Grid {
60            rows,
61            ..Default::default()
62        }
63    }
64
65    /// Returns a new instance of the grid, with a custom pipe-map for the borders.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use cli_2048::Grid;
71    /// use cli_2048::PIPEMAPS;
72    /// 
73    /// //Create a 4x4 grid
74    /// let grid = Grid::new(4, 4);
75    /// //Create a grid with a custom pipe-map
76    /// let grid = grid.with_pipes(PIPEMAPS.get("Medium").unwrap());
77    /// 
78    /// //Or like this
79    /// let grid = Grid::new(4, 4).with_pipes(PIPEMAPS.get("Medium").unwrap());
80    /// 
81    /// ```
82    pub fn with_pipes(&self, pipes: &'static PipeMap) -> Grid {
83        Grid {
84            rows: self.rows.clone(),
85            pipes,
86        }
87    }
88    /// Gets the size of the grid in characters and with borders.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use cli_2048::Grid;
94    /// 
95    /// //Create a 4x4 grid
96    /// let grid = Grid::new(4, 4);
97    /// 
98    /// let x_size = 4*2 + (4-1) + 2;
99    /// let y_size = 4 + (4-1) + 2;
100    ///
101    /// assert_eq!(grid.get_size(), (x_size, y_size));
102    /// 
103    /// ```
104    pub fn get_size(&self) -> (usize, usize) {
105        (self.rows.len() * self.formatted_numbers()[0][0].len() + self.rows.len() + 1, self.rows[0].len() * 2 + 1)
106    }
107    /// Returns a new Grid from the previous.
108    /// Slides and combines the grid in the given direction.
109    /// If the tiles change a new tile will be added at a random empty position.
110    /// If the grid is full the game is over (Err("no more options")).
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use cli_2048::Grid;
116    /// use cli_2048::Direction;
117    /// 
118    /// //Create a 4x4 grid
119    /// let grid = Grid::new(4, 4);
120    /// let grid = grid.slide(Direction::Up);
121    /// let grid = grid.slide(Direction::Down);
122    /// 
123    /// ```
124    pub fn slide(&self, dir: Direction) -> Result<Grid, &'static str> {
125        let mut rows: Vec<Vec<u8>> = self.rows.clone();
126
127        (|| {
128            match dir {
129                Direction::LEFT => {
130                    //Rotate
131                    // -
132                    //Operate
133                    rows = rows.iter().map(|row| self.combine_row(row)).collect();
134                    //Rotate back
135                    // -
136                    //Return
137                    return Ok(());
138                }
139                
140                Direction::RIGHT => {
141                    //Rotate
142                    rows = rows.iter().map(|row| row.iter().rev().cloned().collect()).collect();
143                    //Operate
144                    rows = rows.iter().map(|row| self.combine_row(row)).collect();
145                    //Rotate back
146                    rows = rows.iter().map(|row| row.iter().rev().cloned().collect()).collect();
147                    //Return
148                    return Ok(());
149                }
150                
151                Direction::UP => {
152                    //Rotate
153                    rows = (0..rows[0].len()).map(|col| rows.iter().map(|row| row[col]).collect()).collect();
154                    //Operate
155                    rows = rows.iter().map(|row| self.combine_row(row)).collect();
156                    //Rotate back
157                    rows = (0..rows[0].len()).map(|col| rows.iter().map(|row| row[col]).collect()).collect();
158                    //Return
159                    return Ok(());
160                }
161                Direction::DOWN => {
162                    //Rotate
163                    rows = (0..rows[0].len()).map(|col| rows.iter().map(|row| row[col]).collect()).collect();
164                    rows = rows.iter().map(|row| row.iter().rev().cloned().collect()).collect();
165                    //Operate
166                    rows = rows.iter().map(|row| self.combine_row(row)).collect();
167                    //Rotate back
168                    rows = rows.iter().map(|row| row.iter().rev().cloned().collect()).collect();
169                    rows = (0..rows[0].len()).map(|col| rows.iter().map(|row| row[col]).collect()).collect();
170                    //Return
171                    return Ok(());
172                }
173            }
174        })()?;
175
176        let new_grid = Grid { rows, ..Default::default() }; 
177        let new_grid_with_new_number = new_grid.add_random_number()?;
178        //see if grid has changed
179        if new_grid.rows != self.rows {
180            return Ok(new_grid_with_new_number);
181        }
182        Ok(new_grid)
183    }
184    
185    fn compress_row(&self, row: &Vec<u8>) -> Vec<u8> {
186        let mut new_row = row.iter().filter(|&x| *x != 0).cloned().collect::<Vec<u8>>();
187        new_row.append(&mut vec![0; row.len() - new_row.len()]);
188        new_row
189    }
190
191    fn combine_row(&self, row: &Vec<u8>) -> Vec<u8> {
192        let mut row = self.compress_row(&row);
193        for i in 0..(row.len() - 1) {
194            if row[i] == row[i+1] && row[i] != 0 {
195                row[i] += 1;
196                row[i+1] = 0;
197            }
198        }
199        self.compress_row(&row)
200    }
201
202    fn add_random_number(&self) -> Result<Grid, &'static str> {
203        //get index of all 0 cells
204        let options: Vec<(usize, usize)> = self.rows.iter().enumerate().flat_map(|(x, row)| {
205            row.iter().enumerate().filter(|(_, &cell)| cell == 0).map(move |(y, _)| (x, y))
206        }).collect();
207        
208        //check for no options (GAME OVER)
209        if options.is_empty() {
210            return Err("no more options");
211        }
212
213        let mut rng = rand::thread_rng();
214
215        //get random option
216        let option = options[rng.gen_range(0..options.len())];
217
218        let mut power = 1;
219        //1 in 10 chance of getting 4
220        if rng.gen_range(1..10) == 10 {
221            power = 2;
222        }
223
224        let mut new_rows = self.rows.clone();
225        new_rows[option.0][option.1] = power;
226
227        Ok(Grid { rows: new_rows, ..Default::default() })
228    }
229    fn formatted_numbers(&self) -> Vec<Vec<String>> {
230
231        let mut longest_string_len = 2;
232        for number in self.rows.iter().flatten() {
233            if format_number(number, 0).len() > longest_string_len {
234                longest_string_len = format_number(number, 0).len();
235            }
236        }
237
238        return self.rows.iter().map(|row| {
239            row.iter().map(|number| format_number(number, longest_string_len)).collect()
240        }).collect();
241
242        fn format_number(&number: &u8, len: usize) -> String {
243            let digits = format_digits(&number);
244            if len == 0 {
245                return digits;
246            }
247            return (0..len-digits.len()).map(|_| " ".to_string()).collect::<Vec<String>>().join("") + &digits;
248
249            fn format_digits(&number: &u8) -> String {
250                match number {
251                    0 => " ".to_string(),
252                    x => format!("{}", (2 as usize).pow(x as u32)),
253                }
254            }
255        }
256    }
257}
258
259impl fmt::Debug for Grid {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        let mut grid_str = format!("{} by {} Grid:\n[\n", self.rows.len(), self.rows[0].len());
262        for row in &self.rows {
263            grid_str.push_str("  [");
264            for val in row {
265                grid_str.push_str(&format!(" {} ", val));
266            }
267            grid_str.push_str("]\n");
268        }
269        grid_str.push_str("]\n");
270        
271        
272        Ok(
273            write!(f, "{}", grid_str)?
274        )
275    }
276}
277
278impl fmt::Display for Grid {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280
281        //set pipes
282        let pipes = &self.pipes;
283
284        let grid_str = self.formatted_numbers();
285
286        //draw top border
287        write!(f, "{}", pipes.get("top_left").unwrap())?;
288        for i in 0..grid_str[0].len() {
289            for _ in 0..grid_str[0][0].len() {
290                write!(f, "{}", pipes.get("horizontal").unwrap())?;
291            }
292            if i != grid_str[0].len() - 1 {
293                write!(f, "{}", pipes.get("top_horizontal").unwrap())?;
294            }
295        }
296        write!(f, "{}\n", pipes.get("top_right").unwrap())?;
297
298        //draw row
299        for i in 0..grid_str.len() {
300            for col in &grid_str[i] {
301                write!(f, "{}{}", pipes.get("vertical").unwrap(), col)?;
302            }
303            write!(f, "{}\n", pipes.get("vertical").unwrap())?;
304
305            //Draw bottom border or cross-line
306            if i == grid_str.len() - 1 {
307                //bottom border
308                write!(f, "{}", pipes.get("bottom_left").unwrap())?;
309                for i in 0..grid_str[0].len() {
310                    for _ in 0..grid_str[0][0].len() {
311                        write!(f, "{}", pipes.get("horizontal").unwrap())?;
312                    }
313                    if i != grid_str[0].len() - 1 {
314                        write!(f, "{}", pipes.get("bottom_horizontal").unwrap())?;
315                    }
316                }
317                write!(f, "{}\n", pipes.get("bottom_right").unwrap())?;
318            } else {
319                //cross-line
320                write!(f, "{}", pipes.get("left_vertical").unwrap())?;
321                for i in 0..grid_str[0].len() {
322                    for _ in 0..grid_str[0][0].len() {
323                        write!(f, "{}", pipes.get("horizontal").unwrap())?;
324                    }
325                    if i != grid_str[0].len() - 1 {
326                        write!(f, "{}", pipes.get("cross").unwrap())?;
327                    }
328                }
329                write!(f, "{}\n", pipes.get("right_vertical").unwrap())?;
330            }
331        }
332
333        Ok(())
334    }
335}
336
337    /// Used as parameters to the slide function.
338    /// # Examples
339    ///
340    /// ```
341    /// use cli_2048::Grid;
342    /// use cli_2048::Direction;
343    /// 
344    /// //Create a 4x4 grid
345    /// let grid = Grid::new(4, 4);
346    /// let grid = grid.slide(Direction::Up);
347    /// let grid = grid.slide(Direction::Down);
348    /// 
349    /// ```
350pub enum Direction {
351    LEFT,
352    RIGHT,
353    UP,
354    DOWN,
355}
356
357type PipeMap = phf::Map<&'static str, &'static str>;
358
359    /// Contains three pipe-map presets:
360    /// Thin
361    /// Medium
362    /// Thick
363    /// 
364    /// # Examples
365    ///
366    /// ```
367    /// use cli_2048::Grid;
368    /// use cli_2048::PIPEMAPS;
369    /// 
370    /// //Create a 4x4 grid
371    /// let grid = Grid::new(4, 4);
372    /// //Create a grid with a custom pipe-map
373    /// let grid = grid.with_pipes(PIPEMAPS.get("Medium").unwrap());
374    /// 
375    /// //Or like this
376    /// let grid = Grid::new(4, 4).with_pipes(PIPEMAPS.get("Medium").unwrap());
377    /// 
378    /// ```
379pub static PIPEMAPS: phf::Map<&'static str, &'static PipeMap> = phf_map! {
380    "Thin" => &PIPEMAP_THIN,
381    "Medium" => &PIPEMAP_MEDIUM,
382    "Thick" => &PIPEMAP_THICK,
383};
384
385static PIPEMAP_THIN: PipeMap = phf_map! {
386    "horizontal" => "─",
387    "vertical" => "│",
388    "top_left" => "┌",
389    "top_right" => "┐",
390    "bottom_left" => "└",
391    "bottom_right" => "┘",
392    "top_horizontal" => "┬",
393    "bottom_horizontal" => "┴",
394    "left_vertical" => "├",
395    "right_vertical" => "┤",
396    "cross" => "┼",
397};
398static PIPEMAP_MEDIUM: PipeMap = phf_map! {
399    "horizontal" => "━",
400    "vertical" => "┃",
401    "top_left" => "┏",
402    "top_right" => "┓",
403    "bottom_left" => "┗",
404    "bottom_right" => "┛",
405    "top_horizontal" => "┳",
406    "bottom_horizontal" => "┻",
407    "left_vertical" => "┣",
408    "right_vertical" => "┫",
409    "cross" => "╋",
410};
411static PIPEMAP_THICK: PipeMap = phf_map! {
412    "horizontal" => "═",
413    "vertical" => "║",
414    "top_left" => "╔",
415    "top_right" => "╗",
416    "bottom_left" => "╚",
417    "bottom_right" => "╝",
418    "top_horizontal" => "╦",
419    "bottom_horizontal" => "╩",
420    "left_vertical" => "╠",
421    "right_vertical" => "╣",
422    "cross" => "╬",
423};
424
425
426//tests
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    #[should_panic]
433    fn zero_grid() {
434        let _grid = Grid::new(1, 0);
435    }
436    #[test]
437    fn add_random_number() {
438        let grid = Grid::new(4, 4);
439        let grid = grid.add_random_number().unwrap();
440        println!("{:?}", grid);
441    }
442
443    #[test]
444    fn compress_row() {
445        let grid = Grid::new(4, 4);
446        let row = grid.compress_row(&vec![2, 0, 4, 0]);
447        assert_eq!(row, vec![2, 4, 0, 0]);
448        let row = grid.compress_row(&vec![4, 0, 2, 8]);
449        assert_eq!(row, vec![4, 2, 8, 0]);
450    }
451
452    #[test]
453    fn combine_row() {
454        let grid = Grid::new(4, 4);
455        let row = grid.combine_row(&vec![1, 0, 1, 0]);
456        assert_eq!(row, vec![2, 0, 0, 0]);
457        let row = grid.combine_row(&vec![2, 2, 3, 4, 6, 6, 5, 0, 6]);
458        assert_eq!(row, vec![3, 3, 4, 7, 5, 6, 0, 0, 0]);
459    }
460
461    #[test]
462    fn overwrite_rows() {
463        let grid = Grid::new(2, 2);
464        assert_eq!(grid.rows.len(), 2);
465        let new_rows = vec![vec![1; 4]; 4];
466        let grid = Grid::from_rows(new_rows.clone());
467        assert_eq!(grid.rows, new_rows);
468    }
469}