Skip to content

Commit

Permalink
Jack Variation Rule
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
arunsrinivas20 committed Dec 31, 2024
1 parent 052fbe3 commit a1234c5
Show file tree
Hide file tree
Showing 9 changed files with 1,235 additions and 307 deletions.
87 changes: 87 additions & 0 deletions core/src/game_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ mod tests {
(PlayerID(0), starting_rank),
advance_policy,
RNT,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -340,6 +341,7 @@ mod tests {
(PlayerID(0), starting_rank),
advance_policy,
RA,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -412,6 +414,7 @@ mod tests {
(PlayerID(0), starting_rank),
advance_policy,
RNT,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(
Expand Down Expand Up @@ -479,6 +482,7 @@ mod tests {
(PlayerID(0), p0_rank),
advance_policy,
RNT,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(
Expand All @@ -505,6 +509,7 @@ mod tests {
(PlayerID(0), p0_rank),
AdvancementPolicy::Unrestricted,
RNT,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(ranks, vec![R4, R2, RNT, R2],);
Expand All @@ -522,6 +527,7 @@ mod tests {
(PlayerID(0), p0_rank),
AdvancementPolicy::Unrestricted,
RNT,
false,
);
let ranks = p.iter().map(|pp| pp.rank()).collect::<Vec<Rank>>();
assert_eq!(ranks, vec![R3, R2, R3, R2],);
Expand All @@ -541,6 +547,7 @@ mod tests {
(PlayerID(0), R5),
AdvancementPolicy::Unrestricted,
RNT,
false,
);
for p in &players {
assert_eq!(p.rank(), Rank::Number(Number::Four));
Expand All @@ -556,6 +563,7 @@ mod tests {
(PlayerID(0), Rank::Number(Number::Ace)),
AdvancementPolicy::DefendPoints,
RNT,
false,
);
for p in &players {
assert_eq!(p.rank(), R5);
Expand All @@ -572,6 +580,7 @@ mod tests {
(PlayerID(0), RA),
AdvancementPolicy::DefendPoints,
RNT,
false,
);
for p in &players {
if p.id == PlayerID(0) || p.id == PlayerID(2) {
Expand All @@ -592,6 +601,7 @@ mod tests {
(PlayerID(0), Rank::Number(Number::Ace)),
AdvancementPolicy::DefendPoints,
RNT,
false,
);

for p in &players {
Expand All @@ -603,6 +613,83 @@ mod tests {
}
}

#[test]
fn test_jack_variation_landlord_loses() {
let mut players = init_players();

// 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,
true,
);

for p in &players {
assert_eq!(p.rank(), R2);
}
}

#[test]
fn test_jack_variation_landlord_advances_multiple() {
let mut players = init_players();

// 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,
true,
);

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() {
let mut players = init_players();

// 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,
true,
);

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
43 changes: 41 additions & 2 deletions core/src/game_state/play_phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ 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::types::{Card, PlayerID, Rank, Trump};
use shengji_mechanics::trick::{PlayCards, PlayCardsMessage, Trick, TrickEnded, TrickUnit, PlayedCards};
use shengji_mechanics::types::{Card, PlayerID, Rank, Trump, Number};

use crate::message::MessageVariant;
use crate::settings::{
Expand Down Expand Up @@ -238,6 +238,8 @@ impl PlayPhase {
failed_throw_size,
} = self.trick.complete()?;

println!("{:?}", self.trick.played_cards());

let kitty_multipler = match self.propagated.kitty_penalty {
KittyPenalty::Times => 2 * largest_trick_unit_size,
KittyPenalty::Power => 2usize.pow(largest_trick_unit_size as u32),
Expand Down Expand Up @@ -360,6 +362,7 @@ impl PlayPhase {
landlord: (PlayerID, Rank),
advancement_policy: AdvancementPolicy,
max_rank: Rank,
lost_on_single_jack: bool,
) -> Vec<MessageVariant> {
let mut msgs = vec![];

Expand All @@ -373,6 +376,12 @@ impl PlayPhase {
};
let mut num_advances = 0;
let mut was_blocked = false;
// If the landlord team lost the final trick with a single jack, then they will go back to Rank 2,
// regardless of whether or not they successfully defended or not. If they successfully defended,
// then the level bumps will still apply.
if is_defending && lost_on_single_jack {
player.reset_rank();
};
let initial_rank = player.rank();

for bump_idx in 0..bump {
Expand Down Expand Up @@ -559,6 +568,7 @@ impl PlayPhase {
(self.landlord, self.propagated.players[landlord_idx].level),
propagated.advancement_policy,
*propagated.max_rank,
self.check_jacks_last_trick(),
));

let mut idx = (landlord_idx + 1) % propagated.players.len();
Expand Down Expand Up @@ -621,6 +631,35 @@ impl PlayPhase {
Ok((InitializePhase::from_propagated(propagated), msgs))
}

fn check_jacks_last_trick(&self) -> bool {
// When playing with the jacks variation, the trump number has to be Jack, so we can
// return early otherwise
if !(self.propagated.jack_variation && self.trump.number() == Some(Number::Jack)) {
return false
}

let last_trick = self.last_trick.clone().unwrap();
let TrickEnded {
winner: winner_pid,
..
} = last_trick.complete().unwrap();

// In the jack variation, the rule can only applies if the non-landord team wins the
// last trick
if self.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
return cards.len() == 1 && cards[0].number() == Some(Number::Jack);
}

pub fn destructively_redact_for_player(&mut self, player: PlayerID) {
if self.propagated.hide_landlord_points {
for (k, v) in self.points.iter_mut() {
Expand Down
8 changes: 8 additions & 0 deletions core/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ 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)?
Expand Down Expand Up @@ -454,6 +461,7 @@ pub enum Action {
SetGameStartPolicy(GameStartPolicy),
SetShouldRevealKittyAtEndOfGame(bool),
SetHideThrowHaltingPlayer(bool),
SetJackVariation(bool),
SetTractorRequirements(TractorRequirements),
SetGameVisibility(GameVisibility),
StartGame,
Expand Down
5 changes: 5 additions & 0 deletions core/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ pub enum MessageVariant {
HideThrowHaltingPlayer {
set: bool,
},
JackVariation {
set: bool,
},
TractorRequirementsChanged {
tractor_requirements: TractorRequirements,
},
Expand Down Expand Up @@ -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 { set: true } => format!("{} enabled the jack variation", n?),
JackVariation { set: false } => 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?),
Expand Down
16 changes: 16 additions & 0 deletions core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ pub struct PropagatedState {
#[serde(default)]
pub(crate) hide_throw_halting_player: bool,
#[serde(default)]
pub(crate) jack_variation: bool,
#[serde(default)]
pub(crate) tractor_requirements: TractorRequirements,
#[serde(default)]
pub(crate) max_rank: MaxRank,
Expand Down Expand Up @@ -799,6 +801,20 @@ impl PropagatedState {
}
}

pub fn set_jack_variation(
&mut self,
jack_variation: bool,
) -> Result<Vec<MessageVariant>, Error> {
if self.jack_variation != jack_variation {
self.jack_variation = jack_variation;
Ok(vec![MessageVariant::JackVariation {
set: jack_variation,
}])
} else {
Ok(vec![])
}
}

pub fn make_observer(&mut self, player_id: PlayerID) -> Result<Vec<MessageVariant>, Error> {
if let Some(player) = self.players.iter().find(|p| p.id == player_id).cloned() {
self.players.retain(|p| p.id != player_id);
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/Initialize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ interface IUncommonSettings {
setGameStartPolicy: (v: React.ChangeEvent<HTMLSelectElement>) => void;
setGameShadowingPolicy: (v: React.ChangeEvent<HTMLSelectElement>) => void;
setKittyBidPolicy: (v: React.ChangeEvent<HTMLSelectElement>) => void;
setJackVariation: (v: React.ChangeEvent<HTMLSelectElement>) => void;
setHideThrowHaltingPlayer: (v: React.ChangeEvent<HTMLSelectElement>) => void;
setTractorRequirements: (v: TractorRequirements) => void;
}
Expand Down Expand Up @@ -612,6 +613,24 @@ const UncommonSettings = (props: IUncommonSettings): JSX.Element => {
</select>
</label>
</div>
<div>
<label>
Jacks variation:{" "}
<select
value={
props.state.propagated.jack_variation ? "enabled" : "disabled"
}
onChange={props.setJackVariation}
>
<option value="enabled">
Winning the last trick with a J will set the leader's team to rank 2
</option>
<option value="disabled">
Disable the J variation
</option>
</select>
</label>
</div>
</>
);
return (
Expand Down Expand Up @@ -757,6 +776,18 @@ const Initialize = (props: IProps): JSX.Element => {
});
}
};
const setJackVariation = (
evt: React.ChangeEvent<HTMLSelectElement>
): void => {
evt.preventDefault();
if (evt.target.value !== "") {
send({
Action: {
SetJackVariation: evt.target.value === "enabled",
},
});
}
};

const setKittyPenalty = onSelectStringDefault("SetKittyPenalty", null);
const setAdvancementPolicy = onSelectStringDefault(
Expand Down Expand Up @@ -1015,6 +1046,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: {
Expand Down Expand Up @@ -1286,6 +1320,7 @@ const Initialize = (props: IProps): JSX.Element => {
setGameStartPolicy={setGameStartPolicy}
setGameShadowingPolicy={setGameShadowingPolicy}
setKittyBidPolicy={setKittyBidPolicy}
setJackVariation={setJackVariation}
setTractorRequirements={(requirements) =>
send({ Action: { SetTractorRequirements: requirements } })
}
Expand Down
Loading

0 comments on commit a1234c5

Please sign in to comment.