diff --git a/schafkopf-logic/.gitignore b/schafkopf-logic/.gitignore index f2f9e58..5bdfff6 100644 --- a/schafkopf-logic/.gitignore +++ b/schafkopf-logic/.gitignore @@ -1,2 +1,3 @@ target -Cargo.lock \ No newline at end of file +Cargo.lock +shell.nix \ No newline at end of file diff --git a/schafkopf-logic/src/deck/card.rs b/schafkopf-logic/src/deck/card.rs index 4f0a51c..a8ee4bd 100644 --- a/schafkopf-logic/src/deck/card.rs +++ b/schafkopf-logic/src/deck/card.rs @@ -10,6 +10,6 @@ pub struct Card { impl fmt::Display for Card { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.rank, self.suit) + write!(f, "{} {}", self.suit, self.rank) } } \ No newline at end of file diff --git a/schafkopf-logic/src/gamemode.rs b/schafkopf-logic/src/gamemode.rs new file mode 100644 index 0000000..48c91ac --- /dev/null +++ b/schafkopf-logic/src/gamemode.rs @@ -0,0 +1,135 @@ +use crate::deck::{Card, Suit, Rank}; + +pub enum Gamemode { + Sauspiel(Suit), + Solo(Suit), + Wenz(Suit), + Geier(Suit), + Bettel, + Ramsch +} + +impl Gamemode { + pub fn winning_card<'a>(&self, cards: [&'a Card; 4]) -> &'a Card { + match self { + Gamemode::Sauspiel(_) | Gamemode::Ramsch | Gamemode::Bettel => + winner_for_trump(Suit::Herz, cards), + Gamemode::Solo(solo_suit) => winner_for_trump(*solo_suit, cards), + _ => cards[0], + } + } +} + +fn winner_for_trump<'a>(trump_suit: Suit, cards: [&'a Card; 4]) -> &'a Card { + if cards.iter().any(|&c| is_trump(c, trump_suit)) { + // Highest trump wins + let winner_idx = cards + .iter() + .enumerate() + .filter(|&(_, &c)| is_trump(c, trump_suit)) + .max_by_key(|&(_, &c)| trump_strength(c, trump_suit)) + .map(|(i, _)| i) + .unwrap_or(0); + cards[winner_idx] + } else { + // No trump: highest of the led suit wins + let first_suit = cards[0].suit; + let winner_idx = cards + .iter() + .enumerate() + .filter(|&(_, &c)| c.suit == first_suit) + .max_by_key(|&(_, &c)| non_trump_strength(c.rank)) + .map(|(i, _)| i) + .unwrap_or(0); + cards[winner_idx] + } +} + +// A card is trump in Sauspiel if it's an Ober, an Unter, or of the trump suit. +fn is_trump(card: &Card, trump_suit: Suit) -> bool { + card.rank == Rank::Ober || card.rank == Rank::Unter || card.suit == trump_suit +} + +// Trump strength according to Schafkopf: +// Obers: Eichel > Gras > Herz > Schell +// Unters: Eichel > Gras > Herz > Schell +// Then trump suit cards: A > 10 > K > 9 > 8 > 7 +fn trump_strength(card: &Card, trump_suit: Suit) -> u16 { + match card.rank { + Rank::Ober => 300 + ober_unter_suit_strength(card.suit), + Rank::Unter => 200 + ober_unter_suit_strength(card.suit), + _ if card.suit == trump_suit => 100 + non_trump_strength(card.rank) as u16, + _ => 0, + } +} + +// Suit precedence for Obers/Unters: Eichel > Gras > Herz > Schell +fn ober_unter_suit_strength(s: Suit) -> u16 { + match s { + Suit::Eichel => 4, + Suit::Gras => 3, + Suit::Herz => 2, + Suit::Schell => 1, + } +} + +// Non-trump (following suit) strength: A > 10 > K > 9 > 8 > 7 +fn non_trump_strength(rank: Rank) -> u8 { + match rank { + Rank::Ass => 6, + Rank::Zehn => 5, + Rank::Koenig => 4, + Rank::Neun => 3, + Rank::Acht => 2, + Rank::Sieben => 1, + _ => 0, // Ober/Unter are trumps, handled elsewhere + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::deck::{Card, Suit, Rank}; + + fn card(suit: Suit, rank: Rank) -> Card { + Card { suit, rank } + } + + #[test] + fn sauspiel_trump_beats_non_trump() { + // In Sauspiel, trumps are all Obers, all Unters, and Herz. + let c0 = card(Suit::Eichel, Rank::Ass); // lead (non-trump) + let c1 = card(Suit::Gras, Rank::Ober); // trump (Ober) + let c2 = card(Suit::Herz, Rank::Zehn); // trump (Herz suit) + let c3 = card(Suit::Gras, Rank::Ass); // non-trump + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c0, &c1, &c2, &c3]); + // Ober beats Herz trumps due to higher trump tier + assert_eq!(winner, &c1); + } + + #[test] + fn sauspiel_follow_suit_highest_non_trump_when_no_trumps() { + // No Obers/Unters and no Herz => follow led suit, highest wins (A > 10 > K > 9 > 8 > 7) + let c0 = card(Suit::Eichel, Rank::Koenig); // lead + let c1 = card(Suit::Gras, Rank::Ass); + let c2 = card(Suit::Eichel, Rank::Ass); // same suit as lead, highest + let c3 = card(Suit::Schell, Rank::Zehn); + + let winner = Gamemode::Sauspiel(Suit::Gras).winning_card([&c0, &c1, &c2, &c3]); + assert_eq!(winner, &c2); + } + + #[test] + fn solo_uses_declared_suit_as_trump() { + // In Solo(Gras), trumps are all Obers, all Unters, and all Gras. + // Avoid Obers/Unters to focus on the solo suit behaving as trump. + let c0 = card(Suit::Herz, Rank::Koenig); // lead + let c1 = card(Suit::Gras, Rank::Zehn); // trump (solo suit) + let c2 = card(Suit::Eichel, Rank::Ass); + let c3 = card(Suit::Schell, Rank::Ass); + + let winner = Gamemode::Solo(Suit::Gras).winning_card([&c0, &c1, &c2, &c3]); + assert_eq!(winner, &c1); + } +} \ No newline at end of file diff --git a/schafkopf-logic/src/main.rs b/schafkopf-logic/src/main.rs index 30fa20b..ea1edcb 100644 --- a/schafkopf-logic/src/main.rs +++ b/schafkopf-logic/src/main.rs @@ -1,5 +1,8 @@ mod deck; -use deck::Deck; +mod gamemode; + +use deck::{Deck, Suit, Card, Rank}; +use gamemode::Gamemode; fn main() { let mut deck = Deck::new(); @@ -26,4 +29,23 @@ fn main() { for card in hand4.iter() { println!("{}", card) } + + let mode = Gamemode::Solo(Suit::Gras); + + //let trick: [&Card; 4] = [&hand1[0], &hand2[0], &hand3[0], &hand4[0]]; + let c1 = Card { suit: Suit::Schell, rank: Rank::Zehn }; + let c2 = Card { suit: Suit::Schell, rank: Rank::Ass }; + let c3 = Card { suit: Suit::Schell, rank: Rank::Unter }; + let c4 = Card { suit: Suit::Gras, rank: Rank::Sieben }; + + let trick: [&Card; 4] = [&c1, &c2, &c3, &c4]; + + + for card in trick.iter() { + println!("{}", card) + } + println!("\n"); + + let winner = mode.winning_card(trick); + println!("\nWinning card: {}", winner); }