From 0d492adb0ad9949fba2ee6770770dbc642654db8 Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Mon, 4 Apr 2022 14:23:20 +0300 Subject: [PATCH] initial release --- .gitignore | 1 + Cargo.lock | 118 ++++++++++++++++++++++ Cargo.toml | 10 ++ src/lib/controller.rs | 64 ++++++++++++ src/lib/game.rs | 221 ++++++++++++++++++++++++++++++++++++++++++ src/lib/grid.rs | 195 +++++++++++++++++++++++++++++++++++++ src/lib/mod.rs | 7 ++ src/main.rs | 11 +++ 8 files changed, 627 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib/controller.rs create mode 100644 src/lib/game.rs create mode 100644 src/lib/grid.rs create mode 100644 src/lib/mod.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..681f9a8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,118 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "rs48" +version = "0.1.0" +dependencies = [ + "rand", + "termion", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..41e8fa6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rs48" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.5" +termion = "1.5.6" \ No newline at end of file diff --git a/src/lib/controller.rs b/src/lib/controller.rs new file mode 100644 index 0000000..3f6eb15 --- /dev/null +++ b/src/lib/controller.rs @@ -0,0 +1,64 @@ +use termion::{event::Key, input::TermRead, raw::IntoRawMode}; + +use super::grid::Grid; +use std::{ + error::Error, + fmt::Display, + io::{stdin, stdout}, +}; + +pub enum Move { + LEFT, + RIGHT, + UP, + DOWN, +} + +#[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; +} + +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/src/lib/game.rs b/src/lib/game.rs new file mode 100644 index 0000000..d093aa8 --- /dev/null +++ b/src/lib/game.rs @@ -0,0 +1,221 @@ +use std::{error::Error, fmt::Display}; + +use super::{ + controller::{Controller, Move, PlayerController}, + grid::Grid, +}; + +pub struct Rules { + size: usize, + spawn_per_turn: usize, + controller: Box, +} + +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, + controller: Box::new(PlayerController::new()), + } + } +} + +#[derive(Debug)] +pub enum Err2048 { + GridIsFull, +} + +impl Display for Err2048 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + &Self::GridIsFull => "grid is full", + }; + f.write_str(msg) + } +} + +impl Error for Err2048 {} + +pub struct Game { + board: Grid, + controller: Box, + spawn_per_turn: usize, +} + +impl Game { + pub fn new(rules: Rules) -> Self { + let Rules { + controller, + size, + spawn_per_turn, + } = rules; + + Self { + board: Grid::new(size), + controller, + spawn_per_turn, + } + } + + pub fn turn(&mut self) -> Result<(), Box> { + for _ in 0..self.spawn_per_turn { + self.spawn_random()?; + } + self.refresh_display(); + let movement = self.controller.next_move(&self.board)?; + self.perform_move(movement); + Ok(()) + } + + pub fn spawn_random(&mut self) -> Result<(), Box> { + 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(Err2048::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(()) + } + + pub fn refresh_display(&self) { + super::clear_term(); + let text = self.board.display(); + println!("{text}"); + } + + // TODO: macro peut être ? + pub fn perform_move(&mut self, movement: Move) { + match movement { + Move::LEFT => { + for y in 0..self.board.size() { + for x in 0..self.board.size() { + if !self.board.get((x, y)).unwrap().is_empty() { + 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() { + if !self.board.get((x, y)).unwrap().is_empty() { + self.perform_linear_move((1, 0), (x, y)); + } + } + } + } + Move::UP => { + for x in 0..self.board.size() { + for y in 0..self.board.size() { + if !self.board.get((x, y)).unwrap().is_empty() { + 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() { + if !self.board.get((x, y)).unwrap().is_empty() { + self.perform_linear_move((0, 1), (x, y)); + } + } + } + } + }; + } + + fn perform_linear_move(&mut self, direction: (isize, isize), tile_pos: (usize, usize)) { + let mut displacement = Displacement::new(&mut self.board, tile_pos, direction); + displacement.move_all(); + } +} + +pub struct Displacement<'g> { + grid: &'g mut Grid, + position: (usize, usize), + direction: (isize, isize), +} + +impl<'g> Displacement<'g> { + pub fn new(grid: &'g mut Grid, position: (usize, usize), direction: (isize, isize)) -> Self { + Self { + grid, + position, + direction, + } + } + + pub fn move_all(&mut self) { + while self.move_once() {} + } + + 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)); + 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); + } +} + +fn would_overflow(n: usize, d: isize, max: usize) -> bool { + let too_little = n == 0 && d == -1; + let too_big = n == max && d == 1; + too_little || too_big +} diff --git a/src/lib/grid.rs b/src/lib/grid.rs new file mode 100644 index 0000000..8fa6e5d --- /dev/null +++ b/src/lib/grid.rs @@ -0,0 +1,195 @@ +/// 0: '┘' +/// +/// 1: '┐' +/// +/// 2: '┌' +/// +/// 3: '└' +/// +/// 4: '┼' +/// +/// 5: '─' +/// +/// 6: '├' +/// +/// 7: '┤' +/// +/// 8: '┴' +/// +/// 9: '┬' +/// +/// 10: '│' +const DISPLAY_CHAR: [&'static str; 11] = ["┘", "┐", "┌", "└", "┼", "─", "├", "┤", "┴", "┬", "│"]; + +#[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 + } + } + + const TILE_LENGTH: usize = 7; + const TILE_HEIGHT: usize = 3; + + pub fn display(&self) -> String { + match self.value { + Some(value) => Self::display_number(value), + None => [ + // empty tile + " ", " ", " ", + ] + .join("\n"), + } + } + + pub 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 + } + } +} + +#[derive(Clone)] +pub struct Grid { + size: usize, + tiles: Vec>, +} + +impl Grid { + pub fn new(size: usize) -> Self { + let tiles = (0..size) + .map(|_| (0..size).map(|_| Tile::new_empty()).collect()) + .collect(); + Self { size, tiles } + } + + 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() + }; + } + + 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, + } + } + + 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, + } + } + + pub fn get_mut(&mut self, x: usize, y: usize) -> &Tile { + &mut self.tiles[y][x] + } + + pub fn size(&self) -> usize { + self.size + } + + 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(); + } + + pub fn display(&self) -> String { + let tiles: Vec> = self + .tiles + .iter() + .map(|row| row.iter().map(|tile| tile.display()).collect()) + .collect(); + let row_displays: Vec<_> = tiles + .iter() + .map(|row| { + let mut row_lines = (0..Tile::TILE_HEIGHT).map(|_| vec![]).collect::>(); + // push every item lines in [`row_lines`] + for item in row { + item.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(DISPLAY_CHAR[10]).to_string()) + .map(|line| [DISPLAY_CHAR[10], &line, DISPLAY_CHAR[10]].join("")) + .collect::>(); + row_lines.join("\n") + }) + .collect(); + + [ + self.first_grid_display_line(), + row_displays.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(|_| DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) + .collect::>() + .join(DISPLAY_CHAR[9]); + [DISPLAY_CHAR[2], &middle, DISPLAY_CHAR[1]].join("") + } + + fn between_grid_display_line(&self) -> String { + let middle = (0..self.size) + .map(|_| DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) + .collect::>() + .join(DISPLAY_CHAR[4]); + ["\n", DISPLAY_CHAR[6], &middle, DISPLAY_CHAR[7], "\n"].join("") + } + + fn last_grid_display_line(&self) -> String { + let middle = (0..self.size) + .map(|_| DISPLAY_CHAR[5].repeat(Tile::TILE_LENGTH)) + .collect::>() + .join(DISPLAY_CHAR[8]); + [DISPLAY_CHAR[3], &middle, DISPLAY_CHAR[0], "\n"].join("") + } +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs new file mode 100644 index 0000000..4efef85 --- /dev/null +++ b/src/lib/mod.rs @@ -0,0 +1,7 @@ +pub mod controller; +pub mod game; +pub mod grid; + +pub fn clear_term() { + print!("\x1B[2J\x1B[1;1H"); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4b56d38 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,11 @@ +use lib::game::{Game, Rules}; + +pub mod lib; + +fn main() { + let rules = Rules::default().size(4).spawn_per_turn(1); + let mut game = Game::new(rules); + loop { + game.turn().unwrap(); + } +}