diff --git a/Cargo.lock b/Cargo.lock index 5381686..fdabd84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,16 +33,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.1.8" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ "atty", "bitflags", "clap_derive", + "clap_lex", "indexmap", - "lazy_static", - "os_str_bytes", + "once_cell", "strsim", "termcolor", "textwrap", @@ -50,9 +50,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.7" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -62,10 +62,19 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.6" +name = "clap_lex" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", @@ -74,9 +83,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -95,31 +104,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.121" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "numtoa" @@ -128,13 +125,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "once_cell" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "ppv-lite86" @@ -168,18 +168,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -216,9 +216,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -249,13 +249,13 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.90" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -286,10 +286,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "version_check" @@ -299,9 +299,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 005ac88..a6e519e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,2 @@ -[package] -name = "rs48" -version = "1.0.0" -edition = "2021" -description = "A game of 2048 that plays in the terminal as a TUI with a lot of configurability." -license = "GPL-3.0" -authors = ["JOLIMAITRE Matthieu "] - -[dependencies] -rand = "0.8.5" -termion = "1.5.6" -clap = { version = "3.1.8", features = ["derive"] } +[workspace] +members = ["rs48"] \ No newline at end of file diff --git a/rs48/Cargo.toml b/rs48/Cargo.toml new file mode 100644 index 0000000..005ac88 --- /dev/null +++ b/rs48/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rs48" +version = "1.0.0" +edition = "2021" +description = "A game of 2048 that plays in the terminal as a TUI with a lot of configurability." +license = "GPL-3.0" +authors = ["JOLIMAITRE Matthieu "] + +[dependencies] +rand = "0.8.5" +termion = "1.5.6" +clap = { version = "3.1.8", features = ["derive"] } diff --git a/src/lib/controller.rs b/rs48/src/lib/controller.rs similarity index 100% rename from src/lib/controller.rs rename to rs48/src/lib/controller.rs diff --git a/src/lib/controller/player.rs b/rs48/src/lib/controller/player.rs similarity index 100% rename from src/lib/controller/player.rs rename to rs48/src/lib/controller/player.rs diff --git a/src/lib/controller/random.rs b/rs48/src/lib/controller/random.rs similarity index 100% rename from src/lib/controller/random.rs rename to rs48/src/lib/controller/random.rs diff --git a/src/lib/controller/simulated.rs b/rs48/src/lib/controller/simulated.rs similarity index 100% rename from src/lib/controller/simulated.rs rename to rs48/src/lib/controller/simulated.rs diff --git a/src/lib/game.rs b/rs48/src/lib/game.rs similarity index 100% rename from src/lib/game.rs rename to rs48/src/lib/game.rs diff --git a/src/lib/game_manager.rs b/rs48/src/lib/game_manager.rs similarity index 100% rename from src/lib/game_manager.rs rename to rs48/src/lib/game_manager.rs diff --git a/src/lib/grid.rs b/rs48/src/lib/grid.rs similarity index 100% rename from src/lib/grid.rs rename to rs48/src/lib/grid.rs diff --git a/src/lib/grid_displayer.rs b/rs48/src/lib/grid_displayer.rs similarity index 100% rename from src/lib/grid_displayer.rs rename to rs48/src/lib/grid_displayer.rs diff --git a/src/lib/mod.rs b/rs48/src/lib/mod.rs similarity index 100% rename from src/lib/mod.rs rename to rs48/src/lib/mod.rs diff --git a/src/main.rs b/rs48/src/main.rs similarity index 100% rename from src/main.rs rename to rs48/src/main.rs diff --git a/rs48_lib/Cargo.toml b/rs48_lib/Cargo.toml new file mode 100644 index 0000000..4b479ed --- /dev/null +++ b/rs48_lib/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rs48_lib" +version = "1.0.0" +edition = "2021" +description = "components of rs48" +license = "MIT" +authors = ["JOLIMAITRE Matthieu "] + +[dependencies] +rand = "0.8" +termion = "1.5" diff --git a/rs48_lib/src/controller.rs b/rs48_lib/src/controller.rs new file mode 100644 index 0000000..f2c3698 --- /dev/null +++ b/rs48_lib/src/controller.rs @@ -0,0 +1,58 @@ +use rand::{distributions::Standard, prelude::Distribution}; + +use super::grid::Grid; +use std::{error::Error, fmt::Display}; + +#[derive(Debug)] +pub enum Move { + LEFT, + RIGHT, + UP, + DOWN, +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Move { + match rng.gen_range(0..4) { + 0 => Move::DOWN, + 1 => Move::LEFT, + 2 => Move::RIGHT, + _ => Move::UP, + } + } +} + +#[derive(Debug)] +pub enum ControllerError { + ExitSignal, +} + +impl Display for ControllerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + ControllerError::ExitSignal => "received exit signal", + }; + f.write_str(msg) + } +} + +impl Error for ControllerError {} + +pub trait Controller { + fn next_move(&mut self, grid: &Grid) -> Result; + + fn into_box(self) -> Box + where + Self: Sized + 'static, + { + Box::new(self) + } +} + +pub mod player; +pub mod random; +pub mod simulated; + +pub use player::PlayerController; +pub use random::RandomController; +pub use simulated::SimulatedController; diff --git a/rs48_lib/src/controller/player.rs b/rs48_lib/src/controller/player.rs new file mode 100644 index 0000000..7f3ad2a --- /dev/null +++ b/rs48_lib/src/controller/player.rs @@ -0,0 +1,32 @@ +use std::io::{stdin, stdout}; +use termion::{event::Key, input::TermRead, raw::IntoRawMode}; + +use super::{Controller, ControllerError, Move}; +use crate::lib::grid::Grid; + +pub struct PlayerController; + +impl PlayerController { + pub fn new() -> Self { + Self + } +} + +impl Controller for PlayerController { + fn next_move(&mut self, _grid: &Grid) -> Result { + let stdin = stdin(); + let mut _stdout = stdout().into_raw_mode().unwrap(); + for c in stdin.keys() { + let movement = match c.unwrap() { + Key::Char('q') => return Err(ControllerError::ExitSignal), + Key::Left => Move::LEFT, + Key::Right => Move::RIGHT, + Key::Up => Move::UP, + Key::Down => Move::DOWN, + _ => continue, + }; + return Ok(movement); + } + unreachable!() + } +} diff --git a/rs48_lib/src/controller/random.rs b/rs48_lib/src/controller/random.rs new file mode 100644 index 0000000..3446d9f --- /dev/null +++ b/rs48_lib/src/controller/random.rs @@ -0,0 +1,19 @@ +use rand::random; + +use super::{Controller, ControllerError, Move}; +use crate::lib::grid::Grid; + +pub struct RandomController; + +impl RandomController { + pub fn new() -> Self { + Self + } +} + +impl Controller for RandomController { + fn next_move(&mut self, _grid: &Grid) -> Result { + let movement = random(); + Ok(movement) + } +} diff --git a/rs48_lib/src/controller/simulated.rs b/rs48_lib/src/controller/simulated.rs new file mode 100644 index 0000000..7405aea --- /dev/null +++ b/rs48_lib/src/controller/simulated.rs @@ -0,0 +1,34 @@ +use crate::lib::grid::Grid; + +use super::{Controller, ControllerError, Move}; + +pub enum Objective { + Score, + TileCount, +} + +pub struct SimulatedController { + _simulations_per_move: usize, + _length_of_simulation: usize, + _objective: Objective, +} + +impl SimulatedController { + pub fn new( + _simulations_per_move: usize, + _length_of_simulation: usize, + _objective: Objective, + ) -> Self { + Self { + _simulations_per_move, + _length_of_simulation, + _objective, + } + } +} + +impl Controller for SimulatedController { + fn next_move(&mut self, _grid: &Grid) -> Result { + todo!() + } +} diff --git a/rs48_lib/src/game.rs b/rs48_lib/src/game.rs new file mode 100644 index 0000000..e417364 --- /dev/null +++ b/rs48_lib/src/game.rs @@ -0,0 +1,251 @@ +use std::{error::Error, fmt::Display}; + +use super::{ + controller::{ControllerError, Move}, + grid::Grid, +}; + +pub struct Rules { + size: usize, + spawn_per_turn: usize, +} + +impl Rules { + pub fn size(mut self, size: usize) -> Self { + self.size = size; + self + } + + pub fn spawn_per_turn(mut self, spawn_per_turn: usize) -> Self { + self.spawn_per_turn = spawn_per_turn; + self + } +} + +impl Default for Rules { + fn default() -> Self { + Self { + size: 4, + spawn_per_turn: 1, + } + } +} + +#[derive(Debug)] +pub enum GameError { + GridIsFull, + ControllerError(ControllerError), +} + +impl From for GameError { + fn from(error: ControllerError) -> Self { + Self::ControllerError(error) + } +} + +impl Display for GameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GridIsFull => f.write_str("grid is full"), + GameError::ControllerError(err) => err.fmt(f), + } + } +} + +impl Error for GameError {} + +#[derive(Clone)] +pub struct Game { + board: Grid, + score: usize, + turn_index: usize, + spawn_per_turn: usize, +} + +impl Game { + pub fn new(rules: Rules) -> Self { + let Rules { + size, + spawn_per_turn, + } = rules; + + Self { + board: Grid::new(size), + score: 0, + turn_index: 0, + spawn_per_turn, + } + } + + pub fn get_board(&self) -> &Grid { + &self.board + } + + pub fn get_score(&self) -> usize { + self.score + } + + pub fn get_turn_index(&self) -> usize { + self.turn_index + } + + pub fn turn(&mut self, movement: Move) -> Result<(), GameError> { + let move_score = self.perform_move(movement); + self.score += move_score; + for _ in 0..self.spawn_per_turn { + self.spawn_random()?; + } + self.turn_index += 1; + Ok(()) + } + + 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() { + if self.board.get((x, y)).unwrap().is_empty() { + potentials.push((x, y)); + } + } + } + let potential_count = potentials.len() as f32; + if potential_count == 0. { + return Err(GameError::GridIsFull.into()); + } + let random = rand::random::() * potential_count; + let index = random.floor() as usize; + let (x, y) = potentials[index]; + self.board.set((x, y), Some(1)); + Ok(()) + } + + // TODO: macro peut être ? + pub fn perform_move(&mut self, movement: Move) -> usize { + let mut move_score = 0; + match movement { + Move::LEFT => { + for y in 0..self.board.size() { + for x in 0..self.board.size() { + move_score += self.perform_linear_move((-1, 0), (x, y)); + } + } + } + Move::RIGHT => { + for y in 0..self.board.size() { + for x in (0..self.board.size()).rev() { + move_score += self.perform_linear_move((1, 0), (x, y)); + } + } + } + Move::UP => { + for x in 0..self.board.size() { + for y in 0..self.board.size() { + move_score += self.perform_linear_move((0, -1), (x, y)); + } + } + } + Move::DOWN => { + for x in 0..self.board.size() { + for y in (0..self.board.size()).rev() { + move_score += self.perform_linear_move((0, 1), (x, y)); + } + } + } + }; + move_score + } + + fn perform_linear_move( + &mut self, + direction: (isize, isize), + tile_pos: (usize, usize), + ) -> usize { + if self.board.get(tile_pos.clone()).unwrap().is_empty() { + 0 + } else { + let mut displacement = Displacement::new(&mut self.board, tile_pos, direction); + displacement.move_all(); + displacement.pop_score() + } + } +} + +pub struct Displacement<'g> { + grid: &'g mut Grid, + position: (usize, usize), + direction: (isize, isize), + score: usize, +} + +impl<'g> Displacement<'g> { + pub fn new(grid: &'g mut Grid, position: (usize, usize), direction: (isize, isize)) -> Self { + Self { + grid, + position, + direction, + score: 0, + } + } + + pub fn pop_score(self) -> usize { + let Displacement { score, .. } = self; + score + } + + pub fn move_all(&mut self) { + loop { + let can_continue = self.move_once(); + if !can_continue { + break; + } + } + } + + fn move_once(&mut self) -> bool { + let current_pos = self.position.clone(); + let current_value = self.grid.get_val(current_pos).unwrap(); + if let Some(next_pos) = self.get_next_pos() { + match self.grid.get_val(next_pos) { + None => { + self.grid.move_tile(current_pos, next_pos); + self.set_pos(next_pos); + true + } + Some(value) if value == current_value => { + self.grid.move_tile(current_pos, next_pos); + self.grid.set(next_pos, Some(value * 2)); + self.score = value * 2; + false + } + Some(_) => false, + } + } else { + false + } + } + + fn get_next_pos(&self) -> Option<(usize, usize)> { + let (current_x, current_y) = self.position.clone(); + let (dx, dy) = self.direction.clone(); + if would_overflow(current_x, dx, self.grid.size() - 1) + || would_overflow(current_y, dy, self.grid.size() - 1) + { + None + } else { + let next_x = (current_x as isize) + dx; + let next_y = (current_y as isize) + dy; + Some((next_x as usize, next_y as usize)) + } + } + + fn set_pos(&mut self, (x, y): (usize, usize)) { + self.position = (x, y); + } +} + +/// determine if the given number, added a delta that is either 1 or -1 to it, would overflow a certain maximum value for n +fn would_overflow(number: usize, delta: isize, max: usize) -> bool { + let too_little = number == 0 && delta == -1; + let too_big = number == max && delta == 1; + too_little || too_big +} diff --git a/rs48_lib/src/game_manager.rs b/rs48_lib/src/game_manager.rs new file mode 100644 index 0000000..e9490dd --- /dev/null +++ b/rs48_lib/src/game_manager.rs @@ -0,0 +1,148 @@ +use std::{thread, time::Duration}; + +use crate::lib::{ + controller::Controller, + game::{self, Game, GameError}, +}; + +use super::{clear_term, grid_displayer::GridDisplayer}; + +pub struct Rules { + display: bool, + display_skips: usize, + clear_term: bool, + color_seed: u16, + turn_duration: Duration, +} + +impl Rules { + /// wether to display the game at all or not + pub fn display(mut self, display: bool) -> Self { + self.display = display; + self + } + + /// turns to skip the display of + pub fn display_skips(mut self, display_skips: usize) -> Self { + self.display_skips = display_skips; + self + } + + /// wether to clear the terminal or not between displays + pub fn clear_term(mut self, clear_term: bool) -> Self { + self.clear_term = clear_term; + self + } + + /// seed for the procedural coloration of tiles + pub fn color_seed(mut self, color_seed: u16) -> Self { + self.color_seed = color_seed; + self + } + + /// duration of pauses between turns + 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> { + self.display_conditionnally(); + self.game_turn()?; + thread::sleep(self.turn_duration); + Ok(()) + } + + fn display_conditionnally(&mut self) { + if self.display { + if self.display_to_skip == 0 { + if self.clear_term { + clear_term(); + } + self.print_display(); + self.display_to_skip = self.display_skips; + } else { + self.display_to_skip -= 1; + } + } + } + + fn game_turn(&mut self) -> Result<(), GameError> { + let board = self.game.get_board(); + let movement = self.controller.next_move(board)?; + self.game.turn(movement)?; + Ok(()) + } + + pub fn print_display(&self) { + let headline_display = self.get_headline_display(); + println!("{headline_display}"); + let grid = self.game.get_board(); + let grid_display = self.grid_displayer.display(grid); + println!("{grid_display}"); + } + + fn get_headline_display(&self) -> String { + let score = self.game.get_score(); + let turn = self.game.get_turn_index(); + let biggest_tile = self.game.get_board().biggest_value(); + format!("score: {score:>12} | biggest tile: {biggest_tile:>12} | turn: {turn:>12}") + } + + pub fn play_all(&mut self) -> Result<(), GameError> { + loop { + self.turn()?; + } + } +} diff --git a/rs48_lib/src/grid.rs b/rs48_lib/src/grid.rs new file mode 100644 index 0000000..53263bc --- /dev/null +++ b/rs48_lib/src/grid.rs @@ -0,0 +1,113 @@ +#[derive(Clone, Copy)] +pub struct Tile { + value: Option, +} + +impl Tile { + pub fn new_with_value(value: usize) -> Self { + Self { value: Some(value) } + } + pub fn new_empty() -> Self { + Self { value: None } + } + + pub fn value(&self) -> Option { + self.value.clone() + } + + pub fn is_empty(&self) -> bool { + if let Some(_) = self.value { + false + } else { + true + } + } +} + +#[derive(Clone)] +pub struct Grid { + size: usize, + tiles: Vec>, +} + +impl Grid { + /// + /// constructor + /// + pub fn new(size: usize) -> Self { + let tiles = (0..size) + .map(|_| (0..size).map(|_| Tile::new_empty()).collect()) + .collect(); + Self { size, tiles } + } + + /// + /// set the value of the tile at the selected position + /// + pub fn set(&mut self, (x, y): (usize, usize), value: Option) { + self.tiles[y][x] = if let Some(value) = value { + Tile::new_with_value(value) + } else { + Tile::new_empty() + }; + } + + /// + /// get a tile if the position is in the grid + /// + pub fn get(&self, (x, y): (usize, usize)) -> Option<&Tile> { + match self.tiles.get(y).map(|row| row.get(x)) { + Some(Some(tile)) => Some(tile), + _ => None, + } + } + + /// + /// get the value of a tile if the position is in the grid and the tile has a value + /// + pub fn get_val(&self, (x, y): (usize, usize)) -> Option { + match self.get((x, y)).map(|tile| tile.value()) { + Some(Some(value)) => Some(value), + _ => None, + } + } + + /// + /// get the size of the grid + /// + pub fn size(&self) -> usize { + 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 + /// + pub fn move_tile(&mut self, (src_x, src_y): (usize, usize), (dst_x, dst_y): (usize, usize)) { + let src = self.tiles[src_y][src_x].clone(); + self.tiles[dst_y][dst_x] = src; + self.tiles[src_y][src_x] = Tile::new_empty(); + } + + /// + /// get the biggest value of the board + /// + pub fn biggest_value(&self) -> usize { + self.tiles() + .iter() + .map(|row| { + row.iter() + .filter_map(|tile| tile.value()) + .reduce(|a, b| a.max(b)) + }) + .filter_map(|value| value) + .reduce(|a, b| a.max(b)) + .unwrap_or(0) + } +} diff --git a/rs48_lib/src/grid_displayer.rs b/rs48_lib/src/grid_displayer.rs new file mode 100644 index 0000000..c944446 --- /dev/null +++ b/rs48_lib/src/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/rs48_lib/src/lib.rs b/rs48_lib/src/lib.rs new file mode 100644 index 0000000..0cd9c0d --- /dev/null +++ b/rs48_lib/src/lib.rs @@ -0,0 +1,19 @@ +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"); +} + +pub mod prelude { + pub use super::controller::{ + Controller, PlayerController, RandomController, SimulatedController, + }; + pub use super::game::GameError; + pub use super::game::Rules as GameRules; + pub use super::game_manager::GameManager; + pub use super::game_manager::Rules as ManagerRules; +}