RosettaCodeData/Task/15-puzzle-game/Rust/15-puzzle-game.rust
2023-07-01 13:44:08 -04:00

226 lines
5.7 KiB
Text

extern crate rand;
use clearscreen::clear;
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Write};
use rand::seq::SliceRandom;
use rand::Rng;
#[derive(Copy, Clone, PartialEq, Debug)]
enum Cell {
Card(usize),
Empty,
}
#[derive(Eq, PartialEq, Hash, Debug)]
enum Direction {
Up,
Down,
Left,
Right,
}
enum Action {
Move(Direction),
Quit,
}
type Board = [Cell; 16];
const EMPTY: Board = [Cell::Empty; 16];
struct P15 {
board: Board,
}
impl P15 {
fn new() -> Self {
let mut board = EMPTY;
for (i, cell) in board.iter_mut().enumerate().skip(1) {
*cell = Cell::Card(i);
}
let mut rng = rand::thread_rng();
board.shuffle(&mut rng);
if !Self::is_valid(board) {
// random swap
let i = rng.gen_range(0..16);
let mut j = rng.gen_range(0..16);
while j == i {
j = rng.gen_range(0..16);
}
board.swap(i, j);
}
Self { board }
}
fn is_valid(mut board: Board) -> bool {
// TODO: optimize
let mut permutations = 0;
let pos = board.iter().position(|&cell| cell == Cell::Empty).unwrap();
if pos != 15 {
board.swap(pos, 15);
permutations += 1;
}
for i in 1..16 {
let pos = board
.iter()
.position(|&cell| matches!(cell, Cell::Card(value) if value == i))
.unwrap();
if pos + 1 != i {
board.swap(pos, i - 1);
permutations += 1;
}
}
permutations % 2 == 0
}
fn get_empty_position(&self) -> usize {
self.board.iter().position(|&c| c == Cell::Empty).unwrap()
}
fn get_moves(&self) -> HashMap<Direction, Cell> {
let mut moves = HashMap::new();
let i = self.get_empty_position();
if i > 3 {
moves.insert(Direction::Up, self.board[i - 4]);
}
if i % 4 != 0 {
moves.insert(Direction::Left, self.board[i - 1]);
}
if i < 12 {
moves.insert(Direction::Down, self.board[i + 4]);
}
if i % 4 != 3 {
moves.insert(Direction::Right, self.board[i + 1]);
}
moves
}
fn play(&mut self, direction: &Direction) {
let i = self.get_empty_position();
// This is safe because `ask_action` only returns legal moves
match *direction {
Direction::Up => self.board.swap(i, i - 4),
Direction::Left => self.board.swap(i, i - 1),
Direction::Right => self.board.swap(i, i + 1),
Direction::Down => self.board.swap(i, i + 4),
};
}
fn is_complete(&self) -> bool {
self.board.iter().enumerate().all(|(i, &cell)| match cell {
Cell::Card(value) => value == i + 1,
Cell::Empty => i == 15,
})
}
}
impl fmt::Display for P15 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(writeln!(f, "+----+----+----+----+"))?;
for (i, &cell) in self.board.iter().enumerate() {
match cell {
Cell::Card(value) => (write!(f, "| {value:2} "))?,
Cell::Empty => (write!(f, "| "))?,
}
if i % 4 == 3 {
(writeln!(f, "|"))?;
(writeln!(f, "+----+----+----+----+"))?;
}
}
Ok(())
}
}
fn main() {
let mut p15 = P15::new();
for turns in 1.. {
println!("{p15}");
match ask_action(&p15.get_moves(), true) {
Action::Move(direction) => {
p15.play(&direction);
clear().expect("failed to clear screen");
}
Action::Quit => {
clear().expect("failed to clear screen");
println!("Bye!");
break;
}
}
if p15.is_complete() {
println!("Well done! You won in {turns} turns");
break;
}
}
}
fn ask_action(moves: &HashMap<Direction, Cell>, render_list: bool) -> Action {
use Action::*;
use Direction::*;
if render_list {
println!("Possible moves:");
if let Some(&Cell::Card(value)) = moves.get(&Up) {
println!("\tU) {value}");
}
if let Some(&Cell::Card(value)) = moves.get(&Left) {
println!("\tL) {value}");
}
if let Some(&Cell::Card(value)) = moves.get(&Right) {
println!("\tR) {value}");
}
if let Some(&Cell::Card(value)) = moves.get(&Down) {
println!("\tD) {value}");
}
println!("\tQ) Quit");
print!("Choose your move: ");
io::stdout().flush().unwrap();
} else {
print!("Unknown move, try again: ");
io::stdout().flush().unwrap();
}
let mut action = String::new();
io::stdin().read_line(&mut action).expect("read error");
match action.to_uppercase().trim() {
"U" if moves.contains_key(&Up) => Move(Up),
"L" if moves.contains_key(&Left) => Move(Left),
"R" if moves.contains_key(&Right) => Move(Right),
"D" if moves.contains_key(&Down) => Move(Down),
"Q" => Quit,
_ => {
if unknown_action() {
ask_action(moves, true)
} else {
ask_action(moves, false)
}
}
}
}
fn unknown_action() -> bool {
use crossterm::{
cursor::MoveToPreviousLine,
terminal::{Clear, ClearType},
ExecutableCommand,
};
std::io::stdout().execute(MoveToPreviousLine(1)).unwrap();
std::io::stdout()
.execute(Clear(ClearType::CurrentLine))
.unwrap();
false
}