initial commit

This commit is contained in:
2025-11-10 18:13:59 +01:00
commit 29b34fa3f0
12 changed files with 863 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
target
Cargo.lock
shell.nix
dist
generated_cards

18
Cargo.toml Normal file
View File

@@ -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 <valentin@heiserer.de>"]
exclude = ["shell.nix"]
[dependencies]
strum = "0.27"
strum_macros = "0.27"
rand = "0.9"

21
LICENSE-MIT Normal file
View File

@@ -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.

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
Maintainer
Valentin Heiserer <valentin@heiserer.de>
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 <valentin@heiserer.de>

15
src/deck/card.rs Normal file
View File

@@ -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)
}
}

56
src/deck/mod.rs Normal file
View File

@@ -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<Card>,
}
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<Card> {
self.cards.pop()
}
pub fn shuffle(&mut self) {
self.cards.shuffle(&mut rng());
}
pub fn deal_4x8(&mut self) -> Option<[Vec<Card>; 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<Item=&Card> {
self.cards.iter()
}
}

43
src/deck/rank.rs Normal file
View File

@@ -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)
}
}

22
src/deck/suit.rs Normal file
View File

@@ -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)
}
}

125
src/gamemode/mod.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::deck::{Card, Suit, Rank};
pub enum Gamemode {
Sauspiel(Suit),
Solo(Suit),
Wenz(Option<Suit>),
Geier(Option<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),
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<Suit>, 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<Suit>) -> 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<Suit>) -> 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;

313
src/gamemode/tests.rs Normal file
View File

@@ -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);
}

3
src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod deck;
pub mod gamemode;
pub mod player;

175
src/player/mod.rs Normal file
View File

@@ -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<String>) -> 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<String>) {
self.name = name.into();
}
pub fn play_card(&mut self, hand: &mut Vec<Card>) -> Result<Card, PlayerError> {
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<String>) {
self.base_mut().set_name(name);
}
}
pub trait ExternalPlayer: PlayerBaseAccess {
fn play_card(&mut self, hand: &mut Vec<Card>) -> Result<Card, PlayerError> {
self.base_mut().play_card(hand)
}
}
pub trait InternalPlayer: PlayerBaseAccess {
fn play_card_from_hand(&mut self) -> Result<Card, PlayerError>;
fn receive_card(&mut self, card: Card);
fn set_hand(&mut self, hand: Vec<Card>);
fn hand(&self) -> &Vec<Card>;
}
pub struct HumanPlayer {
pub base: PlayerBase,
pub hand: Vec<Card>,
}
impl HumanPlayer {
pub fn new(id: u32, name: impl Into<String>) -> 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<Card, PlayerError> {
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::<usize>() {
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<Card>) {
self.hand = hand;
}
fn hand(&self) -> &Vec<Card> {
&self.hand
}
}
pub struct NpcPlayer {
pub base: PlayerBase,
pub hand: Vec<Card>,
}
impl NpcPlayer {
pub fn new(id: u32, name: impl Into<String>) -> 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<Card, PlayerError> {
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<Card>) {
self.hand = hand;
}
fn hand(&self) -> &Vec<Card> {
&self.hand
}
}