From 29b34fa3f0cbffe6e62a16fa20729b7409e2bdad Mon Sep 17 00:00:00 2001 From: Valentin Heiserer Date: Mon, 10 Nov 2025 18:13:59 +0100 Subject: [PATCH] initial commit --- .gitignore | 5 + Cargo.toml | 18 +++ LICENSE-MIT | 21 +++ README.md | 67 +++++++++ src/deck/card.rs | 15 ++ src/deck/mod.rs | 56 ++++++++ src/deck/rank.rs | 43 ++++++ src/deck/suit.rs | 22 +++ src/gamemode/mod.rs | 125 +++++++++++++++++ src/gamemode/tests.rs | 313 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/player/mod.rs | 175 +++++++++++++++++++++++ 12 files changed, 863 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/deck/card.rs create mode 100644 src/deck/mod.rs create mode 100644 src/deck/rank.rs create mode 100644 src/deck/suit.rs create mode 100644 src/gamemode/mod.rs create mode 100644 src/gamemode/tests.rs create mode 100644 src/lib.rs create mode 100644 src/player/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa47946 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +Cargo.lock +shell.nix +dist +generated_cards \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a96f12c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "schafkopf-logic" +version = "0.1.0" +edition = "2024" +description = "Logic and rules for the Schafkopf card game: deck, suits, ranks and game modes." +license = "MIT" +readme = "README.md" +repository = "https://github.com/Vale54321/schafkopf-logic" +homepage = "https://github.com/Vale54321/schafkopf-logic" +keywords = ["schafkopf", "card-game", "trick-taking"] +categories = ["game-development"] +authors = ["Valentin Heiserer "] +exclude = ["shell.nix"] + +[dependencies] +strum = "0.27" +strum_macros = "0.27" +rand = "0.9" \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9ff28d5 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Valentin Heiserer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa2a345 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +Maintainer + +Valentin Heiserer + +Features +- Deck construction and iteration +- Shuffling and dealing (4 players × 8 cards) +- Card and rank helpers (human-readable Display, point values) +- Game mode rules to determine trick winners (Sauspiel, Solo, Wenz, Geier, + Bettel, Ramsch) +# schafkopf-logic + +Logic and rules for the German card game Schafkopf. This crate provides types +and helpers for deck construction, common game modes and basic trick-taking +logic. + +**Crate:** `schafkopf-logic` • **Version:** 0.1.0 + +## Features + +- Deck and card types (suits, ranks, cards) with Display implementations +- Shuffling and dealing (4 players × 8 cards) +- Rank point values and helpers +- Game mode logic to determine trick winners (Sauspiel, Solo, Wenz, Geier, + Bettel, Ramsch) + +## Quick example + +```rust +use schafkopf_logic::deck::{Deck, Suit, Rank}; +use schafkopf_logic::gamemode::Gamemode; + +fn main() { + // Create, shuffle and deal a deck + let mut deck = Deck::new(); + deck.shuffle(); + let hands = deck.deal_4x8().expect("deck should contain 32 cards"); + + // form a sample trick from the first card of each hand + let trick = [&hands[0][0], &hands[1][0], &hands[2][0], &hands[3][0]]; + let winner = Gamemode::Sauspiel(Suit::Herz).winning_card(trick); + println!("Winning card: {}", winner); + + // rank points example + assert_eq!(Rank::Ass.points(), 11); +} +``` + +## Development + +Build and run the tests locally: + +```fish +cargo build +cargo test +``` + +If you contribute, please file issues or PRs against the repository: +https://github.com/Vale54321/schafkopf-logic + +## License + +This crate is licensed under the MIT license — see `LICENSE-MIT` for details. + +## Maintainer + +Valentin Heiserer diff --git a/src/deck/card.rs b/src/deck/card.rs new file mode 100644 index 0000000..a8ee4bd --- /dev/null +++ b/src/deck/card.rs @@ -0,0 +1,15 @@ +use std::fmt; + +use crate::deck::{Suit, Rank}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Card { + pub suit: Suit, + pub rank: Rank, +} + +impl fmt::Display for Card { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.suit, self.rank) + } +} \ No newline at end of file diff --git a/src/deck/mod.rs b/src/deck/mod.rs new file mode 100644 index 0000000..7a70f9a --- /dev/null +++ b/src/deck/mod.rs @@ -0,0 +1,56 @@ +mod rank; +pub use rank::Rank; + +mod suit; +pub use suit::Suit; + +mod card; +pub use card::Card; + +use strum::IntoEnumIterator; + +use rand::seq::SliceRandom; +use rand::rng; + +pub struct Deck { + cards: Vec, +} + +impl Default for Deck { + fn default() -> Self { + Self::new() + } +} + +impl Deck { + pub fn new() -> Self { + let cards = Suit::iter() + .flat_map(|suit| Rank::iter().map(move |rank| Card { suit, rank })) + .collect(); + Self { cards } + } + + pub fn draw(&mut self) -> Option { + self.cards.pop() + } + + pub fn shuffle(&mut self) { + self.cards.shuffle(&mut rng()); + } + + pub fn deal_4x8(&mut self) -> Option<[Vec; 4]> { + if self.cards.len() < 32 { return None; } + let mut hands = [Vec::with_capacity(8), Vec::with_capacity(8), + Vec::with_capacity(8), Vec::with_capacity(8)]; + for _ in 0..8 { + for h in 0..4 { + hands[h].push(self.draw()?); + } + } + Some(hands) + } + + pub fn iter(&self) -> impl Iterator { + self.cards.iter() + } +} \ No newline at end of file diff --git a/src/deck/rank.rs b/src/deck/rank.rs new file mode 100644 index 0000000..1dcb25b --- /dev/null +++ b/src/deck/rank.rs @@ -0,0 +1,43 @@ +use std::fmt; +use strum_macros::EnumIter; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumIter)] +pub enum Rank { + Ass, + Zehn, + Koenig, + Ober, + Unter, + Neun, + Acht, + Sieben, +} + +impl Rank { + pub fn points(&self) -> u8 { + match self { + Rank::Ass => 11, + Rank::Zehn => 10, + Rank::Koenig => 4, + Rank::Ober => 3, + Rank::Unter => 3, + _ => 0 + } + } +} + +impl fmt::Display for Rank { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Rank::Ass => "Ass", + Rank::Zehn => "Zehn", + Rank::Koenig => "König", + Rank::Ober => "Ober", + Rank::Unter => "Unter", + Rank::Neun => "Neun", + Rank::Acht => "Acht", + Rank::Sieben => "Sieben", + }; + write!(f, "{}", name) + } +} \ No newline at end of file diff --git a/src/deck/suit.rs b/src/deck/suit.rs new file mode 100644 index 0000000..dac890a --- /dev/null +++ b/src/deck/suit.rs @@ -0,0 +1,22 @@ +use std::fmt; +use strum_macros::EnumIter; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumIter)] +pub enum Suit { + Eichel, + Gras, + Herz, + Schell, +} + +impl fmt::Display for Suit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Suit::Eichel => "Eichel", + Suit::Gras => "Gras", + Suit::Herz => "Herz", + Suit::Schell => "Schell", + }; + write!(f, "{}", name) + } +} \ No newline at end of file diff --git a/src/gamemode/mod.rs b/src/gamemode/mod.rs new file mode 100644 index 0000000..ab5f3c3 --- /dev/null +++ b/src/gamemode/mod.rs @@ -0,0 +1,125 @@ +use crate::deck::{Card, Suit, Rank}; + +pub enum Gamemode { + Sauspiel(Suit), + Solo(Suit), + Wenz(Option), + Geier(Option), + 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), + Gamemode::Wenz(wenz_suit) => winner_for_wenz(Rank::Unter, *wenz_suit, cards), + Gamemode::Geier(geier_suit) => winner_for_wenz(Rank::Ober, *geier_suit, cards), + } + } +} + +fn winner_for_wenz(rank: Rank, trump_suit: Option, cards: [&Card; 4]) -> &Card { + let ranks = [rank]; + + if cards.iter().any(|&c| is_trump(c, &ranks, trump_suit)) { + let winner_idx = cards + .iter() + .enumerate() + .filter(|&(_, &c)| is_trump(c, &ranks, trump_suit)) + .max_by_key(|&(_, &c)| trump_strength_wenz(c, rank, trump_suit)) + .map(|(i, _)| i) + .unwrap_or(0); + cards[winner_idx] + } else { + 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] + } +} + +fn winner_for_trump(trump_suit: Suit, cards: [&Card; 4]) -> &Card { + let ranks = [Rank::Ober, Rank::Unter]; + if cards.iter().any(|&c| is_trump(c, &ranks, Some(trump_suit))) { + // Highest trump wins + let winner_idx = cards + .iter() + .enumerate() + .filter(|&(_, &c)| is_trump(c, &ranks, Some(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] + } +} + +fn is_trump(card: &Card, trump_ranks: &[Rank], trump_suit: Option) -> bool { + trump_ranks.contains(&card.rank) || (trump_suit == Some(card.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, + } +} + +fn trump_strength_wenz(card: &Card, rank: Rank, trump_suit: Option) -> u16 { + if card.rank == rank { + 200 + ober_unter_suit_strength(card.suit) + } else if trump_suit == Some(card.suit) { + 100 + non_trump_strength(card.rank) as u16 + } else { + 0 + } +} + +fn ober_unter_suit_strength(suit: Suit) -> u16 { + match suit { + Suit::Eichel => 4, + Suit::Gras => 3, + Suit::Herz => 2, + Suit::Schell => 1, + } +} + +fn non_trump_strength(rank: Rank) -> u8 { + match rank { + Rank::Ass => 8, + Rank::Zehn => 7, + Rank::Koenig => 6, + Rank::Ober => 5, + Rank::Unter => 4, + Rank::Neun => 3, + Rank::Acht => 2, + Rank::Sieben => 1, + } +} + +#[cfg(test)] +mod tests; diff --git a/src/gamemode/tests.rs b/src/gamemode/tests.rs new file mode 100644 index 0000000..ba66b86 --- /dev/null +++ b/src/gamemode/tests.rs @@ -0,0 +1,313 @@ +use super::*; +use crate::deck::{Card, Suit, Rank}; + +fn card(suit: Suit, rank: Rank) -> Card { + Card { suit, rank } +} + +#[test] +fn winner_test_1() { + let c1 = card(Suit::Herz, Rank::Ober); + let c2 = card(Suit::Gras, Rank::Ober); + let c3 = card(Suit::Schell, Rank::Ass); + let c4 = card(Suit::Gras, Rank::Koenig); + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c1); + + let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c1); + + let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c4); + + let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // Extra: Solo and Herz-trump modes behave consistently with O/U > suit trumps + let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); +} + +#[test] +fn sauspiel_trump_hierarchy() { + // In Sauspiel, trump is always Herz; Obers > Unters > Herz-suit trumps + let c1 = card(Suit::Eichel, Rank::Neun); // led suit, non-trump + let c2 = card(Suit::Gras, Rank::Ober); // trump (Ober) + let c3 = card(Suit::Herz, Rank::Ass); // trump (trump suit) + let c4 = card(Suit::Schell, Rank::Unter); // trump (Unter) + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // More checks on the same trick: + // Wenz: only Unters are trump -> Unter wins + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c4); + + // Geier: only Obers are trump -> Ober wins + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // Solo (any suit): O/U outrank suit trumps -> Ober wins + let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // Herz-trump modes equivalent + let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); +} + +#[test] +fn sauspiel_ober_suit_precedence() { + // Among Obers: Eichel > Gras > Herz > Schell + let c1 = card(Suit::Gras, Rank::Koenig); // led + let c2 = card(Suit::Eichel, Rank::Ober); // highest Ober + let c3 = card(Suit::Herz, Rank::Ober); // lower Ober + let c4 = card(Suit::Schell, Rank::Unter); // trump but below any Ober + + let winner = Gamemode::Sauspiel(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // More checks: + let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // O/U trump + assert_eq!(winner, &c2); + + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // Obers trump + assert_eq!(winner, &c2); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // only Unter trump + assert_eq!(winner, &c4); + + let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); // Herz-trump + assert_eq!(winner, &c2); +} + +#[test] +fn sauspiel_no_trump_led_suit_highest() { + // No Obers/Unters and no Herz cards: highest of led suit wins (A > 10 > K > 9 > 8 > 7) + let c1 = card(Suit::Eichel, Rank::Koenig); // led suit + let c2 = card(Suit::Gras, Rank::Ass); + let c3 = card(Suit::Eichel, Rank::Zehn); // higher than König + let c4 = card(Suit::Schell, Rank::Neun); + + let winner = Gamemode::Sauspiel(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters + assert_eq!(winner, &c3); + + let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // Gras suit trump + assert_eq!(winner, &c2); + + let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // Schell suit trump + assert_eq!(winner, &c4); + + let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // both Eichel trumps; A>10>K... + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell + assert_eq!(winner, &c4); +} + +#[test] +fn solo_suit_trumps_only_internal_order() { + // In Solo, chosen suit is trump plus all Obers/Unters; with only suit trumps present, A > 10 > K > 9 > 8 > 7 + let c1 = card(Suit::Schell, Rank::Zehn); // trump suit + let c2 = card(Suit::Gras, Rank::Koenig); + let c3 = card(Suit::Schell, Rank::Ass); // highest among suit trumps + let c4 = card(Suit::Eichel, Rank::Neun); + + let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // no O/U, Herz not present -> follow suit + assert_eq!(winner, &c3); + + let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // only Gras becomes trump + assert_eq!(winner, &c2); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters -> follow suit + assert_eq!(winner, &c3); + + let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell + assert_eq!(winner, &c3); +} + +#[test] +fn wenz_unter_trumps_over_optional_suit_trump() { + // In Wenz with extra suit trump, Unters outrank any suit trumps + let c1 = card(Suit::Eichel, Rank::Ass); // led + let c2 = card(Suit::Gras, Rank::Koenig); // trump by suit (Gras) if chosen + let c3 = card(Suit::Schell, Rank::Unter); // trump by Unter (beats suit trumps) + let c4 = card(Suit::Gras, Rank::Ass); // trump by suit (Gras) if chosen + + let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // only Unter trump + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit + assert_eq!(winner, &c1); + + let winner = Gamemode::Geier(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras + assert_eq!(winner, &c4); + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> Unter wins + assert_eq!(winner, &c3); +} + +#[test] +fn wenz_unter_precedence_between_suits() { + // Unter precedence: Eichel > Gras > Herz > Schell + let c1 = card(Suit::Herz, Rank::Neun); // led + let c2 = card(Suit::Gras, Rank::Unter); + let c3 = card(Suit::Eichel, Rank::Unter); // highest Unter + let c4 = card(Suit::Schell, Rank::Unter); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // Unters still outrank suit trumps + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit + assert_eq!(winner, &c1); + + let winner = Gamemode::Geier(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras + assert_eq!(winner, &c2); + + let winner = Gamemode::Sauspiel(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> highest Unter by suit + assert_eq!(winner, &c3); +} + +#[test] +fn wenz_no_trump_led_suit_highest() { + // No Unters and no optional suit trumps: highest of led suit wins + let c1 = card(Suit::Eichel, Rank::Koenig); // led suit + let c2 = card(Suit::Gras, Rank::Ass); + let c3 = card(Suit::Eichel, Rank::Zehn); // higher than König + let c4 = card(Suit::Schell, Rank::Neun); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras + assert_eq!(winner, &c2); + + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell + assert_eq!(winner, &c4); + + let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Eichel suit trump + assert_eq!(winner, &c3); +} + +#[test] +fn geier_ober_trumps_over_optional_suit_trump() { + // In Geier with extra suit trump, Obers outrank any suit trumps + let c1 = card(Suit::Gras, Rank::Ass); // led + let c2 = card(Suit::Schell, Rank::Koenig); // trump by suit (optional) + let c3 = card(Suit::Eichel, Rank::Ober); // trump by Ober (beats suit trumps) + let c4 = card(Suit::Schell, Rank::Ass); // trump by suit (optional) + + let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // Obers trump + assert_eq!(winner, &c3); + + let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters -> follow suit + assert_eq!(winner, &c1); + + let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell + assert_eq!(winner, &c4); + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> Ober wins + assert_eq!(winner, &c3); +} + +#[test] +fn bettel_behaves_like_herz_trump() { + // Current implementation treats Bettel like Herz-trump + let c1 = card(Suit::Gras, Rank::Ass); // led + let c2 = card(Suit::Herz, Rank::Neun); // trump by Herz suit + let c3 = card(Suit::Schell, Rank::Ass); + let c4 = card(Suit::Eichel, Rank::Koenig); + + let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c2); + + // More checks: + let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]); // same as Bettel currently + assert_eq!(winner, &c2); + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Herz trump + assert_eq!(winner, &c2); + + let winner = Gamemode::Solo(Suit::Herz).winning_card([&c1, &c2, &c3, &c4]); // Herz trump in Solo + assert_eq!(winner, &c2); + + let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // Gras trump; no O/U + assert_eq!(winner, &c1); + + let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz + assert_eq!(winner, &c2); + + let winner = Gamemode::Geier(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz + assert_eq!(winner, &c2); +} + +#[test] +fn ramsch_behaves_like_herz_trump() { + // Current implementation treats Ramsch like Herz-trump + let c1 = card(Suit::Eichel, Rank::Ass); // led + let c2 = card(Suit::Schell, Rank::Koenig); + let c3 = card(Suit::Herz, Rank::Zehn); // trump by Herz suit + let c4 = card(Suit::Gras, Rank::Neun); + + let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]); + assert_eq!(winner, &c3); + + // More checks: + let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); // same as Ramsch currently + assert_eq!(winner, &c3); + + let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Herz trump + assert_eq!(winner, &c3); + + let winner = Gamemode::Solo(Suit::Herz).winning_card([&c1, &c2, &c3, &c4]); // Herz trump in Solo + assert_eq!(winner, &c3); + + let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Eichel trump; no O/U + assert_eq!(winner, &c1); + + let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz + assert_eq!(winner, &c3); + + let winner = Gamemode::Geier(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz + assert_eq!(winner, &c3); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c06dff1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod deck; +pub mod gamemode; +pub mod player; diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..91026c5 --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,175 @@ +use std::io::{self, Write}; +use crate::deck::Card; +use std::fmt; + +#[derive(Debug)] +pub enum PlayerError { + NoCards, +} + +impl fmt::Display for PlayerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PlayerError::NoCards => write!(f, "no cards available to play"), + } + } +} + +impl std::error::Error for PlayerError {} + +pub struct PlayerBase { + pub id: u32, + pub name: String, +} + +impl PlayerBase { + pub fn new(id: u32, name: impl Into) -> Self { + Self { id, name: name.into() } + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn set_name(&mut self, name: impl Into) { + self.name = name.into(); + } + + pub fn play_card(&mut self, hand: &mut Vec) -> Result { + hand.pop().ok_or(PlayerError::NoCards) + } +} + +pub trait PlayerBaseAccess { + fn base(&self) -> &PlayerBase; + fn base_mut(&mut self) -> &mut PlayerBase; + + fn id(&self) -> u32 { + self.base().id() + } + + fn name(&self) -> &str { + self.base().name() + } + + fn set_name(&mut self, name: impl Into) { + self.base_mut().set_name(name); + } +} + +pub trait ExternalPlayer: PlayerBaseAccess { + fn play_card(&mut self, hand: &mut Vec) -> Result { + self.base_mut().play_card(hand) + } +} + +pub trait InternalPlayer: PlayerBaseAccess { + fn play_card_from_hand(&mut self) -> Result; + fn receive_card(&mut self, card: Card); + fn set_hand(&mut self, hand: Vec); + fn hand(&self) -> &Vec; +} + +pub struct HumanPlayer { + pub base: PlayerBase, + pub hand: Vec, +} + +impl HumanPlayer { + pub fn new(id: u32, name: impl Into) -> Self { + Self { + base: PlayerBase::new(id, name), + hand: Vec::with_capacity(8), + } + } +} + +impl PlayerBaseAccess for HumanPlayer { + fn base(&self) -> &PlayerBase { + &self.base + } + fn base_mut(&mut self) -> &mut PlayerBase { + &mut self.base + } +} + +impl InternalPlayer for HumanPlayer { + fn play_card_from_hand(&mut self) -> Result { + if self.hand.is_empty() { + return Err(PlayerError::NoCards); + } + + println!("{}'s hand:", self.name()); + for (i, c) in self.hand.iter().enumerate() { + println!(" {}: {}", i, c); + } + print!("Select card index to play: "); + let _ = io::stdout().flush(); + + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_ok() { + if let Ok(idx) = input.trim().parse::() { + if idx < self.hand.len() { + return Ok(self.hand.remove(idx)); + } + } + } + + // fallback: pop last + self.hand.pop().ok_or(PlayerError::NoCards) + } + + fn receive_card(&mut self, card: Card) { + self.hand.push(card); + } + + fn set_hand(&mut self, hand: Vec) { + self.hand = hand; + } + + fn hand(&self) -> &Vec { + &self.hand + } +} + +pub struct NpcPlayer { + pub base: PlayerBase, + pub hand: Vec, +} + +impl NpcPlayer { + pub fn new(id: u32, name: impl Into) -> Self { + Self { base: PlayerBase::new(id, name), hand: Vec::with_capacity(8) } + } +} + +impl PlayerBaseAccess for NpcPlayer { + fn base(&self) -> &PlayerBase { &self.base } + fn base_mut(&mut self) -> &mut PlayerBase { &mut self.base } +} + +impl InternalPlayer for NpcPlayer { + fn play_card_from_hand(&mut self) -> Result { + if self.hand.is_empty() { + Err(PlayerError::NoCards) + } else { + Ok(self.hand.remove(0)) + } + } + + fn receive_card(&mut self, card: Card) { + self.hand.push(card); + } + + fn set_hand(&mut self, hand: Vec) { + self.hand = hand; + } + + fn hand(&self) -> &Vec { + &self.hand + } +} \ No newline at end of file