Skip to content

Commit

Permalink
v1.0.0: Keyboard shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
hiimsergey committed Sep 30, 2024
0 parents commit 51c0d90
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
19 changes: 19 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "mastermind-rs"
version = "1.0.0"
authors = ["Sergey Lavrent"]
edition = "2021"
description = "A TUI game based on Mastermind/Bulls & Cows"
repository = "https://github.com/hiimsergey/mastermind-rs"
license = "GPL-3.0"
keywords = ["tui", "game", "boardgame", "board-game"]
categories = ["command-line-utilities", "games"]

[dependencies]
fastrand = "2.0.2"

[dependencies.cursive]
version = "0.20.0"
default-features = false
# this one is best compatible with Unix and Windows, in my experience
features = ["crossterm-backend"]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# mastermind-rs
A TUI game of "Mastermind/Master Mind" or "Bulls & Cows" written in Rust.

Utilizes the [cursive](https://github.com/gyscos/cursive) crate for building the User Interface.

![screenshot](https://github.com/hiimsergey/mastermind-rs/assets/91432388/3e8f193b-f958-400f-a9a8-3c0e9466b1b7)
88 changes: 88 additions & 0 deletions src/logic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use cursive::Cursive;
use cursive::views::{EditView, ListView, ScrollView, TextView};
use fastrand;
use crate::{util::CodeError, windows};

/// Generates the code the player should guess as a `String`
pub fn get_code(code_length: u8, digit_number: u8) -> String {
let mut result = String::new();
for _ in 0..code_length {
// Pushes a random digit within the specified bounds
result = format!("{result}{}", fastrand::u8(1..=digit_number));
}
result
}

/// Function that runs right after the player submits their guess.
/// Depending on the validity of the guess, warns about the error or pushes the
/// guess to the list.
pub fn submit_guess(s: &mut Cursive, guess: &str, code: &str, digit_number: u8) {
if let Err(err) = get_code_validitiy(guess, code, digit_number) {
windows::guess_error(s, err);
// Empties the input bar
s.call_on_name("input", |v: &mut EditView| { v.set_content(""); });
} else {
s.call_on_name("list", |v: &mut ScrollView<ListView>| {
// Calculates and formats the number of the guess in the list
let guess_number = format!("{}.", v.get_inner().len() + 1);
v.get_inner_mut().add_child("", TextView::new(
format!(
"{:<5}{guess}{:>len_fmt$}",
guess_number,
compare_guess(guess, code),
len_fmt = guess.len() + 4
)
));
});

if guess == code {
s.call_on_name("input", |v: &mut EditView| v.disable());
windows::win(s, code);
}
}
}

/// Checks whether the guess is valid in terms of length and characters.
/// Returns `Ok(())` if valid and the respective `CodeError` member otherwise.
fn get_code_validitiy(guess: &str, code: &str, digit_number: u8) -> Result<(), CodeError> {
if guess.len() < code.len() { return Err(CodeError::Short); }
if guess.parse::<u32>().is_err() { return Err(CodeError::NaN); }

for c in guess.chars() {
if !(1..=digit_number).contains(&(c.to_digit(10).unwrap() as u8)) {
return Err(CodeError::DigitLimit);
}
}

Ok(())
}

/// Generates the feedback consisting of `!`, `?` and `.` as a `String`
fn compare_guess(guess: &str, code: &str) -> String {
// Shadow with a vector because it's more suited for further computation
let mut guess: Vec<char> = guess.chars().collect();
let mut result = String::new();

for i in 0..code.len() {
if code.chars().nth(i).unwrap() == guess[i] {
guess[i] = ' ';
// Prepends `!` to `result`
result = format!("!{result}");
continue;
}
// Checks if there's a matching character on another index, assuming it
// doesn't belong to a "bull-pair" (`!`)
for k in 0..code.len() {
if code.chars().nth(i).unwrap() == guess[k]
&& code.chars().nth(k).unwrap() != guess[k] {
guess[k] = ' ';
result += "?";
break;
}
}
}

// Repeatedly fills `result` with `.` until it's as long as the code
result += &".".repeat(code.len() - result.len());
result
}
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod logic; mod util; mod windows;

fn main() {
let mut siv = cursive::default();
windows::menu(&mut siv);
siv.run();
}
119 changes: 119 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use cursive::theme::{BaseColor, Color};
use cursive::view::Nameable;
use cursive::views::{DummyView, LinearLayout, SliderView, TextView};

/// Signals the reason why the guess provided by the player is invalid
pub enum CodeError {
/// The guess contains digits outside of the specified `digit_number` setting
DigitLimit,
/// The guess contains characters other than letters
NaN,
/// The guess is shorter than the specified `code_length` setting
Short
}

pub fn text_banner() -> TextView {
TextView::new(
" _ _ _
_ __ ___ __ _ ___| |_ ___ _ __ _ __ ___ (_)_ __ __| |
| '_ ` _ \\ / _` / __| __/ _ \\ '__| '_ ` _ \\| | '_ \\ / _` |
| | | | | | (_| \\__ \\ || __/ | | | | | | | | | | | (_| |
|_| |_| |_|\\__,_|___/\\__\\___|_| |_| |_| |_|_|_| |_|\\__,_|
"
)
}

pub fn text_about() -> TextView {
TextView::new(
"A little game written for the sake of experience in writing
Rust code. Also my first project using a User Interface
library. Rewritten from scratch at v1.0.0.
<https://github.com/hiimsergey/mastermind-rs>
Utilizes the \"cursive\" crate for building TUIs.
<https://crates.io/crates/cursive>
v1.0.0 GPL-3.0 Licence"
).style(Color::Dark(BaseColor::Blue))
}

pub fn text_rules_1() -> TextView {
TextView::new(
"
Use the arrow keys or the mouse to navigate.
Press q to close windows and Esc to quit the game.
"
)
}

pub fn text_rules_2() -> TextView {
TextView::new(
"A random code is generated based on your settings:
"
).style(Color::Dark(BaseColor::Blue))
}

pub fn text_rules_3() -> TextView {
TextView::new(
"1. \"Digit number\" sets the amount of different
characters to feature.
2. \"Code length\" sets the length of the generated
code.
"
).style(Color::Dark(BaseColor::Magenta))
}

pub fn text_rules_4() -> TextView {
TextView::new(
"You try to guess it by filling in the input box.
The game gives you feedback:
"
).style(Color::Dark(BaseColor::Blue))
}

pub fn text_rules_5() -> TextView {
TextView::new(
"1. An exclamation mark means that one character in
your guess is right.
2. A question mark means that one character is
featured in the code but on another position.
3. A dot means that a character isn't featured at
all."
).style(Color::Dark(BaseColor::Magenta))
}

pub fn text_settings_ingame(digit_number: u8, code_length: u8) -> TextView {
TextView::new(
format!(
"Digit number: {digit_number}
Code length: {code_length}"
)
).style(Color::Dark(BaseColor::Blue))
}

pub fn text_quit() -> TextView {
TextView::new("Do you want to\nquit the game?")
.style(Color::Dark(BaseColor::Red))
}

/// Returns a slider belonging to the specified `setting` with the text on the side
/// with the specified `description`
pub fn slider_setting(setting: &'static str, description: &str) -> LinearLayout {
let slider = SliderView::horizontal(8).value(2).on_change(|s, n| {
s.call_on_name(setting, |v: &mut TextView| {
v.set_content(format!("{}", n + 2));
});
});

LinearLayout::horizontal()
.child(TextView::new(description))
.child(slider).child(DummyView)
.child(TextView::new("4").with_name(setting))
}
Loading

0 comments on commit 51c0d90

Please sign in to comment.