diff --git a/schafkopf-logic/Cargo.toml b/schafkopf-logic/Cargo.toml index a5b7692..7b165e2 100644 --- a/schafkopf-logic/Cargo.toml +++ b/schafkopf-logic/Cargo.toml @@ -1,14 +1,11 @@ [package] -name = "schafkopf-logic" +name = "schafkopf-game" version = "0.1.0" edition = "2024" [dependencies] -strum = "0.27" -strum_macros = "0.27" -rand = "0.9" - -bevy = { version = "0.17", features = ["jpeg", "default_font"] } +schafkopf-logic = "0.1.0" +bevy = { version = "0.17", features = ["png", "default_font"] } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.3", features = ["wasm_js"] } \ No newline at end of file diff --git a/schafkopf-logic/src/deck/card.rs b/schafkopf-logic/src/deck/card.rs deleted file mode 100644 index a8ee4bd..0000000 --- a/schafkopf-logic/src/deck/card.rs +++ /dev/null @@ -1,15 +0,0 @@ -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/schafkopf-logic/src/deck/mod.rs b/schafkopf-logic/src/deck/mod.rs deleted file mode 100644 index 7a70f9a..0000000 --- a/schafkopf-logic/src/deck/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -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/schafkopf-logic/src/deck/rank.rs b/schafkopf-logic/src/deck/rank.rs deleted file mode 100644 index 1dcb25b..0000000 --- a/schafkopf-logic/src/deck/rank.rs +++ /dev/null @@ -1,43 +0,0 @@ -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/schafkopf-logic/src/deck/suit.rs b/schafkopf-logic/src/deck/suit.rs deleted file mode 100644 index dac890a..0000000 --- a/schafkopf-logic/src/deck/suit.rs +++ /dev/null @@ -1,22 +0,0 @@ -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/schafkopf-logic/src/gamemode/mod.rs b/schafkopf-logic/src/gamemode/mod.rs deleted file mode 100644 index ab5f3c3..0000000 --- a/schafkopf-logic/src/gamemode/mod.rs +++ /dev/null @@ -1,125 +0,0 @@ -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/schafkopf-logic/src/gamemode/tests.rs b/schafkopf-logic/src/gamemode/tests.rs deleted file mode 100644 index ba66b86..0000000 --- a/schafkopf-logic/src/gamemode/tests.rs +++ /dev/null @@ -1,313 +0,0 @@ -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/schafkopf-logic/src/lib.rs b/schafkopf-logic/src/lib.rs deleted file mode 100644 index c06dff1..0000000 --- a/schafkopf-logic/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod deck; -pub mod gamemode; -pub mod player; diff --git a/schafkopf-logic/src/main.rs b/schafkopf-logic/src/main.rs index d9bdc98..b3ea209 100644 --- a/schafkopf-logic/src/main.rs +++ b/schafkopf-logic/src/main.rs @@ -43,6 +43,11 @@ struct SuitAtlas { layout: Handle, } +#[derive(Resource)] +struct SauImage { + texture: Handle, +} + impl SuitAtlas { fn load( asset_server: &AssetServer, @@ -80,6 +85,10 @@ struct PlayerHandResource { #[derive(Component)] struct BaseCardSprite; +// Resource to track if cards have been saved +#[derive(Resource, Default)] +struct CardsSaved(bool); + fn main() { App::new() .add_plugins( @@ -97,9 +106,10 @@ fn main() { }) .set(ImagePlugin::default_nearest()) ) + .init_resource::() .add_systems(Startup, (setup_game, spawn_click_text)) // Spawn the player hand once the atlas image is fully loaded - .add_systems(Update, spawn_player_hand) + .add_systems(Update, (spawn_player_hand, save_all_cards)) .add_systems(Update, update_click_text) .run(); } @@ -114,6 +124,11 @@ fn setup_game( let atlas = SuitAtlas::load(&asset_server, &mut texture_layouts); commands.insert_resource(atlas); + let sau_image = SauImage { + texture: asset_server.load("schell_sau.png"), + }; + commands.insert_resource(sau_image); + let mut deck = Deck::new(); deck.shuffle(); let [mut hand1, hand2, hand3, hand4] = @@ -175,10 +190,83 @@ fn spawn_click_text(mut commands: Commands, _asset_server: Res) { )); } +fn save_all_cards( + mut images: ResMut>, + atlas: Option>, + sau_image: Option>, + mut cards_saved: ResMut, +) { + use std::fs; + + // Skip if already saved + if cards_saved.0 { + return; + } + + // Check if atlas resource exists + let Some(atlas) = atlas else { + return; + }; + + // Check if sau_image resource exists + let Some(sau_image) = sau_image else { + return; + }; + + // Wait for atlas to load + if images.get(&atlas.texture).and_then(|img| img.data.as_ref()).is_none() { + return; + } + + // Wait for sau image to load + if images.get(&sau_image.texture).and_then(|img| img.data.as_ref()).is_none() { + return; + } + + // Mark as saved to prevent running again + cards_saved.0 = true; + + // Create output directory + let _ = fs::create_dir_all("generated_cards"); + + // Generate all 32 cards + let suits = [Suit::Eichel, Suit::Gras, Suit::Herz, Suit::Schell]; + let ranks = [ + Rank::Ass, Rank::Zehn, Rank::Koenig, Rank::Ober, + Rank::Unter, Rank::Neun, Rank::Acht, Rank::Sieben, + ]; + + for suit in &suits { + for rank in &ranks { + let card = Card { suit: *suit, rank: *rank }; + let image_handle = create_card_texture(&mut images, &atlas, &sau_image, &card); + + if let Some(image) = images.get(&image_handle) { + let filename = format!("generated_cards/{}_{}.png", + format!("{:?}", suit).to_lowercase(), + format!("{:?}", rank).to_lowercase() + ); + + // Save using Bevy's DynamicImage + if let Ok(dynamic_image) = image.clone().try_into_dynamic() { + if let Err(e) = dynamic_image.save(&filename) { + eprintln!("Failed to save {}: {}", filename, e); + } else { + println!("Saved {}", filename); + } + } + } + } + } + + println!("All cards saved to generated_cards/ directory"); +} + fn spawn_player_hand( mut commands: Commands, mut images: ResMut>, atlas: Res, + sau_image: Res, hand: Res, q_existing: Query<(), With>, // guard to spawn once ) { @@ -196,7 +284,7 @@ fn spawn_player_hand( let y = -200.0; for (i, card) in hand.cards.iter().enumerate() { - let base_handle = create_card_texture(&mut images, &atlas, card); + let base_handle = create_card_texture(&mut images, &atlas, &sau_image, card); let parent = commands .spawn(Transform::from_xyz(start_x + i as f32 * spacing, y, 0.0)) @@ -224,7 +312,7 @@ fn sort_cards(cards: &mut Vec) { } -fn create_card_texture(images: &mut Assets, atlas: &SuitAtlas, card: &Card) -> Handle { +fn create_card_texture(images: &mut Assets, atlas: &SuitAtlas, sau_image: &SauImage, card: &Card) -> Handle { let mut pixels = vec![0u8; CARD_TEXTURE_WIDTH * CARD_TEXTURE_HEIGHT * 4]; let top_h = CARD_TEXTURE_HEIGHT / 2; let border_gap = 9; @@ -263,6 +351,18 @@ fn create_card_texture(images: &mut Assets, atlas: &SuitAtlas, card: &Car ); } + // Blit Sau image at the bottom for Ass cards + if card.rank == Rank::Ass && card.suit == Suit::Schell { + if let Some(sau_img) = images.get(&sau_image.texture) { + let sau_width = 94; + let sau_height = 86; + // Center horizontally, position at bottom of top half + let sau_x = (CARD_TEXTURE_WIDTH - sau_width) / 2; + let sau_y = top_h - sau_height; + blit_image(&mut pixels, CARD_TEXTURE_WIDTH, CARD_TEXTURE_HEIGHT, sau_img, sau_x, sau_y); + } + } + // Draw a rounded rectangle border in the TOP half with 9px gap from card edge and 6px radius. // This will be mirrored to the bottom half later. let border_color = [45, 45, 45, 255]; @@ -694,6 +794,46 @@ fn blit_atlas_icon_top_left( } } +// Blit a full image onto the card texture at the specified position +fn blit_image( + dest_pixels: &mut [u8], + dest_w: usize, + dest_h: usize, + src_img: &Image, + dest_x: usize, + dest_y: usize, +) { + if let Some(ref data) = src_img.data { + let src_w = src_img.width() as usize; + let src_h = src_img.height() as usize; + + for y in 0..src_h { + for x in 0..src_w { + let dx = dest_x + x; + let dy = dest_y + y; + + // Skip if out of bounds + if dx >= dest_w || dy >= dest_h { + continue; + } + + let s_index = (y * src_w + x) * 4; + let d_index = (dy * dest_w + dx) * 4; + + if s_index + 4 <= data.len() && d_index + 4 <= dest_pixels.len() { + let a = data[s_index + 3]; + if a == 0 { continue; } // Skip transparent pixels + + dest_pixels[d_index + 0] = data[s_index + 0]; + dest_pixels[d_index + 1] = data[s_index + 1]; + dest_pixels[d_index + 2] = data[s_index + 2]; + dest_pixels[d_index + 3] = 255; // opaque result + } + } + } + } +} + fn draw_rounded_rect_filled( pixels: &mut [u8], x1: usize, diff --git a/schafkopf-logic/src/player/mod.rs b/schafkopf-logic/src/player/mod.rs deleted file mode 100644 index 9eb3f30..0000000 --- a/schafkopf-logic/src/player/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -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() - && let Ok(idx) = input.trim().parse::() - && 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