use super::save::save;
use super::ui::spawn_toast;
use super::{ACHIEVEMENTS, PuzzleAttempt, RoundScratch, StagesRun, Stats, Unlocked, Unsaved};
use crate::app::net::Online;
use crate::app::settings::GameSettings;
use crate::app::sim_events::SimEvent;
use crate::app::{Controllers, SeatController};
use crate::sim::CrabKind;
use bevy::prelude::*;
fn local_seat(online: &Online, controllers: &Controllers) -> Option<u8> {
let local = |seat: usize| controllers.0[seat] == SeatController::Local;
let seat = match &online.0 {
Some(_) => (0..controllers.0.len()).find(|&seat| local(seat))?,
None => 0,
};
local(seat).then_some(seat as u8)
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Trophies<'w> {
pub stats: ResMut<'w, Stats>,
unlocked: ResMut<'w, Unlocked>,
pub settings: Res<'w, GameSettings>,
unsaved: ResMut<'w, Unsaved>,
}
impl Trophies<'_> {
pub fn credit(&mut self, commands: &mut Commands) {
self.unlock(commands);
self.save();
}
pub fn tally(&mut self, commands: &mut Commands) {
if self.unlock(commands) {
self.save();
} else {
self.unsaved.0 = true;
}
}
fn unlock(&mut self, commands: &mut Commands) -> bool {
unlock_new(commands, &self.stats, &mut self.unlocked, &self.settings)
}
fn save(&mut self) {
save(&mut self.stats, &mut self.unlocked);
self.unsaved.0 = false;
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct RoundSeen<'w> {
sim: Res<'w, crate::app::Sim>,
tally: Res<'w, crate::app::awards::RoundTally>,
}
impl RoundSeen<'_> {
fn whole(&self) -> bool {
self.tally.covers(&self.sim.0)
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct DeedsCount<'w> {
screen: Res<'w, State<crate::app::Screen>>,
round: RoundSeen<'w>,
}
impl DeedsCount<'_> {
fn here(&self) -> bool {
*self.screen.get() != crate::app::Screen::Versus || self.round.whole()
}
}
pub fn track_events(
mut commands: Commands,
mut events: MessageReader<SimEvent>,
online: Res<Online>,
controllers: Res<Controllers>,
counted: DeedsCount,
mut trophies: Trophies,
mut scratch: ResMut<RoundScratch>,
) {
let seat = local_seat(&online, &controllers).filter(|_| counted.here());
let Some(seat) = seat else {
for _ in events.read() {}
return;
};
let mut changed = false;
for event in events.read() {
match event {
SimEvent::CrabBanked { owner, kind, .. } if *owner == seat => {
trophies.stats.banked += 1;
scratch.banked += 1;
match kind {
CrabKind::Golden => trophies.stats.golden += 1,
CrabKind::Giant => trophies.stats.giants += 1,
CrabKind::Common
| CrabKind::Juvenile
| CrabKind::Molting
| CrabKind::Sparkling => {}
}
changed = true;
}
SimEvent::TideEventFired { event, by } => {
trophies.stats.events_seen |= 1 << event.index();
if *by == Some(seat) {
trophies.stats.events += 1;
}
changed = true;
}
SimEvent::LureStarted { owner } if *owner == seat => {
trophies.stats.lures += 1;
changed = true;
}
SimEvent::CrabEaten { .. } => {
trophies.stats.gulls_fed += 1;
changed = true;
}
SimEvent::CastleRaided { owner, .. } if *owner == seat => {
trophies.stats.raids_taken += 1;
scratch.raids += 1;
changed = true;
}
SimEvent::CrabBanked { .. }
| SimEvent::LureStarted { .. }
| SimEvent::CastleRaided { .. }
| SimEvent::CrabSpawned { .. }
| SimEvent::GullIncoming { .. }
| SimEvent::GullArrived
| SimEvent::GullTookOff
| SimEvent::GullLanded { .. }
| SimEvent::GullShooed { .. }
| SimEvent::GullFlewOff { .. }
| SimEvent::GoldenCalled { .. }
| SimEvent::SignpostPlaced { .. }
| SimEvent::SignpostRemoved { .. }
| SimEvent::SignpostEvicted { .. }
| SimEvent::TierUp { .. }
| SimEvent::SurgeStarted
| SimEvent::WheelLocked { .. }
| SimEvent::LureRefused { .. }
| SimEvent::GoldenOut { .. }
| SimEvent::RoundEnded => {}
}
}
if changed {
trophies.tally(&mut commands);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(super) struct RoundOutcome {
pub won: bool,
pub online: bool,
pub series: bool,
pub seat: u8,
pub saw_the_end: bool,
}
fn credit_daily(stats: &mut Stats, day: u32, score: u32) {
if stats.daily_day != day {
stats.daily_day = day;
stats.daily_best = 0;
stats.daily_days += 1;
}
stats.daily_best = stats.daily_best.max(score);
stats.daily_record = stats.daily_record.max(stats.daily_best);
}
fn credit_round(stats: &mut Stats, scratch: &mut RoundScratch, outcome: RoundOutcome) {
stats.best_round = stats.best_round.max(scratch.banked);
if outcome.won {
stats.wins += 1;
if scratch.raids == 0 && outcome.saw_the_end {
stats.dry_wins += 1;
}
if outcome.online {
stats.online_wins += 1;
}
stats.seats_won |= 1u8.checked_shl(u32::from(outcome.seat)).unwrap_or(0);
if outcome.series {
stats.series_wins += 1;
}
}
*scratch = RoundScratch::default();
}
fn credit_table(stats: &mut Stats, seats: u8, online: &Online) {
stats.rounds += 1;
stats.crowd = stats.crowd.max(u32::from(seats));
if online
.0
.as_ref()
.is_some_and(crate::app::net::OnlineSession::is_host)
{
stats.hosted += 1;
}
}
pub fn record_round(
mut commands: Commands,
round: RoundSeen,
seating: crate::app::seating::Seating,
scoring: crate::app::teams::Scoring,
tournament: Res<crate::app::tournament::Tournament>,
mut trophies: Trophies,
mut scratch: ResMut<RoundScratch>,
) {
let crate::app::seating::Seating {
seats,
online,
controllers,
..
} = seating;
if !round.whole() {
*scratch = RoundScratch::default();
return;
}
let Some(seat) = local_seat(&online, &controllers) else {
return;
};
let scores = round.sim.0.scores();
if scoring.local.daily.active {
credit_daily(
&mut trophies.stats,
scoring.local.daily.day,
scores[usize::from(seat)],
);
}
let mode = scoring.mode(seats.0);
let won = scoring.leaders(scores, seats.0)[usize::from(seat)];
credit_played(
&mut commands,
&mut trophies,
&mut scratch,
seats.0,
&online,
RoundOutcome {
won,
online: online.0.is_some(),
series: tournament.is_decided()
&& tournament
.winner(mode, seats.0)
.is_some_and(|champion| champion.claims(seat, mode)),
seat,
saw_the_end: true,
},
);
}
fn credit_played(
commands: &mut Commands,
trophies: &mut Trophies,
scratch: &mut RoundScratch,
seats: u8,
online: &Online,
outcome: RoundOutcome,
) {
credit_table(&mut trophies.stats, seats, online);
credit_round(&mut trophies.stats, scratch, outcome);
trophies.credit(commands);
}
pub fn record_called_round(
mut commands: Commands,
round: RoundSeen,
seating: crate::app::seating::Seating,
mut trophies: Trophies,
mut scratch: ResMut<RoundScratch>,
) {
let crate::app::seating::Seating {
seats,
online,
controllers,
..
} = seating;
if !crate::app::session::walked_on(&round.sim.0, &online) || !round.whole() {
return;
}
let (Some(seat), Some(session)) = (local_seat(&online, &controllers), online.0.as_ref()) else {
return;
};
credit_played(
&mut commands,
&mut trophies,
&mut scratch,
seats.0,
&online,
RoundOutcome {
won: crate::transport::is_crowned(session.crowned, seat),
online: true,
series: false,
seat,
saw_the_end: false,
},
);
}
pub fn save_now(
mut stats: ResMut<Stats>,
mut unlocked: ResMut<Unlocked>,
mut unsaved: ResMut<Unsaved>,
) {
save(&mut stats, &mut unlocked);
unsaved.0 = false;
}
pub fn save_on_exit(
mut exits: MessageReader<AppExit>,
mut stats: ResMut<Stats>,
mut unlocked: ResMut<Unlocked>,
mut unsaved: ResMut<Unsaved>,
) {
if exits.read().count() > 0 && unsaved.0 {
save(&mut stats, &mut unlocked);
unsaved.0 = false;
}
}
pub fn record_level_built(
mut commands: Commands,
mut saved: MessageReader<crate::app::LevelSaved>,
mut trophies: Trophies,
) {
let built = saved.read().count() as u32;
if built == 0 {
return;
}
trophies.stats.levels_built += built;
trophies.credit(&mut commands);
}
pub fn reset_round_scratch(mut scratch: ResMut<RoundScratch>) {
*scratch = RoundScratch::default();
}
pub fn record_puzzle(
mut commands: Commands,
progress: Res<crate::app::progress::Progress>,
campaign: Res<crate::app::Campaign>,
mut attempt: ResMut<PuzzleAttempt>,
mut trophies: Trophies,
) {
trophies.stats.puzzles += 1;
credit_stage(
&mut trophies.stats,
&mut attempt,
campaign.current().posts,
campaign.place().section == crate::app::campaign::Section::Custom,
);
let shipped = campaign.section(crate::app::campaign::Section::TidePool);
let builtins = &campaign.levels[shipped];
if !builtins.is_empty() && builtins.iter().all(|level| progress.level_cleared(level)) {
trophies.stats.campaign_done = 1;
}
trophies.credit(&mut commands);
}
fn credit_stage(stats: &mut Stats, attempt: &mut PuzzleAttempt, posts: u8, own: bool) {
if !std::mem::replace(&mut attempt.unbeaten, false) {
return;
}
if own && attempt.layout.len() < usize::from(posts) {
stats.under_par += 1;
}
if posts >= DEEP_POSTS {
stats.deep_solves += 1;
}
if attempt.retries == 0 && attempt.first_run {
stats.clean_solves += 1;
}
}
const DEEP_POSTS: u8 = 5;
pub fn track_puzzle_attempt(
mut loads: MessageReader<crate::app::LoadLevel>,
campaign: Res<crate::app::Campaign>,
progress: Res<crate::app::progress::Progress>,
mut attempt: ResMut<PuzzleAttempt>,
) {
let mut loads = loads.read().count() as u32;
if loads == 0 {
return;
}
let name = &campaign.current().name;
if attempt.stage != *name {
attempt.unbeaten = !progress.level_cleared(campaign.current());
attempt.stage = name.clone();
attempt.retries = 0;
attempt.layout.clear();
loads -= 1;
}
attempt.retries += loads;
}
pub fn note_posts_spent(
sim: Res<crate::app::Sim>,
campaign: Res<crate::app::Campaign>,
mut attempt: ResMut<PuzzleAttempt>,
mut ran: ResMut<StagesRun>,
) {
attempt.first_run = ran.0.insert(campaign.current().name.clone());
attempt.layout = sim
.0
.tiles()
.filter_map(|(x, y, _)| {
let post = sim.0.signpost_at(x, y)?;
Some((x, y, post.dir, post.owner))
})
.collect();
}
pub fn reset_puzzle_attempt(mut attempt: ResMut<PuzzleAttempt>) {
*attempt = PuzzleAttempt::default();
}
pub fn record_codes(
mut commands: Commands,
mut shared: MessageReader<crate::app::CodeShared>,
mut taken: MessageReader<crate::app::CodeTaken>,
mut trophies: Trophies,
) {
let out = shared.read().count() as u32;
let inn = taken.read().count() as u32;
if out == 0 && inn == 0 {
return;
}
trophies.stats.codes_shared += out;
trophies.stats.codes_taken += inn;
trophies.credit(&mut commands);
}
fn unlock_new(
commands: &mut Commands,
stats: &Stats,
unlocked: &mut Unlocked,
settings: &GameSettings,
) -> bool {
let tr = settings.tr();
let mut any = false;
for (index, achievement) in ACHIEVEMENTS.iter().enumerate() {
if !achievement.met(stats) || !unlocked.0.insert(achievement.id) {
continue;
}
any = true;
spawn_toast(commands, tr.ach_names[index], tr.ach_descs[index]);
}
any
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::audio::Muted;
use bevy::ecs::system::RunSystemOnce;
fn win() -> RoundOutcome {
RoundOutcome {
won: true,
saw_the_end: true,
..RoundOutcome::default()
}
}
fn scratch(raids: u32, banked: u32) -> RoundScratch {
RoundScratch { raids, banked }
}
fn tracking(all_unlocked: bool) -> App {
let _ = std::fs::remove_file(crate::app::achievements::save_path());
let mut app = App::new();
app.add_message::<SimEvent>();
app.add_message::<AppExit>();
app.init_resource::<Stats>();
app.init_resource::<RoundScratch>();
app.init_resource::<Unsaved>();
app.init_resource::<GameSettings>();
app.init_resource::<Muted>();
app.init_resource::<Online>();
app.init_resource::<Controllers>();
app.insert_resource(State::new(crate::app::Screen::Puzzle));
app.insert_resource(crate::app::Sim(crate::sim::Board::new(5, 5, 0)));
app.init_resource::<crate::app::awards::RoundTally>();
let mut unlocked = Unlocked::default();
if all_unlocked {
unlocked.0.extend(ACHIEVEMENTS.iter().map(|a| a.id));
}
app.insert_resource(unlocked);
app
}
fn eaten(app: &mut App) {
app.world_mut()
.write_message(SimEvent::CrabEaten { pos: Vec2::ZERO });
let _ = app.world_mut().run_system_once(track_events);
}
#[test]
fn a_change_mid_round_is_kept_until_the_game_closes() {
let mut app = tracking(true);
eaten(&mut app);
assert_eq!(app.world().resource::<Stats>().gulls_fed, 1, "counted");
assert!(app.world().resource::<Unsaved>().0, "and not written yet");
app.world_mut().write_message(AppExit::Success);
let _ = app.world_mut().run_system_once(save_on_exit);
assert!(
!app.world().resource::<Unsaved>().0,
"written on the way out"
);
}
#[test]
fn a_change_that_earns_a_trophy_is_written_at_once() {
let mut app = tracking(false);
app.world_mut().write_message(SimEvent::CrabBanked {
id: 0,
owner: 0,
pos: Vec2::ZERO,
keep: Vec2::ZERO,
points: 1,
kind: CrabKind::Common,
handed: crate::sim::Handedness::Right,
});
let _ = app.world_mut().run_system_once(track_events);
assert!(!app.world().resource::<Unlocked>().0.is_empty(), "unlocked");
assert!(!app.world().resource::<Unsaved>().0, "and written");
}
#[test]
fn a_dry_win_needs_an_unraided_castle() {
let mut stats = Stats::default();
let mut round = RoundScratch::default();
credit_round(&mut stats, &mut round, win());
assert_eq!((stats.wins, stats.dry_wins), (1, 1));
let mut round = scratch(2, 0);
credit_round(&mut stats, &mut round, win());
assert_eq!((stats.wins, stats.dry_wins), (2, 1), "raided: no trophy");
assert_eq!(round.raids, 0, "the scratch resets");
let mut round = scratch(5, 0);
credit_round(&mut stats, &mut round, RoundOutcome::default());
assert_eq!((stats.wins, stats.dry_wins), (2, 1));
assert_eq!(round.raids, 0);
}
#[test]
fn the_best_round_is_a_high_water_mark() {
let mut stats = Stats::default();
let mut round = scratch(0, 30);
credit_round(&mut stats, &mut round, RoundOutcome::default());
assert_eq!(stats.best_round, 30, "a losing round still counts");
assert_eq!(round.banked, 0);
credit_round(&mut stats, &mut scratch(0, 12), win());
assert_eq!(stats.best_round, 30, "a worse round does not lower it");
credit_round(&mut stats, &mut scratch(0, 51), win());
assert_eq!(stats.best_round, 51);
}
#[test]
fn a_stage_already_beaten_is_not_another_stage() {
use crate::app::progress::Progress;
use crate::app::{Campaign, LoadLevel};
let mut levels = crate::sim::campaign_levels();
levels.truncate(2);
let first = levels[0].name.clone();
let mut app = App::new();
app.add_message::<LoadLevel>();
app.init_resource::<PuzzleAttempt>();
app.init_resource::<Progress>();
app.insert_resource(Campaign::tide_pool_of(levels));
app.add_systems(Update, track_puzzle_attempt);
let load = |app: &mut App| {
app.world_mut()
.write_message(LoadLevel { keep_posts: false });
app.update();
};
let unbeaten = |app: &App| app.world().resource::<PuzzleAttempt>().unbeaten;
load(&mut app);
assert!(unbeaten(&app), "nothing cleared yet");
app.world_mut().resource_mut::<Progress>().mark(&first);
let _ = app.world_mut().run_system_once(reset_puzzle_attempt);
load(&mut app);
assert!(!unbeaten(&app), "a stage already in the book");
app.world_mut().resource_mut::<Campaign>().index = 1;
load(&mut app);
assert!(unbeaten(&app), "a stage never played");
}
#[test]
fn a_stage_won_twice_through_a_restart_counts_once() {
use crate::app::{Campaign, LoadLevel};
let mut levels = crate::sim::campaign_levels();
let deep = levels
.iter()
.position(|level| level.posts >= DEEP_POSTS)
.expect("a deep stage");
let posts = levels[deep].posts;
levels.truncate(deep + 1);
let mut app = App::new();
app.add_message::<LoadLevel>();
app.init_resource::<PuzzleAttempt>();
app.init_resource::<crate::app::progress::Progress>();
app.insert_resource(Campaign {
index: deep,
..Campaign::tide_pool_of(levels)
});
app.add_systems(Update, track_puzzle_attempt);
let load = |app: &mut App, keep_posts: bool| {
app.world_mut().write_message(LoadLevel { keep_posts });
app.update();
};
let mut stats = Stats::default();
let mut clear = |app: &mut App| {
let mut attempt = app.world_mut().resource_mut::<PuzzleAttempt>();
attempt.layout = vec![(0, 0, crate::sim::Direction::Up, 0)];
attempt.first_run = true;
credit_stage(&mut stats, &mut attempt, posts, true);
};
load(&mut app, false);
clear(&mut app);
load(&mut app, true);
clear(&mut app);
assert_eq!(
(stats.under_par, stats.deep_solves, stats.clean_solves),
(1, 1, 1),
"one stage, counted once"
);
}
#[test]
fn spare_signposts_count_only_on_a_players_own_level() {
use crate::app::Campaign;
use crate::app::progress::Progress;
let mut levels = crate::sim::campaign_levels();
levels.truncate(1);
assert_eq!(levels[0].name, "Welcome Ashore");
assert!(levels[0].posts > 0, "it grants a post");
levels.push(levels[0].clone());
let mut app = tracking(true);
app.init_resource::<Progress>();
app.insert_resource(Campaign {
levels,
index: 0,
builtins: 1,
coop: 0,
});
let spare = |app: &mut App, index: usize| {
app.world_mut().resource_mut::<Campaign>().index = index;
app.insert_resource(PuzzleAttempt {
unbeaten: true,
..PuzzleAttempt::default()
});
let _ = app.world_mut().run_system_once(record_puzzle);
app.world().resource::<Stats>().under_par
};
assert_eq!(spare(&mut app, 0), 0, "a shipped stage is not one of yours");
assert_eq!(spare(&mut app, 1), 1, "a level of the player's own is");
}
#[test]
fn the_first_load_of_a_stage_is_not_a_retry() {
use crate::app::{Campaign, LoadLevel};
let mut levels = crate::sim::campaign_levels();
levels.truncate(2);
let mut app = App::new();
app.add_message::<LoadLevel>();
app.init_resource::<PuzzleAttempt>();
app.init_resource::<crate::app::progress::Progress>();
app.insert_resource(Campaign::tide_pool_of(levels));
app.add_systems(Update, track_puzzle_attempt);
let load = |app: &mut App| {
app.world_mut()
.write_message(LoadLevel { keep_posts: false });
app.update();
};
let retries = |app: &App| app.world().resource::<PuzzleAttempt>().retries;
load(&mut app);
assert_eq!(retries(&app), 0, "arriving at a stage is not a retry");
load(&mut app);
load(&mut app);
assert_eq!(retries(&app), 2, "two restarts");
app.world_mut().resource_mut::<Campaign>().index = 1;
load(&mut app);
assert_eq!(retries(&app), 0);
load(&mut app);
assert_eq!(retries(&app), 1);
let _ = app.world_mut().run_system_once(reset_puzzle_attempt);
app.world_mut().resource_mut::<Campaign>().index = 0;
load(&mut app);
assert_eq!(retries(&app), 0, "a second visit is a fresh attempt");
}
#[test]
fn a_stage_left_and_come_back_to_is_not_a_first_try() {
use crate::app::Campaign;
let mut levels = crate::sim::campaign_levels();
levels.truncate(2);
let mut app = App::new();
app.init_resource::<PuzzleAttempt>();
app.init_resource::<StagesRun>();
app.insert_resource(crate::app::Sim(crate::sim::Board::new(3, 3, 1)));
app.insert_resource(Campaign::tide_pool_of(levels));
let run = |app: &mut App| {
let _ = app.world_mut().run_system_once(note_posts_spent);
app.world().resource::<PuzzleAttempt>().first_run
};
assert!(run(&mut app), "the first go at a stage");
let _ = app.world_mut().run_system_once(reset_puzzle_attempt);
assert!(!run(&mut app), "back after leaving: a second go");
app.world_mut().resource_mut::<Campaign>().index = 1;
assert!(run(&mut app), "another stage's first");
}
#[test]
fn the_seat_bitmask_counts_chairs_not_wins() {
let mut stats = Stats::default();
for _ in 0..4 {
credit_round(&mut stats, &mut RoundScratch::default(), win());
}
assert_eq!(stats.wins, 4);
assert_eq!(stats.seats_won, 0b0001, "four wins, one chair");
for seat in 1..4 {
credit_round(
&mut stats,
&mut RoundScratch::default(),
RoundOutcome { seat, ..win() },
);
}
assert_eq!(stats.seats_won.count_ones(), 4, "four of the six chairs");
credit_round(
&mut stats,
&mut RoundScratch::default(),
RoundOutcome {
seat: 5,
..RoundOutcome::default()
},
);
assert_eq!(stats.seats_won.count_ones(), 4);
}
#[test]
fn online_and_series_wins_need_the_win() {
let mut stats = Stats::default();
credit_round(
&mut stats,
&mut RoundScratch::default(),
RoundOutcome {
won: false,
online: true,
series: true,
seat: 0,
saw_the_end: true,
},
);
assert_eq!((stats.online_wins, stats.series_wins), (0, 0));
credit_round(
&mut stats,
&mut RoundScratch::default(),
RoundOutcome {
won: true,
online: true,
series: true,
seat: 0,
saw_the_end: true,
},
);
assert_eq!((stats.online_wins, stats.series_wins), (1, 1));
credit_round(&mut stats, &mut RoundScratch::default(), win());
assert_eq!((stats.online_wins, stats.series_wins), (1, 1));
assert_eq!(stats.wins, 2);
}
#[test]
fn a_pasted_round_earns_no_round_stats() {
use crate::app::awards::{Reading, RoundTally};
use crate::sim::{MAX_PLAYERS, PlayerAction, generate_arena};
let idle = [PlayerAction::None; MAX_PLAYERS];
let ended = |resumed: bool| {
let mut app = tracking(false);
app.init_resource::<crate::app::Daily>();
app.init_resource::<crate::app::tournament::Tournament>();
app.init_resource::<crate::app::Playback>();
app.init_resource::<crate::app::SeatNames>();
app.insert_resource(crate::app::Seats(2));
let mut board = generate_arena(7, 2, 12, 9);
let mut tally = RoundTally::default();
for tick in 0..40 {
let before = Reading::of(&board);
board.tick(&idle);
if !resumed || tick >= 20 {
tally.observe(before, &board, &idle);
}
}
board.set_score(0, 9);
app.insert_resource(crate::app::Sim(board));
app.insert_resource(tally);
let _ = app.world_mut().run_system_once(record_round);
let stats = app.world().resource::<Stats>();
(stats.rounds, stats.wins, stats.dry_wins)
};
assert_eq!(ended(false), (1, 1, 1), "played whole, an unraided win");
assert_eq!(ended(true), (0, 0, 0), "pasted half way");
}
#[test]
fn a_pasted_round_counts_no_deeds() {
use crate::app::awards::{Reading, RoundTally};
use crate::sim::{MAX_PLAYERS, PlayerAction, generate_arena};
let idle = [PlayerAction::None; MAX_PLAYERS];
let fed = |resumed: bool| {
let mut app = tracking(false);
app.insert_resource(State::new(crate::app::Screen::Versus));
let mut board = generate_arena(7, 2, 12, 9);
let mut tally = RoundTally::default();
for tick in 0..40 {
let before = Reading::of(&board);
board.tick(&idle);
if !resumed || tick >= 20 {
tally.observe(before, &board, &idle);
}
}
app.insert_resource(crate::app::Sim(board));
app.insert_resource(tally);
eaten(&mut app);
app.world().resource::<Stats>().gulls_fed
};
assert_eq!(fed(false), 1, "played whole");
assert_eq!(fed(true), 0, "pasted half way");
}
#[test]
fn a_round_walked_on_from_is_counted_on_the_hosts_word() {
use crate::app::awards::{Reading, RoundTally};
use crate::sim::{MAX_PLAYERS, PlayerAction, generate_arena};
use crate::transport::{MatchTerms, UdpTransport};
let idle = [PlayerAction::None; MAX_PLAYERS];
let walked_on = |crowned: u8, finished: bool| {
let mut app = tracking(false);
app.init_resource::<crate::app::Playback>();
app.init_resource::<crate::app::SeatNames>();
app.insert_resource(crate::app::Seats(2));
let mut controllers = Controllers([SeatController::Remote; MAX_PLAYERS]);
controllers.0[1] = SeatController::Local;
app.insert_resource(controllers);
let mut joiner = crate::app::net::OnlineSession::new(
UdpTransport::host(0).expect("socket"),
crate::sim::Lockstep::new(2, vec![0, 1, 2], crate::sim::DEFAULT_DELAY),
2,
MatchTerms::default(),
);
joiner.next_round = true;
joiner.crowned = crowned;
app.insert_resource(Online(Some(joiner)));
let mut board = generate_arena(7, 2, 12, 9);
let mut tally = RoundTally::default();
let ticks = if finished { u64::MAX } else { 40 };
while board.ticks() < ticks && !board.round_over() {
let before = Reading::of(&board);
board.tick(&idle);
tally.observe(before, &board, &idle);
}
app.insert_resource(crate::app::Sim(board));
app.insert_resource(tally);
let _ = app.world_mut().run_system_once(record_called_round);
let stats = app.world().resource::<Stats>();
(stats.rounds, stats.wins, stats.dry_wins, stats.online_wins)
};
assert_eq!(walked_on(0b10, false), (1, 1, 0, 1), "seat 1 crowned");
assert_eq!(walked_on(0b01, false), (1, 0, 0, 0), "seat 0 crowned");
assert_eq!(walked_on(0b10, true), (0, 0, 0, 0), "the card's to count");
}
#[test]
fn a_daily_counts_on_the_day_it_was_seeded_for() {
let mut stats = Stats::default();
credit_daily(&mut stats, 100, 12);
assert_eq!(
(stats.daily_day, stats.daily_best, stats.daily_days),
(100, 12, 1)
);
credit_daily(&mut stats, 100, 30);
assert_eq!(
(stats.daily_day, stats.daily_best, stats.daily_days),
(100, 30, 1)
);
credit_daily(&mut stats, 101, 5);
assert_eq!(
(stats.daily_day, stats.daily_best, stats.daily_days),
(101, 5, 2)
);
assert_eq!(stats.daily_record, 30);
}
}