diff --git a/backend/src/state_dump.rs b/backend/src/state_dump.rs index af3bf7a..c28bc98 100644 --- a/backend/src/state_dump.rs +++ b/backend/src/state_dump.rs @@ -111,7 +111,7 @@ pub async fn dump_state( let send_header_messages = { let mut stats = stats.lock().await; if stats.header_messages != header_messages { - stats.header_messages = header_messages.clone(); + stats.header_messages.clone_from(&header_messages); true } else { false diff --git a/core/src/game_state/mod.rs b/core/src/game_state/mod.rs index 6ad77be..94e9920 100644 --- a/core/src/game_state/mod.rs +++ b/core/src/game_state/mod.rs @@ -193,16 +193,21 @@ impl Deref for GameState { #[cfg(test)] mod tests { use crate::settings::{ - AdvancementPolicy, FriendSelection, FriendSelectionPolicy, GameMode, GameModeSettings, - KittyTheftPolicy, + AdvancementPolicy, BackToTwoSetting, FriendSelection, FriendSelectionPolicy, GameMode, + GameModeSettings, KittyTheftPolicy, }; use shengji_mechanics::player::Player; - use shengji_mechanics::types::{cards, Card, Number, PlayerID, Rank, FULL_DECK}; + use shengji_mechanics::types::{cards, Card, Number, PlayerID, Rank, Suit, Trump, FULL_DECK}; use crate::game_state::{initialize_phase::InitializePhase, play_phase::PlayPhase}; use crate::message::MessageVariant; + use shengji_mechanics::hands::Hands; + use shengji_mechanics::trick::{ + PlayCards, ThrowEvaluationPolicy, TractorRequirements, Trick, TrickDrawPolicy, + }; + const R2: Rank = Rank::Number(Number::Two); const R3: Rank = Rank::Number(Number::Three); const R4: Rank = Rank::Number(Number::Four); @@ -218,6 +223,31 @@ mod tests { const RA: Rank = Rank::Number(Number::Ace); const RNT: Rank = Rank::NoTrump; + const P1: PlayerID = PlayerID(0); + const P2: PlayerID = PlayerID(1); + const P3: PlayerID = PlayerID(2); + const P4: PlayerID = PlayerID(3); + + macro_rules! pc { + ($id:expr, $hands:expr, $cards:expr) => { + PlayCards { + id: $id, + hands: $hands, + cards: $cards, + trick_draw_policy: TrickDrawPolicy::NoProtections, + throw_eval_policy: ThrowEvaluationPolicy::All, + format_hint: None, + hide_throw_halting_player: false, + tractor_requirements: TractorRequirements::default(), + } + }; + } + + const JACK_TRUMP: Trump = Trump::Standard { + number: Number::Jack, + suit: Suit::Spades, + }; + fn init_players() -> Vec { vec![ Player { @@ -289,6 +319,8 @@ mod tests { (PlayerID(0), starting_rank), advance_policy, RNT, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!( @@ -340,6 +372,8 @@ mod tests { (PlayerID(0), starting_rank), advance_policy, RA, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!( @@ -412,6 +446,8 @@ mod tests { (PlayerID(0), starting_rank), advance_policy, RNT, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!( @@ -479,6 +515,8 @@ mod tests { (PlayerID(0), p0_rank), advance_policy, RNT, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!( @@ -505,6 +543,8 @@ mod tests { (PlayerID(0), p0_rank), AdvancementPolicy::Unrestricted, RNT, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!(ranks, vec![R4, R2, RNT, R2],); @@ -522,6 +562,8 @@ mod tests { (PlayerID(0), p0_rank), AdvancementPolicy::Unrestricted, RNT, + None, + BackToTwoSetting::Disabled, ); let ranks = p.iter().map(|pp| pp.rank()).collect::>(); assert_eq!(ranks, vec![R3, R2, R3, R2],); @@ -541,6 +583,8 @@ mod tests { (PlayerID(0), R5), AdvancementPolicy::Unrestricted, RNT, + None, + BackToTwoSetting::Disabled, ); for p in &players { assert_eq!(p.rank(), Rank::Number(Number::Four)); @@ -556,6 +600,8 @@ mod tests { (PlayerID(0), Rank::Number(Number::Ace)), AdvancementPolicy::DefendPoints, RNT, + None, + BackToTwoSetting::Disabled, ); for p in &players { assert_eq!(p.rank(), R5); @@ -572,6 +618,8 @@ mod tests { (PlayerID(0), RA), AdvancementPolicy::DefendPoints, RNT, + None, + BackToTwoSetting::Disabled, ); for p in &players { if p.id == PlayerID(0) || p.id == PlayerID(2) { @@ -592,6 +640,8 @@ mod tests { (PlayerID(0), Rank::Number(Number::Ace)), AdvancementPolicy::DefendPoints, RNT, + None, + BackToTwoSetting::Disabled, ); for p in &players { @@ -603,6 +653,122 @@ mod tests { } } + #[test] + fn test_jack_variation_landlord_loses() { + use cards::*; + + let mut players = init_players(); + let mut hands = Hands::new(vec![P1, P2, P3, P4]); + hands.add(P1, vec![S_2]).unwrap(); + hands.add(P2, vec![S_J]).unwrap(); + hands.add(P3, vec![S_2]).unwrap(); + hands.add(P4, vec![S_3]).unwrap(); + let mut trick = Trick::new(JACK_TRUMP, vec![P1, P2, P3, P4]); + trick.play_cards(pc!(P1, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P2, &mut hands, &[S_J])).unwrap(); + trick.play_cards(pc!(P3, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P4, &mut hands, &[S_3])).unwrap(); + + // Neither side levels up, but the non-landlord team wins the final trick with + // a single jack + let _ = PlayPhase::compute_player_level_deltas( + players.iter_mut(), + 0, + 0, + &[PlayerID(0), PlayerID(2)], + false, // landlord team does not defend + (PlayerID(0), Rank::Number(Number::Jack)), + AdvancementPolicy::DefendPoints, + RNT, + Some(trick), + BackToTwoSetting::SingleJack, + ); + + for p in &players { + assert_eq!(p.rank(), R2); + } + } + + #[test] + fn test_jack_variation_landlord_advances_multiple() { + use cards::*; + + let mut players = init_players(); + let mut hands = Hands::new(vec![P1, P2, P3, P4]); + hands.add(P1, vec![S_2]).unwrap(); + hands.add(P2, vec![S_J]).unwrap(); + hands.add(P3, vec![S_2]).unwrap(); + hands.add(P4, vec![S_3]).unwrap(); + let mut trick = Trick::new(JACK_TRUMP, vec![P1, P2, P3, P4]); + trick.play_cards(pc!(P1, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P2, &mut hands, &[S_J])).unwrap(); + trick.play_cards(pc!(P3, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P4, &mut hands, &[S_3])).unwrap(); + + // The landlord team defends, but the non-landlord team wins the final trick with + // a single jack + let _ = PlayPhase::compute_player_level_deltas( + players.iter_mut(), + 0, + 2, + &[PlayerID(0), PlayerID(2)], + true, // landlord team defends + (PlayerID(0), Rank::Number(Number::Jack)), + AdvancementPolicy::DefendPoints, + RNT, + Some(trick), + BackToTwoSetting::SingleJack, + ); + + for p in &players { + if p.id == PlayerID(0) || p.id == PlayerID(2) { + assert_eq!(p.rank(), R4); + } else { + assert_eq!(p.rank(), R2); + } + } + } + + #[test] + fn test_jack_variation_non_landlord_advances() { + use cards::*; + + let mut players = init_players(); + let mut hands = Hands::new(vec![P1, P2, P3, P4]); + hands.add(P1, vec![S_2]).unwrap(); + hands.add(P2, vec![S_J]).unwrap(); + hands.add(P3, vec![S_2]).unwrap(); + hands.add(P4, vec![S_3]).unwrap(); + let mut trick = Trick::new(JACK_TRUMP, vec![P1, P2, P3, P4]); + trick.play_cards(pc!(P1, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P2, &mut hands, &[S_J])).unwrap(); + trick.play_cards(pc!(P3, &mut hands, &[S_2])).unwrap(); + trick.play_cards(pc!(P4, &mut hands, &[S_3])).unwrap(); + + // The non-landlord team advances and they win the final trick with + // a single jack + let _ = PlayPhase::compute_player_level_deltas( + players.iter_mut(), + 2, + 0, + &[PlayerID(0), PlayerID(2)], + false, // landlord team does not defend + (PlayerID(0), Rank::Number(Number::Jack)), + AdvancementPolicy::DefendPoints, + RNT, + Some(trick), + BackToTwoSetting::SingleJack, + ); + + for p in &players { + if p.id == PlayerID(0) || p.id == PlayerID(2) { + assert_eq!(p.rank(), R2); + } else { + assert_eq!(p.rank(), R4); + } + } + } + #[test] fn test_unusual_kitty_sizes() { let mut init = InitializePhase::new(); diff --git a/core/src/game_state/play_phase.rs b/core/src/game_state/play_phase.rs index 9d6868d..13c784f 100644 --- a/core/src/game_state/play_phase.rs +++ b/core/src/game_state/play_phase.rs @@ -8,13 +8,15 @@ use shengji_mechanics::deck::Deck; use shengji_mechanics::hands::Hands; use shengji_mechanics::player::Player; use shengji_mechanics::scoring::{compute_level_deltas, next_threshold_reachable, GameScoreResult}; -use shengji_mechanics::trick::{PlayCards, PlayCardsMessage, Trick, TrickEnded, TrickUnit}; +use shengji_mechanics::trick::{ + PlayCards, PlayCardsMessage, PlayedCards, Trick, TrickEnded, TrickUnit, +}; use shengji_mechanics::types::{Card, PlayerID, Rank, Trump}; use crate::message::MessageVariant; use crate::settings::{ - AdvancementPolicy, GameMode, KittyPenalty, MultipleJoinPolicy, PlayTakebackPolicy, - PropagatedState, ThrowPenalty, + AdvancementPolicy, BackToTwoSetting, GameMode, KittyPenalty, MultipleJoinPolicy, + PlayTakebackPolicy, PropagatedState, ThrowPenalty, }; use crate::game_state::initialize_phase::InitializePhase; @@ -360,9 +362,14 @@ impl PlayPhase { landlord: (PlayerID, Rank), advancement_policy: AdvancementPolicy, max_rank: Rank, + last_trick: Option, + jack_variation: BackToTwoSetting, ) -> Vec { let mut msgs = vec![]; + let should_go_back_to_two = + Self::check_jacks_last_trick(last_trick, jack_variation, landlords_team, landlord.1); + let result = players .map(|player| { let is_defending = landlords_team.contains(&player.id); @@ -373,6 +380,9 @@ impl PlayPhase { }; let mut num_advances = 0; let mut was_blocked = false; + if is_defending && should_go_back_to_two { + player.reset_rank(); + }; let initial_rank = player.rank(); for bump_idx in 0..bump { @@ -446,6 +456,37 @@ impl PlayPhase { msgs } + pub fn check_jacks_last_trick( + last_trick: Option, + jack_variation: BackToTwoSetting, + landlords_team: &[PlayerID], + landlord_rank: Rank, + ) -> bool { + if !jack_variation.is_applicable(landlord_rank) { + return false; + } + + let last_trick = last_trick.unwrap(); + let TrickEnded { + winner: winner_pid, .. + } = last_trick.complete().unwrap(); + + // In any jack variation, the rule can only applies if the non-landord team wins the + // last trick + if landlords_team.contains(&winner_pid) { + return false; + } + + let lt_played_cards = last_trick.played_cards(); + let PlayedCards { cards, .. } = lt_played_cards + .iter() + .find(|pc| pc.id == winner_pid) + .unwrap(); + + // In the jack variation, the last trick must be won with a single (trump) jack + jack_variation.compute(cards) + } + pub fn calculate_points(&self) -> (isize, isize) { let mut non_landlords_points: isize = self .points @@ -559,6 +600,8 @@ impl PlayPhase { (self.landlord, self.propagated.players[landlord_idx].level), propagated.advancement_policy, *propagated.max_rank, + self.last_trick.clone(), + self.propagated.jack_variation, )); let mut idx = (landlord_idx + 1) % propagated.players.len(); diff --git a/core/src/interactive.rs b/core/src/interactive.rs index b6f094f..f59d0a4 100644 --- a/core/src/interactive.rs +++ b/core/src/interactive.rs @@ -16,10 +16,10 @@ use shengji_mechanics::types::{Card, PlayerID, Rank}; use crate::game_state::{initialize_phase::InitializePhase, GameState}; use crate::message::MessageVariant; use crate::settings::{ - AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelection, FriendSelectionPolicy, - GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, - KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, PropagatedState, - ThrowPenalty, + AdvancementPolicy, BackToTwoSetting, FirstLandlordSelectionPolicy, FriendSelection, + FriendSelectionPolicy, GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, + KittyBidPolicy, KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, + PropagatedState, ThrowPenalty, }; pub struct InteractiveGame { state: GameState, @@ -220,6 +220,10 @@ impl InteractiveGame { info!(logger, "Setting hide throw halting player"; "hide_throw_halting_player" => hide_throw_halting_player); state.set_hide_throw_halting_player(hide_throw_halting_player)? } + (Action::SetJackVariation(jack_variation), GameState::Initialize(ref mut state)) => { + info!(logger, "Setting jack variation"; "jack_variation" => jack_variation); + state.set_jack_variation(jack_variation)? + } (Action::SetGameMode(game_mode), GameState::Initialize(ref mut state)) => { info!(logger, "Setting game mode"; "game_mode" => game_mode.variant()); state.set_game_mode(game_mode)? @@ -454,6 +458,7 @@ pub enum Action { SetGameStartPolicy(GameStartPolicy), SetShouldRevealKittyAtEndOfGame(bool), SetHideThrowHaltingPlayer(bool), + SetJackVariation(BackToTwoSetting), SetTractorRequirements(TractorRequirements), SetGameVisibility(GameVisibility), StartGame, diff --git a/core/src/message.rs b/core/src/message.rs index 206a4ff..d9e569b 100644 --- a/core/src/message.rs +++ b/core/src/message.rs @@ -14,9 +14,9 @@ use shengji_mechanics::types::{Card, PlayerID, Rank}; use crate::game_state::play_phase::PlayerGameFinishedResult; use crate::settings::{ - AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelectionPolicy, GameModeSettings, - GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, KittyPenalty, - KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, ThrowPenalty, + AdvancementPolicy, BackToTwoSetting, FirstLandlordSelectionPolicy, FriendSelectionPolicy, + GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, + KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, ThrowPenalty, }; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type")] @@ -186,6 +186,9 @@ pub enum MessageVariant { HideThrowHaltingPlayer { set: bool, }, + JackVariation { + variation: BackToTwoSetting, + }, TractorRequirementsChanged { tractor_requirements: TractorRequirements, }, @@ -376,6 +379,8 @@ impl MessageVariant { format!("Landlord team lost, opposing team collected {non_landlords_points} points"), HideThrowHaltingPlayer { set: true } => format!("{} hid the player who prevents throws", n?), HideThrowHaltingPlayer { set: false } => format!("{} un-hid the player who prevents throws", n?), + JackVariation { variation: BackToTwoSetting::SingleJack } => format!("{} enabled the single jack variation", n?), + JackVariation { variation: BackToTwoSetting::Disabled } => format!("{} disabled the jack variation", n?), TractorRequirementsChanged { tractor_requirements } => format!("{} required tractors to be at least {} cards wide by {} tuples long", n?, tractor_requirements.min_count, tractor_requirements.min_length), GameVisibilitySet { visibility: GameVisibility::Public} => format!("{} listed the game publicly", n?), diff --git a/core/src/settings.rs b/core/src/settings.rs index 2d5fbd9..827f258 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -213,6 +213,33 @@ impl Deref for MaxRank { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, Default, KV)] +pub enum BackToTwoSetting { + #[default] + Disabled, + SingleJack, +} + +shengji_mechanics::impl_slog_value!(BackToTwoSetting); + +impl BackToTwoSetting { + pub fn is_applicable(&self, landlord_rank: Rank) -> bool { + match self { + BackToTwoSetting::Disabled => false, + BackToTwoSetting::SingleJack => landlord_rank == Rank::Number(Number::Jack), + } + } + + pub fn compute(&self, cards: &[Card]) -> bool { + match self { + BackToTwoSetting::Disabled => false, + BackToTwoSetting::SingleJack => { + cards.len() == 1 && cards[0].number() == Some(Number::Jack) + } + } + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, KV)] pub struct PropagatedState { #[slog(skip)] @@ -282,6 +309,8 @@ pub struct PropagatedState { #[serde(default)] pub(crate) hide_throw_halting_player: bool, #[serde(default)] + pub(crate) jack_variation: BackToTwoSetting, + #[serde(default)] pub(crate) tractor_requirements: TractorRequirements, #[serde(default)] pub(crate) max_rank: MaxRank, @@ -799,6 +828,16 @@ impl PropagatedState { } } + pub fn set_jack_variation( + &mut self, + jack_variation: BackToTwoSetting, + ) -> Result, Error> { + self.jack_variation = jack_variation; + Ok(vec![MessageVariant::JackVariation { + variation: jack_variation, + }]) + } + pub fn make_observer(&mut self, player_id: PlayerID) -> Result, Error> { if let Some(player) = self.players.iter().find(|p| p.id == player_id).cloned() { self.players.retain(|p| p.id != player_id); diff --git a/frontend/src/Initialize.tsx b/frontend/src/Initialize.tsx index c065737..a41afde 100644 --- a/frontend/src/Initialize.tsx +++ b/frontend/src/Initialize.tsx @@ -440,6 +440,7 @@ interface IUncommonSettings { setGameStartPolicy: (v: React.ChangeEvent) => void; setGameShadowingPolicy: (v: React.ChangeEvent) => void; setKittyBidPolicy: (v: React.ChangeEvent) => void; + setJackVariation: (v: React.ChangeEvent) => void; setHideThrowHaltingPlayer: (v: React.ChangeEvent) => void; setTractorRequirements: (v: TractorRequirements) => void; } @@ -612,6 +613,21 @@ const UncommonSettings = (props: IUncommonSettings): JSX.Element => { +
+ +
); return ( @@ -757,6 +773,18 @@ const Initialize = (props: IProps): JSX.Element => { }); } }; + const setJackVariation = ( + evt: React.ChangeEvent, + ): void => { + evt.preventDefault(); + if (evt.target.value !== "") { + send({ + Action: { + SetJackVariation: evt.target.value, + }, + }); + } + }; const setKittyPenalty = onSelectStringDefault("SetKittyPenalty", null); const setAdvancementPolicy = onSelectStringDefault( @@ -1015,6 +1043,9 @@ const Initialize = (props: IProps): JSX.Element => { case "hide_throw_halting_player": send({ Action: { SetHideThrowHaltingPlayer: value } }); break; + case "set_jack_variation": + send({ Action: { SetJackVariation: value } }); + break; case "game_scoring_parameters": send({ Action: { @@ -1286,6 +1317,7 @@ const Initialize = (props: IProps): JSX.Element => { setGameStartPolicy={setGameStartPolicy} setGameShadowingPolicy={setGameShadowingPolicy} setKittyBidPolicy={setKittyBidPolicy} + setJackVariation={setJackVariation} setTractorRequirements={(requirements) => send({ Action: { SetTractorRequirements: requirements } }) } diff --git a/frontend/src/gen-types.d.ts b/frontend/src/gen-types.d.ts index df7a76a..1906eb9 100644 --- a/frontend/src/gen-types.d.ts +++ b/frontend/src/gen-types.d.ts @@ -127,6 +127,9 @@ export type Action = | { SetHideThrowHaltingPlayer: boolean; } + | { + SetJackVariation: BackToTwoSetting; + } | { SetTractorRequirements: TractorRequirements; } @@ -210,6 +213,7 @@ export type BidTakebackPolicy = "AllowBidTakeback" | "NoBidTakeback"; export type KittyTheftPolicy = "AllowKittyTheft" | "NoKittyTheft"; export type GameShadowingPolicy = "AllowMultipleSessions" | "SingleSessionOnly"; export type GameStartPolicy = "AllowAnyPlayer" | "AllowLandlordOnly"; +export type BackToTwoSetting = "Disabled" | "SingleJack"; export type GameVisibility = "Public" | "Unlisted"; export type Card = string; export type TrickUnit = @@ -616,6 +620,11 @@ export type MessageVariant = type: "HideThrowHaltingPlayer"; [k: string]: unknown; } + | { + type: "JackVariation"; + variation: BackToTwoSetting; + [k: string]: unknown; + } | { tractor_requirements: TractorRequirements; type: "TractorRequirementsChanged"; @@ -888,6 +897,7 @@ export interface PropagatedState { hide_landlord_points?: boolean; hide_played_cards?: boolean; hide_throw_halting_player?: boolean; + jack_variation?: BackToTwoSetting & string; joker_bid_policy?: JokerBidPolicy & string; kitty_bid_policy?: KittyBidPolicy & string; kitty_penalty?: KittyPenalty & string; diff --git a/frontend/src/gen-types.schema.json b/frontend/src/gen-types.schema.json index 44b2ae9..d1891b4 100644 --- a/frontend/src/gen-types.schema.json +++ b/frontend/src/gen-types.schema.json @@ -490,6 +490,16 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": ["SetJackVariation"], + "properties": { + "SetJackVariation": { + "$ref": "#/definitions/BackToTwoSetting" + } + }, + "additionalProperties": false + }, { "type": "object", "required": ["SetTractorRequirements"], @@ -610,6 +620,10 @@ "type": "string", "enum": ["Unrestricted", "FullyUnrestricted", "DefendPoints"] }, + "BackToTwoSetting": { + "type": "string", + "enum": ["Disabled", "SingleJack"] + }, "Bid": { "type": "object", "required": ["card", "count", "id"], @@ -2492,6 +2506,19 @@ } } }, + { + "type": "object", + "required": ["type", "variation"], + "properties": { + "type": { + "type": "string", + "enum": ["JackVariation"] + }, + "variation": { + "$ref": "#/definitions/BackToTwoSetting" + } + } + }, { "type": "object", "required": ["tractor_requirements", "type"], @@ -2859,6 +2886,14 @@ "default": false, "type": "boolean" }, + "jack_variation": { + "default": "Disabled", + "allOf": [ + { + "$ref": "#/definitions/BackToTwoSetting" + } + ] + }, "joker_bid_policy": { "default": "BothTwoOrMore", "allOf": [ diff --git a/mechanics/src/player.rs b/mechanics/src/player.rs index 02e9e4e..5f47224 100644 --- a/mechanics/src/player.rs +++ b/mechanics/src/player.rs @@ -33,6 +33,10 @@ impl Player { self.metalevel = metalevel; } + pub fn reset_rank(&mut self) { + self.level = Rank::Number(Number::Two); + } + pub fn advance(&mut self, max_rank: Rank) { match self.level.successor() { Some(next_level) if self.level != max_rank => {