Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Jack Variation #466

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading