use std::num::{NonZeroU32, NonZeroUsize};
use crate::core_game_engine::{
BOARD_WIDTH, Game, GameAccess, GameBuilder, GameEndCause, GameModifier, GameRng, Line,
MiscPceRots, MiscTetGens, NotificationFeed, Phase, TileType,
};
use rand::seq::SliceRandom;
use crate::savefile_logic::to_savefile_string;
#[derive(
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub struct Cheese {
config: CheeseConfig,
cheese_eaten: u32,
temp_last_clear_actual_cheese_lines: usize,
cheese_generated: u32,
last_hole_pattern_generated: Vec<usize>,
cached_display_values: [(String, String); 2],
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct CheeseConfig {
pub holes_per_line: NonZeroUsize,
pub ensure_distinct_holes: bool,
pub limit: Option<NonZeroU32>,
}
impl Default for CheeseConfig {
fn default() -> Self {
Self {
holes_per_line: NonZeroUsize::MIN,
ensure_distinct_holes: true,
limit: Some(NonZeroU32::try_from(40).unwrap()),
}
}
}
impl Cheese {
pub const MOD_ID: &str = stringify!(Cheese);
pub fn build(builder: &GameBuilder, config: CheeseConfig) -> Game {
let modifier = Box::new(Self {
config,
cheese_eaten: 0,
temp_last_clear_actual_cheese_lines: 0,
cheese_generated: 0,
last_hole_pattern_generated: Vec::new(),
cached_display_values: [
("Cheese eaten".to_owned(), 0.to_string()),
("Efficiency".to_owned(), Self::fmt_efficiency(0, 0)),
],
});
builder.build_modded(vec![modifier])
}
}
impl GameModifier<MiscTetGens, MiscPceRots, TileType> for Cheese {
fn id(&self) -> String {
Self::MOD_ID.to_owned()
}
fn cfg(&self) -> String {
to_savefile_string(&(self.config)).unwrap()
}
fn values(&self) -> &[(String, String)] {
&self.cached_display_values
}
fn try_clone(
&self,
) -> Result<Box<dyn GameModifier<MiscTetGens, MiscPceRots, TileType>>, String> {
Ok(Box::new(self.clone()))
}
fn on_game_built(&mut self, game: GameAccess) {
let cheese_lines = Self::cheese_lines(
&self.config,
&mut self.last_hole_pattern_generated,
&mut self.cheese_generated,
&mut game.state.rng,
);
const INIT_HEIGHT: usize = 10;
game.state.board.resize(INIT_HEIGHT, Default::default());
for ((line, _is_frozen), cheese) in game
.state
.board
.iter_mut()
.take(INIT_HEIGHT)
.zip(cheese_lines)
{
*line = cheese;
}
}
fn on_lock_post(&mut self, game: GameAccess, _feed: &mut NotificationFeed) {
self.temp_last_clear_actual_cheese_lines = 0;
for (line, _is_frozen) in game.state.board.iter() {
if line.iter().all(|mino| mino.is_some()) {
if line.contains(&Some(TileType::Generic)) {
self.cheese_eaten += 1;
self.temp_last_clear_actual_cheese_lines += 1;
}
}
}
}
fn on_lines_clear_post(&mut self, game: GameAccess, _feed: &mut NotificationFeed) {
if let Some(limit) = self.config.limit
&& self.cheese_eaten >= limit.get()
{
*game.phase = Phase::GameEnd {
cause: GameEndCause::Custom("All cheese devoured".to_owned()),
is_win: true,
};
return;
}
let cheese_lines = Self::cheese_lines(
&self.config,
&mut self.last_hole_pattern_generated,
&mut self.cheese_generated,
&mut game.state.rng,
);
for cheese_line in cheese_lines.take(self.temp_last_clear_actual_cheese_lines) {
game.state.board.insert(0, (cheese_line, false));
}
self.cached_display_values[0].1 = Self::fmt_cheese_eaten(self.cheese_eaten);
self.cached_display_values[1].1 = Self::fmt_efficiency(
self.cheese_eaten,
game.state.pieces_locked.iter().sum::<u32>(),
);
game.state.points = self.cheese_eaten;
}
}
impl Cheese {
fn fmt_cheese_eaten(cheese_eaten: u32) -> String {
format!("{}", cheese_eaten)
}
fn fmt_efficiency(cheese_eaten: u32, pieces: u32) -> String {
if pieces == 0 {
"-".to_owned()
} else {
format!(
"{:.01}%",
100.0 * f64::from(cheese_eaten) / f64::from(pieces)
)
}
}
pub(in crate::game_modding) fn cheese_lines<'a>(
config: &'a CheeseConfig,
last_hole_pattern_generated: &'a mut Vec<usize>,
generated: &'a mut u32,
rng: &'a mut GameRng,
) -> impl Iterator<Item = Line> + 'a {
std::iter::from_fn(move || {
config.limit.is_none_or(|l| *generated < l.get()).then(|| {
*generated += 1;
let mut line = Line::default();
for tile in line
.iter_mut()
.take(BOARD_WIDTH.saturating_sub(config.holes_per_line.get()))
{
*tile = Some(TileType::Generic);
}
loop {
line.shuffle(rng);
let hole_pattern_generated: Vec<usize> = line
.iter()
.enumerate()
.filter_map(|(i, x)| x.is_some().then_some(i))
.collect();
if !config.ensure_distinct_holes
|| hole_pattern_generated != *last_hole_pattern_generated
|| hole_pattern_generated.len() == line.len()
{
*last_hole_pattern_generated = hole_pattern_generated;
break;
}
}
line
})
})
}
}