Randomize the deck of tiles
This commit is contained in:
@@ -11,3 +11,4 @@ categories = ["gui", "wasm", "web-programming"]
|
||||
yew = { version="0.20", features=["csr"] }
|
||||
board-shared = { path = "../board-shared" }
|
||||
gloo-dialogs = "0.1.1"
|
||||
getrandom = { version = "0.2.8", features = ["js"] }
|
||||
|
@@ -1,11 +1,12 @@
|
||||
use crate::hand_view::HandView;
|
||||
use crate::tile_view::PlacedTileView;
|
||||
use board_shared::board::Board;
|
||||
use board_shared::deck::RngDeck;
|
||||
use board_shared::expr::is_valid_guess;
|
||||
use board_shared::game::Game;
|
||||
use board_shared::position::Grid2d;
|
||||
use board_shared::tile::Tile;
|
||||
use gloo_dialogs::alert;
|
||||
use board_shared::position::Grid2d;
|
||||
use yew::prelude::*;
|
||||
|
||||
enum SelectedTile {
|
||||
@@ -78,11 +79,14 @@ pub fn app() -> Html {
|
||||
let current_game = current_game.clone();
|
||||
Callback::from(move |_| {
|
||||
let diff = game.board.difference(¤t_game.board);
|
||||
let mut deck = RngDeck::new_complete();
|
||||
if let Some(true) = Board::is_contiguous(&diff) {
|
||||
if let Ok(true) = is_valid_guess(¤t_game.board, &diff) {
|
||||
alert("Valid move!");
|
||||
let mut in_hand = current_game.in_hand.clone();
|
||||
in_hand.complete();
|
||||
if in_hand.complete(&mut deck).is_err() {
|
||||
alert("No more tiles left in deck!");
|
||||
}
|
||||
game.set(Game {
|
||||
board: current_game.board.clone(),
|
||||
in_hand: in_hand.clone(),
|
||||
@@ -103,7 +107,9 @@ pub fn app() -> Html {
|
||||
alert("Invalid move! (not contiguous)");
|
||||
}
|
||||
let mut in_hand = game.in_hand.clone();
|
||||
in_hand.complete();
|
||||
if in_hand.complete(&mut deck).is_err() {
|
||||
alert("No more tiles left in deck!");
|
||||
}
|
||||
current_game.set(Game {
|
||||
board: game.board.clone(),
|
||||
in_hand,
|
||||
|
@@ -6,3 +6,5 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
enum-map = "2.4.2"
|
||||
rand = "0.8.5"
|
||||
|
166
board-shared/src/deck.rs
Normal file
166
board-shared/src/deck.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::tile::Operator;
|
||||
use enum_map::EnumMap;
|
||||
use rand::Rng;
|
||||
|
||||
type DeckSize = u16;
|
||||
type DigitDeck = [DeckSize; 19];
|
||||
|
||||
/// When a deck is empty, new tiles cannot be retrieved.
|
||||
pub type EmptyDeckError = ();
|
||||
|
||||
/// A entire deck of tiles.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Deck {
|
||||
/// The digits and their current count.
|
||||
digits: DigitDeck,
|
||||
/// The operators and their current count.
|
||||
operators: EnumMap<Operator, DeckSize>,
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
pub fn new_complete() -> Self {
|
||||
let mut deck = Self::default();
|
||||
deck.add_operator_times(Operator::Add, 10);
|
||||
deck.add_operator_times(Operator::Subtract, 10);
|
||||
deck.add_operator_times(Operator::Multiply, 6);
|
||||
deck.add_operator_times(Operator::Divide, 4);
|
||||
for digit in -9..0 {
|
||||
deck.add_digit_times(digit, 2);
|
||||
}
|
||||
for digit in 0..=9 {
|
||||
deck.add_digit_times(digit, 8);
|
||||
}
|
||||
deck
|
||||
}
|
||||
|
||||
/// Adds a single digit to the deck.
|
||||
pub fn add_digit(&mut self, digit: i8) {
|
||||
self.add_digit_times(digit, 1)
|
||||
}
|
||||
|
||||
/// Adds a digit multiple times to the deck.
|
||||
pub fn add_digit_times(&mut self, digit: i8, times: DeckSize) {
|
||||
self.digits[Deck::digit_index(digit)] += times;
|
||||
}
|
||||
|
||||
/// Adds a single operator to the deck.
|
||||
pub fn add_operator(&mut self, operator: Operator) {
|
||||
self.add_operator_times(operator, 1)
|
||||
}
|
||||
|
||||
/// Adds an operator multiple times to the deck.
|
||||
pub fn add_operator_times(&mut self, operator: Operator, times: DeckSize) {
|
||||
self.operators[operator] += times;
|
||||
}
|
||||
|
||||
/// Gets the index of a digit in the digit deck.
|
||||
fn digit_index(digit: i8) -> usize {
|
||||
(digit + 9) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// A deck of tiles that can be chosen at random.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RngDeck {
|
||||
deck: Deck,
|
||||
rng: rand::rngs::ThreadRng,
|
||||
}
|
||||
|
||||
impl RngDeck {
|
||||
pub fn new_complete() -> Self {
|
||||
Self {
|
||||
deck: Deck::new_complete(),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a random tile from the deck and remove it from the deck.
|
||||
pub fn rand_digit(&mut self) -> Option<i8> {
|
||||
let sum = self.deck.digits.iter().sum();
|
||||
Self::select_rng(&mut self.rng, sum, self.deck.digits.iter_mut().enumerate())
|
||||
.map(|n| n as i8 - 9)
|
||||
}
|
||||
|
||||
/// Gets a random operator from the deck and remove it from the deck.
|
||||
pub fn rand_operator(&mut self) -> Option<Operator> {
|
||||
let sum = self.deck.operators.values().sum();
|
||||
Self::select_rng(&mut self.rng, sum, self.deck.operators.iter_mut())
|
||||
}
|
||||
|
||||
/// Selects a random item from an iterator of (item, count) pairs.
|
||||
/// The count is decremented by one if the item is selected.
|
||||
fn select_rng<'a, T>(
|
||||
rng: &mut rand::rngs::ThreadRng,
|
||||
sum: DeckSize,
|
||||
it: impl Iterator<Item = (T, &'a mut DeckSize)>,
|
||||
) -> Option<T> {
|
||||
if sum == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut threshold = rng.gen_range(1..=sum);
|
||||
for (item, count) in it {
|
||||
threshold = threshold.saturating_sub(*count);
|
||||
if threshold == 0 {
|
||||
*count -= 1;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RngDeck {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.deck == other.deck
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_empty() {
|
||||
let mut deck = RngDeck::default();
|
||||
assert_eq!(deck.rand_digit(), None);
|
||||
assert_eq!(deck.rand_operator(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_digit() {
|
||||
let mut deck = RngDeck::default();
|
||||
deck.deck.add_digit(1);
|
||||
assert_eq!(deck.rand_digit(), Some(1));
|
||||
assert_eq!(deck.rand_operator(), None);
|
||||
assert_eq!(deck.rand_digit(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_operator() {
|
||||
let mut deck = RngDeck::default();
|
||||
deck.deck.add_operator(Operator::Multiply);
|
||||
assert_eq!(deck.rand_digit(), None);
|
||||
assert_eq!(deck.rand_operator(), Some(Operator::Multiply));
|
||||
assert_eq!(deck.rand_operator(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_proportion() {
|
||||
let mut deck = RngDeck::default();
|
||||
deck.deck.add_digit_times(-4, 2);
|
||||
deck.deck.add_digit_times(3, 7);
|
||||
deck.deck.add_digit_times(7, 3);
|
||||
|
||||
let mut actual = DigitDeck::default();
|
||||
while let Some(digit) = deck.rand_digit() {
|
||||
actual[Deck::digit_index(digit)] += 1;
|
||||
}
|
||||
|
||||
let mut excepted = DigitDeck::default();
|
||||
excepted[Deck::digit_index(-4)] = 2;
|
||||
excepted[Deck::digit_index(3)] = 7;
|
||||
excepted[Deck::digit_index(7)] = 3;
|
||||
|
||||
assert_eq!(actual, excepted);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
use crate::board::Board;
|
||||
use crate::tile::{Digit, Operator, Tile};
|
||||
use crate::deck::{EmptyDeckError, RngDeck};
|
||||
use crate::tile::{Digit, Tile};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct Game {
|
||||
@@ -31,12 +32,15 @@ impl Hand {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn complete(&mut self) {
|
||||
pub fn complete(&mut self, deck: &mut RngDeck) -> Result<(), EmptyDeckError> {
|
||||
for _ in 0..self.count_missing_operators() {
|
||||
self.tiles.push(Tile::Operator(Operator::Add));
|
||||
self.tiles
|
||||
.push(Tile::Operator(deck.rand_operator().ok_or(())?));
|
||||
}
|
||||
for n in 0..self.count_missing_numbers() {
|
||||
self.tiles.push(Tile::Digit(Digit::new((n % 10) as u8)));
|
||||
for _ in 0..self.count_missing_numbers() {
|
||||
self.tiles
|
||||
.push(Tile::Digit(Digit::new(deck.rand_digit().ok_or(())?)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
pub mod board;
|
||||
pub mod deck;
|
||||
pub mod expr;
|
||||
pub mod game;
|
||||
mod lexer;
|
||||
|
@@ -1,15 +1,16 @@
|
||||
use enum_map::Enum;
|
||||
use std::fmt;
|
||||
|
||||
/// A single digit that can be wrapped in parentheses.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct Digit {
|
||||
pub value: u8,
|
||||
pub value: i8,
|
||||
pub has_left_parenthesis: bool,
|
||||
pub has_right_parenthesis: bool,
|
||||
}
|
||||
|
||||
impl Digit {
|
||||
pub fn new(value: u8) -> Self {
|
||||
pub fn new(value: i8) -> Self {
|
||||
Self {
|
||||
value,
|
||||
has_left_parenthesis: false,
|
||||
@@ -44,9 +45,9 @@ impl TryFrom<&str> for Digit {
|
||||
let c = it.next().ok_or(())?;
|
||||
if c == '(' {
|
||||
res.has_left_parenthesis = true;
|
||||
res.value = it.next().ok_or(())?.to_digit(10).ok_or(())? as u8;
|
||||
res.value = it.next().ok_or(())?.to_digit(10).ok_or(())? as i8;
|
||||
} else {
|
||||
res.value = c.to_digit(10).ok_or(())? as u8;
|
||||
res.value = c.to_digit(10).ok_or(())? as i8;
|
||||
}
|
||||
if let Some(c) = it.next() {
|
||||
if c != ')' {
|
||||
@@ -59,7 +60,7 @@ impl TryFrom<&str> for Digit {
|
||||
}
|
||||
|
||||
/// An operator that can be applied between two terms.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Enum)]
|
||||
pub enum Operator {
|
||||
Add,
|
||||
Subtract,
|
||||
|
Reference in New Issue
Block a user