Skip to content

Commit

Permalink
Add Jack Variation (#466)
Browse files Browse the repository at this point in the history
* Jack Variation Rule

- If the dealer team is on rank J and the non-dealer team wins the last trick
  with a single jack, then the dealer team's ranks will be set back to 2. This
  is applied before any rank advancements are applied.
- Works for Tractor and Finding Friends modes
- Adds a setting under "More Game Settings" to enable the variation

* Address CR

* prettier

* clippy

* clippy fixes

* fmt

---------

Co-authored-by: Robert Ying <[email protected]>
  • Loading branch information
arunsrinivas20 and rbtying authored Jan 15, 2025
1 parent ab505ca commit 96d539c
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 14 deletions.
2 changes: 1 addition & 1 deletion backend/src/state_dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 169 additions & 3 deletions core/src/game_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<Player> {
vec![
Player {
Expand Down Expand Up @@ -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::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -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::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -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::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -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::<Vec<Rank>>();
assert_eq!(
Expand All @@ -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::<Vec<Rank>>();
assert_eq!(ranks, vec![R4, R2, RNT, R2],);
Expand All @@ -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::<Vec<Rank>>();
assert_eq!(ranks, vec![R3, R2, R3, R2],);
Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -592,6 +640,8 @@ mod tests {
(PlayerID(0), Rank::Number(Number::Ace)),
AdvancementPolicy::DefendPoints,
RNT,
None,
BackToTwoSetting::Disabled,
);

for p in &players {
Expand All @@ -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();
Expand Down
49 changes: 46 additions & 3 deletions core/src/game_state/play_phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -360,9 +362,14 @@ impl PlayPhase {
landlord: (PlayerID, Rank),
advancement_policy: AdvancementPolicy,
max_rank: Rank,
last_trick: Option<Trick>,
jack_variation: BackToTwoSetting,
) -> Vec<MessageVariant> {
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);
Expand All @@ -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 {
Expand Down Expand Up @@ -446,6 +456,37 @@ impl PlayPhase {
msgs
}

pub fn check_jacks_last_trick(
last_trick: Option<Trick>,
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
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 96d539c

Please sign in to comment.