initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
target
|
||||
Cargo.lock
|
||||
shell.nix
|
||||
dist
|
||||
generated_cards
|
||||
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
21
LICENSE-MIT
Normal 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
67
README.md
Normal 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
15
src/deck/card.rs
Normal 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
56
src/deck/mod.rs
Normal 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
43
src/deck/rank.rs
Normal 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
22
src/deck/suit.rs
Normal 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
125
src/gamemode/mod.rs
Normal 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
313
src/gamemode/tests.rs
Normal 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
3
src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod deck;
|
||||
pub mod gamemode;
|
||||
pub mod player;
|
||||
175
src/player/mod.rs
Normal file
175
src/player/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user