From 32cf3d280bf80deca899c82caddde78a714e0337 Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Mon, 4 Apr 2022 18:22:57 +0300 Subject: [PATCH] big refactor - moved display logic into their own module for less cluttering - made abstractions for the display process so it can be owned by the game manager and game may be played without displays. - separated the main game management from the game struct makes it easier to clone games and play few turns on the fly. --- src/lib/game.rs | 74 ++------------- src/lib/game_manager.rs | 114 ++++++++++++++++++++++ src/lib/grid.rs | 192 ++------------------------------------ src/lib/grid_displayer.rs | 188 +++++++++++++++++++++++++++++++++++++ src/lib/mod.rs | 2 + src/main.rs | 19 ++-- 6 files changed, 327 insertions(+), 262 deletions(-) create mode 100644 src/lib/game_manager.rs create mode 100644 src/lib/grid_displayer.rs diff --git a/src/lib/game.rs b/src/lib/game.rs index 24c7f9e..09c976e 100644 --- a/src/lib/game.rs +++ b/src/lib/game.rs @@ -1,16 +1,13 @@ -use std::{error::Error, fmt::Display, thread, time::Duration}; +use std::{error::Error, fmt::Display}; use super::{ - controller::{Controller, ControllerError, Move}, + controller::{ControllerError, Move}, grid::Grid, }; pub struct Rules { size: usize, spawn_per_turn: usize, - clear_term: bool, - color_seed: u16, - pause: Duration, } impl Rules { @@ -23,21 +20,6 @@ impl Rules { self.spawn_per_turn = spawn_per_turn; self } - - pub fn clear_term(mut self, clear_term: bool) -> Self { - self.clear_term = clear_term; - self - } - - pub fn color_seed(mut self, color_seed: u16) -> Self { - self.color_seed = color_seed; - self - } - - pub fn pause(mut self, pause: Duration) -> Self { - self.pause = pause; - self - } } impl Default for Rules { @@ -45,9 +27,6 @@ impl Default for Rules { Self { size: 4, spawn_per_turn: 1, - clear_term: true, - color_seed: 35, - pause: Duration::ZERO, } } } @@ -79,8 +58,6 @@ impl Error for GameError {} pub struct Game { board: Grid, spawn_per_turn: usize, - clear_term: bool, - pause: Duration, } impl Game { @@ -88,21 +65,16 @@ impl Game { let Rules { size, spawn_per_turn, - clear_term, - color_seed, - pause, } = rules; Self { - board: Grid::new(size, color_seed), + board: Grid::new(size), spawn_per_turn, - clear_term, - pause, } } - pub fn controlled(self, controller: Box) -> ControlledGame { - ControlledGame::new(self, controller, true) + pub fn get_board(&self) -> &Grid { + &self.board } pub fn turn(&mut self, movement: Move) -> Result<(), GameError> { @@ -110,11 +82,10 @@ impl Game { for _ in 0..self.spawn_per_turn { self.spawn_random()?; } - thread::sleep(self.pause); Ok(()) } - pub fn spawn_random(&mut self) -> Result<(), GameError> { + fn spawn_random(&mut self) -> Result<(), GameError> { let mut potentials = vec![]; for x in 0..self.board.size() { for y in 0..self.board.size() { @@ -134,14 +105,6 @@ impl Game { Ok(()) } - pub fn refresh_display(&self) { - if self.clear_term { - super::clear_term(); - } - let text = self.board.display(); - println!("{text}"); - } - // TODO: macro peut être ? pub fn perform_move(&mut self, movement: Move) { match movement { @@ -261,28 +224,3 @@ fn would_overflow(number: usize, delta: isize, max: usize) -> bool { let too_big = number == max && delta == 1; too_little || too_big } - -pub struct ControlledGame { - game: Game, - controller: Box, - display: bool, -} - -impl ControlledGame { - pub fn new(game: Game, controller: Box, display: bool) -> Self { - Self { - game, - controller, - display, - } - } - - pub fn turn(&mut self) -> Result<(), GameError> { - if self.display { - self.game.refresh_display(); - } - let movement = self.controller.next_move(&self.game.board)?; - self.game.turn(movement)?; - Ok(()) - } -} diff --git a/src/lib/game_manager.rs b/src/lib/game_manager.rs new file mode 100644 index 0000000..03da2ad --- /dev/null +++ b/src/lib/game_manager.rs @@ -0,0 +1,114 @@ +use std::{thread, time::Duration}; + +use crate::lib::{ + controller::Controller, + game::{self, Game, GameError}, +}; + +use super::grid_displayer::GridDisplayer; + +pub struct Rules { + display: bool, + display_skips: usize, + clear_term: bool, + color_seed: u16, + turn_duration: Duration, +} + +impl Rules { + pub fn clear_term(mut self, clear_term: bool) -> Self { + self.clear_term = clear_term; + self + } + + pub fn color_seed(mut self, color_seed: u16) -> Self { + self.color_seed = color_seed; + self + } + + pub fn turn_duration(mut self, turn_duration: Duration) -> Self { + self.turn_duration = turn_duration; + self + } +} + +impl Default for Rules { + fn default() -> Self { + Self { + display: true, + display_skips: 0, + clear_term: true, + color_seed: 35, + turn_duration: Duration::ZERO, + } + } +} + +pub struct GameManager { + game: Game, + controller: Box, + grid_displayer: GridDisplayer, + display_to_skip: usize, + display: bool, + display_skips: usize, + clear_term: bool, + turn_duration: Duration, +} + +impl GameManager { + pub fn new( + game_rules: game::Rules, + manager_rules: self::Rules, + controller: Box, + ) -> Self { + let game = Game::new(game_rules); + let Rules { + clear_term, + color_seed, + display, + display_skips, + turn_duration, + } = manager_rules; + let grid_displayer = GridDisplayer::new(color_seed); + Self { + game, + controller, + display_to_skip: 0, + display, + display_skips, + clear_term, + turn_duration, + grid_displayer, + } + } + + pub fn turn(&mut self) -> Result<(), GameError> { + if self.display { + if self.display_to_skip == 0 { + self.refresh_display(); + self.display_to_skip = self.display_skips; + } else { + self.display_to_skip -= 1; + } + } + let movement = self.controller.next_move(self.game.get_board())?; + self.game.turn(movement)?; + thread::sleep(self.turn_duration); + Ok(()) + } + + pub fn refresh_display(&self) { + if self.clear_term { + super::clear_term(); + } + let grid = self.game.get_board(); + let text = self.grid_displayer.display(grid); + println!("{text}"); + } + + pub fn play_all(&mut self) -> Result<(), GameError> { + loop { + self.turn()?; + } + } +} diff --git a/src/lib/grid.rs b/src/lib/grid.rs index 275f73f..646f934 100644 --- a/src/lib/grid.rs +++ b/src/lib/grid.rs @@ -1,11 +1,3 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, - mem::transmute_copy, -}; - -use termion::color; - #[derive(Clone, Copy)] pub struct Tile { value: Option, @@ -32,111 +24,21 @@ impl Tile { } } -/// -/// displayability -/// -impl Tile { - const TILE_LENGTH: usize = 7; - const TILE_HEIGHT: usize = 3; - - pub fn display(&self, color_seed: u16) -> String { - match self.value { - Some(value) => { - Self::color_representation(Self::display_number(value), value, color_seed) - } - None => [ - // empty tile - " ", " ", " ", - ] - .join("\n"), - } - } - - fn display_number(value: usize) -> String { - let result = [ - // number tile - "┌─ ─┐", - &Self::pad_both(value.to_string(), Self::TILE_LENGTH), - "└─ ─┘", - ] - .join("\n"); - result - } - - fn pad_both(text: String, length: usize) -> String { - let mut text = text; - while text.len() < length { - text = format!(" {text} "); - } - if text.len() > length { - (&text)[..length].to_string() - } else { - text - } - } - - fn color_representation(text: String, value: usize, color_seed: u16) -> String { - let color = Self::hashed_color(value, color_seed); - let color_code = color::Bg(color); - let reset_code = color::Bg(color::Reset); - - let text = text - .split("\n") - .map(|line| format!("{color_code}{line}{reset_code}")) - .collect::>() - .join("\n"); - text - } - - // [ | | ] - - fn hashed_color(value: usize, color_seed: u16) -> color::Rgb { - let mut hasher = DefaultHasher::new(); - (value + color_seed as usize).hash(&mut hasher); - let hash = hasher.finish(); - // SAFETY: - // there are no logic that relies on the value of the outputted numbers, thus it is safe to create them without constructors - let [frac_a, frac_b]: [f64; 2] = unsafe { transmute_copy::<_, [u32; 2]>(&hash) } - .into_iter() - .map(|frac| (frac as f64) / (u32::MAX as f64)) - .collect::>() - .try_into() - .unwrap(); - - let mut remaining = 255f64; - let r = Self::take_fraction(&mut remaining, frac_a, 150.) as u8; - let g = Self::take_fraction(&mut remaining, frac_b, 150.) as u8; - let b = remaining as u8; - color::Rgb(r, g, b) - } - - fn take_fraction(remainder: &mut f64, frac: f64, max: f64) -> f64 { - let result = (*remainder * frac).min(max); - *remainder -= result; - result - } -} - #[derive(Clone)] pub struct Grid { size: usize, tiles: Vec>, - color_seed: u16, } impl Grid { /// /// constructor /// - pub fn new(size: usize, color_seed: u16) -> Self { + pub fn new(size: usize) -> Self { let tiles = (0..size) .map(|_| (0..size).map(|_| Tile::new_empty()).collect()) .collect(); - Self { - size, - tiles, - color_seed, - } + Self { size, tiles } } /// @@ -177,6 +79,13 @@ impl Grid { self.size } + /// + /// get the array of tiles + /// + pub fn tiles(&self) -> &Vec> { + &self.tiles + } + /// /// move a tile over another one, replace the previously occupied place by an empty tile and overrides the destination /// @@ -186,86 +95,3 @@ impl Grid { self.tiles[src_y][src_x] = Tile::new_empty(); } } - -/// -/// displayability -/// -impl Grid { - /// (0: '┘'), (1: '┐'), (2: '┌'), (3: '└'), (4: '┼'), (5: '─'), (6: '├'), (7: '┤'), (8: '┴'), (9: '┬'), (10: '│') - const DISPLAY_CHAR: [&'static str; 11] = - ["┘", "┐", "┌", "└", "┼", "─", "├", "┤", "┴", "┬", "│"]; - - /// - /// returns a string of multiple lines representing the grid - /// - pub fn display(&self) -> String { - let tiles: Vec> = self - .tiles - .iter() - .map(|row| { - row.iter() - .map(|tile| tile.display(self.color_seed)) - .collect() - }) - .collect(); - let row_representations: Vec<_> = tiles - .iter() - .map(|row_representation| { - let mut row_lines = (0..Tile::TILE_HEIGHT).map(|_| vec![]).collect::>(); - // push every item lines in [`row_lines`] - for item_representation in row_representation { - item_representation - .split('\n') - .into_iter() - .zip(row_lines.iter_mut()) - .for_each(|(item_line, row_line)| row_line.push(item_line.to_string())); - } - // join lines of [`row_lines`] - let row_lines = row_lines - .iter_mut() - .map(|line_parts| line_parts.join(Self::DISPLAY_CHAR[10]).to_string()) - .map(|line| [Self::DISPLAY_CHAR[10], &line, Self::DISPLAY_CHAR[10]].join("")) - .collect::>(); - row_lines.join("\n") - }) - .collect(); - - [ - self.first_grid_display_line(), - row_representations.join(&self.between_grid_display_line()), - self.last_grid_display_line(), - ] - .join("\n") - } - - fn first_grid_display_line(&self) -> String { - let middle = (0..self.size) - .map(|_| Self::DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) - .collect::>() - .join(Self::DISPLAY_CHAR[9]); - [Self::DISPLAY_CHAR[2], &middle, Self::DISPLAY_CHAR[1]].join("") - } - - fn between_grid_display_line(&self) -> String { - let middle = (0..self.size) - .map(|_| Self::DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) - .collect::>() - .join(Self::DISPLAY_CHAR[4]); - [ - "\n", - Self::DISPLAY_CHAR[6], - &middle, - Self::DISPLAY_CHAR[7], - "\n", - ] - .join("") - } - - fn last_grid_display_line(&self) -> String { - let middle = (0..self.size) - .map(|_| Self::DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) - .collect::>() - .join(Self::DISPLAY_CHAR[8]); - [Self::DISPLAY_CHAR[3], &middle, Self::DISPLAY_CHAR[0], "\n"].join("") - } -} diff --git a/src/lib/grid_displayer.rs b/src/lib/grid_displayer.rs new file mode 100644 index 0000000..c944446 --- /dev/null +++ b/src/lib/grid_displayer.rs @@ -0,0 +1,188 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + mem::transmute_copy, +}; + +use termion::color; + +use super::grid::{Grid, Tile}; + +pub struct TileDisplayer { + color_seed: u16, +} + +impl TileDisplayer { + pub fn new(color_seed: u16) -> Self { + Self { color_seed } + } + + const TILE_LENGTH: usize = 7; + const TILE_HEIGHT: usize = 3; + + pub fn display(&self, tile: &Tile) -> String { + match tile.value() { + Some(value) => { + Self::color_representation(Self::display_number(value), value, self.color_seed) + } + None => [ + // empty tile + " ", " ", " ", + ] + .join("\n"), + } + } + + fn display_number(value: usize) -> String { + let result = [ + // number tile + "┌─ ─┐", + &Self::pad_both(value.to_string(), Self::TILE_LENGTH), + "└─ ─┘", + ] + .join("\n"); + result + } + + fn pad_both(text: String, length: usize) -> String { + let mut text = text; + while text.len() < length { + text = format!(" {text} "); + } + if text.len() > length { + (&text)[..length].to_string() + } else { + text + } + } + + fn color_representation(text: String, value: usize, color_seed: u16) -> String { + let color = Self::hashed_color(value, color_seed); + let color_code = color::Bg(color); + let reset_code = color::Bg(color::Reset); + + let text = text + .split("\n") + .map(|line| format!("{color_code}{line}{reset_code}")) + .collect::>() + .join("\n"); + text + } + + fn hashed_color(value: usize, color_seed: u16) -> color::Rgb { + let mut hasher = DefaultHasher::new(); + (value + color_seed as usize).hash(&mut hasher); + let hash = hasher.finish(); + // SAFETY: + // there are no logic that relies on the value of the outputted numbers, thus it is safe to create them without constructors + let [frac_a, frac_b]: [f64; 2] = unsafe { transmute_copy::<_, [u32; 2]>(&hash) } + .into_iter() + .map(|frac| (frac as f64) / (u32::MAX as f64)) + .collect::>() + .try_into() + .unwrap(); + + let mut remaining = 255f64; + let r = Self::take_fraction(&mut remaining, frac_a, 150.) as u8; + let g = Self::take_fraction(&mut remaining, frac_b, 150.) as u8; + let b = remaining as u8; + color::Rgb(r, g, b) + } + + fn take_fraction(remainder: &mut f64, frac: f64, max: f64) -> f64 { + let result = (*remainder * frac).min(max); + *remainder -= result; + result + } +} + +pub struct GridDisplayer { + tile_displayer: TileDisplayer, +} + +impl GridDisplayer { + pub fn new(color_seed: u16) -> Self { + let tile_displayer = TileDisplayer::new(color_seed); + Self { tile_displayer } + } + + /// (0: '┘'), (1: '┐'), (2: '┌'), (3: '└'), (4: '┼'), (5: '─'), (6: '├'), (7: '┤'), (8: '┴'), (9: '┬'), (10: '│') + const DISPLAY_CHAR: [&'static str; 11] = + ["┘", "┐", "┌", "└", "┼", "─", "├", "┤", "┴", "┬", "│"]; + + /// + /// returns a string of multiple lines representing the grid + /// + pub fn display(&self, grid: &Grid) -> String { + let tiles: Vec> = grid + .tiles() + .iter() + .map(|row| { + row.iter() + .map(|tile| self.tile_displayer.display(tile)) + .collect() + }) + .collect(); + let row_representations: Vec<_> = tiles + .iter() + .map(|row_representation| { + let mut row_lines = (0..TileDisplayer::TILE_HEIGHT) + .map(|_| vec![]) + .collect::>(); + // push every item lines in [`row_lines`] + for item_representation in row_representation { + item_representation + .split('\n') + .into_iter() + .zip(row_lines.iter_mut()) + .for_each(|(item_line, row_line)| row_line.push(item_line.to_string())); + } + // join lines of [`row_lines`] + let row_lines = row_lines + .iter_mut() + .map(|line_parts| line_parts.join(Self::DISPLAY_CHAR[10]).to_string()) + .map(|line| [Self::DISPLAY_CHAR[10], &line, Self::DISPLAY_CHAR[10]].join("")) + .collect::>(); + row_lines.join("\n") + }) + .collect(); + + [ + self.first_grid_display_line(grid), + row_representations.join(&self.between_grid_display_line(grid)), + self.last_grid_display_line(grid), + ] + .join("\n") + } + + fn first_grid_display_line(&self, grid: &Grid) -> String { + let middle = (0..grid.size()) + .map(|_| Self::DISPLAY_CHAR[5].repeat(TileDisplayer::TILE_LENGTH)) + .collect::>() + .join(Self::DISPLAY_CHAR[9]); + [Self::DISPLAY_CHAR[2], &middle, Self::DISPLAY_CHAR[1]].join("") + } + + fn between_grid_display_line(&self, grid: &Grid) -> String { + let middle = (0..grid.size()) + .map(|_| Self::DISPLAY_CHAR[5].repeat(TileDisplayer::TILE_LENGTH)) + .collect::>() + .join(Self::DISPLAY_CHAR[4]); + [ + "\n", + Self::DISPLAY_CHAR[6], + &middle, + Self::DISPLAY_CHAR[7], + "\n", + ] + .join("") + } + + fn last_grid_display_line(&self, grid: &Grid) -> String { + let middle = (0..grid.size()) + .map(|_| Self::DISPLAY_CHAR[5].repeat(TileDisplayer::TILE_LENGTH)) + .collect::>() + .join(Self::DISPLAY_CHAR[8]); + [Self::DISPLAY_CHAR[3], &middle, Self::DISPLAY_CHAR[0], "\n"].join("") + } +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs index dccb036..47aef3d 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,6 +1,8 @@ pub mod controller; pub mod game; +pub mod game_manager; pub mod grid; +pub mod grid_displayer; pub fn clear_term() { print!("\x1B[2J\x1B[1;1H"); diff --git a/src/main.rs b/src/main.rs index b4eacbe..d8a37a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,15 @@ use lib::{ - controller::{player::PlayerController, Controller}, - game::{Game, Rules}, + controller::{player::PlayerController, random::RandomController, Controller}, + game::{self, GameError}, + game_manager::{self, GameManager}, }; pub mod lib; -fn main() { - let rules = Rules::default() - .size(4) - .spawn_per_turn(1) - .clear_term(false); +fn main() -> Result<(), GameError> { + let game_rules = game::Rules::default().size(8).spawn_per_turn(1); + let manager_rules = game_manager::Rules::default(); let controller = PlayerController::new().into_box(); - let mut game = Game::new(rules).controlled(controller); - loop { - game.turn().unwrap(); - } + let mut managed = GameManager::new(game_rules, manager_rules, controller); + managed.play_all() }