initial release

This commit is contained in:
JOLIMAITRE Matthieu 2022-04-04 14:23:20 +03:00
commit 0d492adb0a
8 changed files with 627 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

118
Cargo.lock generated Normal file
View file

@ -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"

10
Cargo.toml Normal file
View file

@ -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"

64
src/lib/controller.rs Normal file
View file

@ -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<Move, ControllerError>;
}
pub struct PlayerController {
//
}
impl PlayerController {
pub fn new() -> Self {
Self {}
}
}
impl Controller for PlayerController {
fn next_move(&mut self, _grid: &Grid) -> Result<Move, ControllerError> {
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!()
}
}

221
src/lib/game.rs Normal file
View file

@ -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<dyn Controller>,
}
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<dyn Controller>,
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<dyn Error>> {
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<dyn Error>> {
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::<f32>() * 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
}

195
src/lib/grid.rs Normal file
View file

@ -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<usize>,
}
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<usize> {
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<Vec<Tile>>,
}
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<usize>) {
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<usize> {
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<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::<Vec<_>>();
// 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::<Vec<_>>();
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.join(DISPLAY_CHAR[8]);
[DISPLAY_CHAR[3], &middle, DISPLAY_CHAR[0], "\n"].join("")
}
}

7
src/lib/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod controller;
pub mod game;
pub mod grid;
pub fn clear_term() {
print!("\x1B[2J\x1B[1;1H");
}

11
src/main.rs Normal file
View file

@ -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();
}
}