use std::fmt;
use repame_sim::Sim;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GameState {
pub playing: bool,
pub transitional: bool,
pub modal: bool,
pub muted: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PortalView {
pub ad_pending: bool,
pub ad_visible: bool,
pub host_paused: bool,
pub backgrounded: bool,
pub audio_enabled: bool,
}
impl Default for PortalView {
fn default() -> Self {
Self {
ad_pending: false,
ad_visible: false,
host_paused: false,
backgrounded: false,
audio_enabled: true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cause {
Backgrounded,
HostPaused,
Ad,
Modal,
}
impl fmt::Display for Cause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Backgrounded => "the app is in the background",
Self::HostPaused => "the host paused the game",
Self::Ad => "an ad is on screen",
Self::Modal => "a menu owns the screen",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Suspend {
suspended: bool,
live: bool,
silenced: bool,
host_gameplay: bool,
cause: Option<Cause>,
}
impl Suspend {
pub fn resolve(game: GameState, portal: PortalView) -> Self {
let ad = portal.ad_pending || portal.ad_visible;
let away = portal.host_paused || portal.backgrounded;
let cause = if portal.backgrounded {
Some(Cause::Backgrounded)
} else if portal.host_paused {
Some(Cause::HostPaused)
} else if ad {
Some(Cause::Ad)
} else if game.modal {
Some(Cause::Modal)
} else {
None
};
Self {
suspended: cause.is_some() && !game.transitional,
live: game.playing && !game.transitional && !game.modal,
silenced: game.muted || !portal.audio_enabled || ad || away,
host_gameplay: game.playing && !ad && !away,
cause: if game.transitional { None } else { cause },
}
}
pub fn suspended(&self) -> bool {
self.suspended
}
pub fn live(&self) -> bool {
self.live
}
pub fn silenced(&self) -> bool {
self.silenced
}
pub fn host_gameplay(&self) -> bool {
self.host_gameplay
}
pub fn cause(&self) -> Option<Cause> {
self.cause
}
pub fn time_scale(&self) -> f32 {
if self.suspended { 0.0 } else { 1.0 }
}
pub fn apply(&self, sim: &mut Sim) {
sim.time_scale = self.time_scale();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn game(playing: bool, transitional: bool, modal: bool, muted: bool) -> GameState {
GameState {
playing,
transitional,
modal,
muted,
}
}
fn portal(
ad_pending: bool,
ad_visible: bool,
host_paused: bool,
backgrounded: bool,
) -> PortalView {
PortalView {
ad_pending,
ad_visible,
host_paused,
backgrounded,
..Default::default()
}
}
#[test]
fn the_truth_table_is_the_same_rule_everywhere() {
for bits in 0..512u32 {
let game_state = game(bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0);
let view = PortalView {
ad_pending: bits & 16 != 0,
ad_visible: bits & 32 != 0,
host_paused: bits & 64 != 0,
backgrounded: bits & 128 != 0,
audio_enabled: bits & 256 != 0,
};
let decision = Suspend::resolve(game_state, view);
let ad = view.ad_pending || view.ad_visible;
let away = view.host_paused || view.backgrounded;
let held = ad || away || game_state.modal;
let reported = {
if view.backgrounded {
Some(Cause::Backgrounded)
} else if view.host_paused {
Some(Cause::HostPaused)
} else if ad {
Some(Cause::Ad)
} else if game_state.modal {
Some(Cause::Modal)
} else {
None
}
};
assert_eq!(
decision.suspended(),
held && !game_state.transitional,
"clock at {bits:#011b}"
);
assert_eq!(
decision.cause(),
if game_state.transitional {
None
} else {
reported
},
"cause at {bits:#011b}"
);
assert_eq!(
decision.live(),
game_state.playing && !game_state.transitional && !game_state.modal,
"gameplay at {bits:#011b}"
);
assert_eq!(
decision.silenced(),
game_state.muted || !view.audio_enabled || ad || away,
"sound at {bits:#011b}"
);
assert_eq!(
decision.host_gameplay(),
game_state.playing && !ad && !away,
"host gameplay at {bits:#011b}"
);
assert_eq!(
decision.time_scale(),
if decision.suspended() { 0.0 } else { 1.0 },
"clock scale at {bits:#011b}"
);
}
}
#[test]
fn a_transition_keeps_the_clock_for_every_holder() {
for view in [
portal(true, false, false, false),
portal(false, true, false, false),
portal(false, false, true, false),
portal(false, false, false, true),
] {
let decision = Suspend::resolve(game(true, true, false, false), view);
assert!(
!decision.suspended(),
"a transition must be able to finish: {view:?}"
);
assert_eq!(decision.time_scale(), 1.0);
assert!(!decision.live(), "gameplay stays off under a transition");
}
}
#[test]
fn a_modal_keeps_the_clock_and_the_transition_rule_is_not_a_modal() {
let menu = Suspend::resolve(game(false, false, true, false), PortalView::default());
assert!(menu.suspended());
assert_eq!(menu.cause(), Some(Cause::Modal));
assert_eq!(menu.time_scale(), 0.0);
assert!(!menu.live());
}
#[test]
fn the_default_view_suspends_nothing_and_silences_nothing() {
let decision = Suspend::resolve(GameState::default(), PortalView::default());
assert!(!decision.suspended() && !decision.silenced());
assert_eq!(decision.time_scale(), 1.0);
assert!(!decision.live(), "a default state is not a playing run");
assert!(!decision.host_gameplay());
}
#[test]
fn apply_writes_the_clock_and_a_stopped_clock_is_dropped_not_banked() {
let mut sim = Sim::with_default_step();
Suspend::resolve(
game(true, false, false, false),
portal(true, false, false, false),
)
.apply(&mut sim);
assert_eq!(sim.time_scale, 0.0);
Suspend::resolve(game(true, false, false, false), PortalView::default()).apply(&mut sim);
assert_eq!(sim.time_scale, 1.0);
}
}