diff --git a/schafkopf-logic/.gitignore b/schafkopf-logic/.gitignore
index 5bdfff6..6813bcc 100644
--- a/schafkopf-logic/.gitignore
+++ b/schafkopf-logic/.gitignore
@@ -1,3 +1,4 @@
target
Cargo.lock
-shell.nix
\ No newline at end of file
+shell.nix
+dist
\ No newline at end of file
diff --git a/schafkopf-logic/Cargo.toml b/schafkopf-logic/Cargo.toml
index 30f55f3..2abd6cf 100644
--- a/schafkopf-logic/Cargo.toml
+++ b/schafkopf-logic/Cargo.toml
@@ -6,4 +6,9 @@ edition = "2024"
[dependencies]
strum = "0.27"
strum_macros = "0.27"
-rand = "0.9"
\ No newline at end of file
+rand = "0.9"
+
+bevy = { version = "0.17", features = ["jpeg"] }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+getrandom = { version = "0.3", features = ["wasm_js"] }
\ No newline at end of file
diff --git a/schafkopf-logic/Trunk.toml b/schafkopf-logic/Trunk.toml
new file mode 100644
index 0000000..eaf2280
--- /dev/null
+++ b/schafkopf-logic/Trunk.toml
@@ -0,0 +1,7 @@
+[build]
+dist = "dist"
+release = true
+
+[serve]
+open = false
+port = 8080
diff --git a/schafkopf-logic/assets/symbole.png b/schafkopf-logic/assets/symbole.png
new file mode 100644
index 0000000..dc90e81
Binary files /dev/null and b/schafkopf-logic/assets/symbole.png differ
diff --git a/schafkopf-logic/index.html b/schafkopf-logic/index.html
new file mode 100644
index 0000000..c22e723
--- /dev/null
+++ b/schafkopf-logic/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Schafkopf Logic
+
+
+
+
+
+
diff --git a/schafkopf-logic/src/gamemode.rs b/schafkopf-logic/src/gamemode.rs
deleted file mode 100644
index 48c91ac..0000000
--- a/schafkopf-logic/src/gamemode.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use crate::deck::{Card, Suit, Rank};
-
-pub enum Gamemode {
- Sauspiel(Suit),
- Solo(Suit),
- Wenz(Suit),
- Geier(Suit),
- Bettel,
- Ramsch
-}
-
-impl Gamemode {
- pub fn winning_card<'a>(&self, cards: [&'a Card; 4]) -> &'a Card {
- match self {
- Gamemode::Sauspiel(_) | Gamemode::Ramsch | Gamemode::Bettel =>
- winner_for_trump(Suit::Herz, cards),
- Gamemode::Solo(solo_suit) => winner_for_trump(*solo_suit, cards),
- _ => cards[0],
- }
- }
-}
-
-fn winner_for_trump<'a>(trump_suit: Suit, cards: [&'a Card; 4]) -> &'a Card {
- if cards.iter().any(|&c| is_trump(c, trump_suit)) {
- // Highest trump wins
- let winner_idx = cards
- .iter()
- .enumerate()
- .filter(|&(_, &c)| is_trump(c, trump_suit))
- .max_by_key(|&(_, &c)| trump_strength(c, trump_suit))
- .map(|(i, _)| i)
- .unwrap_or(0);
- cards[winner_idx]
- } else {
- // No trump: highest of the led suit wins
- let first_suit = cards[0].suit;
- let winner_idx = cards
- .iter()
- .enumerate()
- .filter(|&(_, &c)| c.suit == first_suit)
- .max_by_key(|&(_, &c)| non_trump_strength(c.rank))
- .map(|(i, _)| i)
- .unwrap_or(0);
- cards[winner_idx]
- }
-}
-
-// A card is trump in Sauspiel if it's an Ober, an Unter, or of the trump suit.
-fn is_trump(card: &Card, trump_suit: Suit) -> bool {
- card.rank == Rank::Ober || card.rank == Rank::Unter || card.suit == trump_suit
-}
-
-// Trump strength according to Schafkopf:
-// Obers: Eichel > Gras > Herz > Schell
-// Unters: Eichel > Gras > Herz > Schell
-// Then trump suit cards: A > 10 > K > 9 > 8 > 7
-fn trump_strength(card: &Card, trump_suit: Suit) -> u16 {
- match card.rank {
- Rank::Ober => 300 + ober_unter_suit_strength(card.suit),
- Rank::Unter => 200 + ober_unter_suit_strength(card.suit),
- _ if card.suit == trump_suit => 100 + non_trump_strength(card.rank) as u16,
- _ => 0,
- }
-}
-
-// Suit precedence for Obers/Unters: Eichel > Gras > Herz > Schell
-fn ober_unter_suit_strength(s: Suit) -> u16 {
- match s {
- Suit::Eichel => 4,
- Suit::Gras => 3,
- Suit::Herz => 2,
- Suit::Schell => 1,
- }
-}
-
-// Non-trump (following suit) strength: A > 10 > K > 9 > 8 > 7
-fn non_trump_strength(rank: Rank) -> u8 {
- match rank {
- Rank::Ass => 6,
- Rank::Zehn => 5,
- Rank::Koenig => 4,
- Rank::Neun => 3,
- Rank::Acht => 2,
- Rank::Sieben => 1,
- _ => 0, // Ober/Unter are trumps, handled elsewhere
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::deck::{Card, Suit, Rank};
-
- fn card(suit: Suit, rank: Rank) -> Card {
- Card { suit, rank }
- }
-
- #[test]
- fn sauspiel_trump_beats_non_trump() {
- // In Sauspiel, trumps are all Obers, all Unters, and Herz.
- let c0 = card(Suit::Eichel, Rank::Ass); // lead (non-trump)
- let c1 = card(Suit::Gras, Rank::Ober); // trump (Ober)
- let c2 = card(Suit::Herz, Rank::Zehn); // trump (Herz suit)
- let c3 = card(Suit::Gras, Rank::Ass); // non-trump
-
- let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c0, &c1, &c2, &c3]);
- // Ober beats Herz trumps due to higher trump tier
- assert_eq!(winner, &c1);
- }
-
- #[test]
- fn sauspiel_follow_suit_highest_non_trump_when_no_trumps() {
- // No Obers/Unters and no Herz => follow led suit, highest wins (A > 10 > K > 9 > 8 > 7)
- let c0 = card(Suit::Eichel, Rank::Koenig); // lead
- let c1 = card(Suit::Gras, Rank::Ass);
- let c2 = card(Suit::Eichel, Rank::Ass); // same suit as lead, highest
- let c3 = card(Suit::Schell, Rank::Zehn);
-
- let winner = Gamemode::Sauspiel(Suit::Gras).winning_card([&c0, &c1, &c2, &c3]);
- assert_eq!(winner, &c2);
- }
-
- #[test]
- fn solo_uses_declared_suit_as_trump() {
- // In Solo(Gras), trumps are all Obers, all Unters, and all Gras.
- // Avoid Obers/Unters to focus on the solo suit behaving as trump.
- let c0 = card(Suit::Herz, Rank::Koenig); // lead
- let c1 = card(Suit::Gras, Rank::Zehn); // trump (solo suit)
- let c2 = card(Suit::Eichel, Rank::Ass);
- let c3 = card(Suit::Schell, Rank::Ass);
-
- let winner = Gamemode::Solo(Suit::Gras).winning_card([&c0, &c1, &c2, &c3]);
- assert_eq!(winner, &c1);
- }
-}
\ No newline at end of file
diff --git a/schafkopf-logic/src/gamemode/mod.rs b/schafkopf-logic/src/gamemode/mod.rs
new file mode 100644
index 0000000..bfa1f2a
--- /dev/null
+++ b/schafkopf-logic/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<'a>(rank: Rank, trump_suit: Option, cards: [&'a Card; 4]) -> &'a 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<'a>(trump_suit: Suit, cards: [&'a Card; 4]) -> &'a 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.iter().any(|&r| card.rank == r) || trump_suit.map_or(false, |s| card.suit == s)
+}
+
+// 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.map_or(false, |s| card.suit == s) {
+ 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
new file mode 100644
index 0000000..ba66b86
--- /dev/null
+++ b/schafkopf-logic/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/schafkopf-logic/src/lib.rs b/schafkopf-logic/src/lib.rs
new file mode 100644
index 0000000..c06dff1
--- /dev/null
+++ b/schafkopf-logic/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod deck;
+pub mod gamemode;
+pub mod player;
diff --git a/schafkopf-logic/src/main.rs b/schafkopf-logic/src/main.rs
index ea1edcb..c538aa3 100644
--- a/schafkopf-logic/src/main.rs
+++ b/schafkopf-logic/src/main.rs
@@ -1,51 +1,335 @@
-mod deck;
-mod gamemode;
+use bevy::{
+ asset::{AssetMetaCheck, AssetPlugin, RenderAssetUsages},
+ prelude::*,
+ sprite::Anchor,
+ render::render_resource::{Extent3d, TextureDimension, TextureFormat},
+};
+use schafkopf_logic::{
+ deck::{Card, Deck, Rank, Suit},
+ gamemode::Gamemode,
+ player::{HumanPlayer, InternalPlayer},
+};
-use deck::{Deck, Suit, Card, Rank};
-use gamemode::Gamemode;
+use std::fmt::Debug;
+
+
+const CARD_TEXTURE_WIDTH: usize = 96;
+const CARD_TEXTURE_HEIGHT: usize = 135;
+const CARD_WORLD_SIZE: Vec2 = Vec2::new(96.0, 135.0);
+const ICON_OFFSET_TL: Vec2 = Vec2::new(-CARD_WORLD_SIZE.x * 0.5 + 16.0, CARD_WORLD_SIZE.y * 0.5 - 20.0);
+const ICON_OFFSET_BR: Vec2 = Vec2::new( CARD_WORLD_SIZE.x * 0.5 - 16.0, -CARD_WORLD_SIZE.y * 0.5 + 20.0);
+const GLYPH_WIDTH: usize = 5;
+const GLYPH_HEIGHT: usize = 7;
+const GLYPH_STRIDE: usize = 6;
+const SUIT_ICON_PX: usize = 32;
+const LABEL_MARGIN_X: usize = 14;
+const LABEL_MARGIN_Y: usize = 8;
+const LABEL_TEXT_GAP: usize = 4;
+
+#[derive(Resource)]
+struct CurrentGamemode(Gamemode);
+
+#[derive(Resource)]
+struct SuitAtlas {
+ texture: Handle,
+ layout: Handle,
+}
+
+impl SuitAtlas {
+ fn load(
+ asset_server: &AssetServer,
+ layouts: &mut Assets,
+ ) -> Self {
+ let texture: Handle = asset_server.load("symbole.png");
+ let layout = TextureAtlasLayout::from_grid(UVec2::splat(32), 2, 2, None, None);
+ let layout_handle = layouts.add(layout);
+
+ Self { texture, layout: layout_handle }
+ }
+
+ fn index_for(&self, suit: Suit) -> usize {
+ match suit {
+ Suit::Eichel => 0,
+ Suit::Gras => 1,
+ Suit::Herz => 2,
+ Suit::Schell => 3,
+ }
+ }
+}
+
+#[derive(Resource)]
+struct PlayerHandResource {
+ cards: Vec,
+}
+
+#[derive(Component)]
+struct PlayerCardVisual {
+ card: Card,
+ index: usize,
+}
fn main() {
+ App::new()
+ .add_plugins(
+ DefaultPlugins.set(
+ AssetPlugin {
+ meta_check: AssetMetaCheck::Never,
+ ..default()
+ }
+ ).set(ImagePlugin::default_nearest()))
+ .add_systems(Startup, setup_game)
+ .add_systems(PostStartup, spawn_player_hand)
+ .run();
+}
+
+fn setup_game(
+ mut commands: Commands,
+ asset_server: Res,
+ mut texture_layouts: ResMut>,
+) {
+ commands.spawn(Camera2d);
+
+ let atlas = SuitAtlas::load(&asset_server, &mut texture_layouts);
+ commands.insert_resource(atlas);
+
let mut deck = Deck::new();
deck.shuffle();
+ let [mut hand1, mut hand2, mut hand3, mut hand4] =
+ deck.deal_4x8().expect("expected a full deck to deal four hands");
- let [mut hand1, mut hand2, mut hand3, mut hand4] = deck.deal_4x8().unwrap();
+ sort_cards(&mut hand1);
- println!("Player 1 has:");
- for card in hand1.iter() {
- println!("{}", card)
- }
+ let mut p1 = HumanPlayer::new(1, "Alice");
+ let mut p2 = HumanPlayer::new(2, "Bob");
+ let mut p3 = HumanPlayer::new(3, "Clara");
+ let mut p4 = HumanPlayer::new(4, "Max");
- println!("\nPlayer 2 has:");
- for card in hand2.iter() {
- println!("{}", card)
- }
+ p1.set_hand(hand1);
+ p2.set_hand(hand2);
+ p3.set_hand(hand3);
+ p4.set_hand(hand4);
- println!("\nPlayer 3 has:");
- for card in hand3.iter() {
- println!("{}", card)
- }
+ let mode = Gamemode::Wenz(None);
+ commands.insert_resource(CurrentGamemode(mode));
- println!("\nPlayer 4 has:");
- for card in hand4.iter() {
- println!("{}", card)
- }
-
- let mode = Gamemode::Solo(Suit::Gras);
-
- //let trick: [&Card; 4] = [&hand1[0], &hand2[0], &hand3[0], &hand4[0]];
- let c1 = Card { suit: Suit::Schell, rank: Rank::Zehn };
- let c2 = Card { suit: Suit::Schell, rank: Rank::Ass };
- let c3 = Card { suit: Suit::Schell, rank: Rank::Unter };
- let c4 = Card { suit: Suit::Gras, rank: Rank::Sieben };
-
- let trick: [&Card; 4] = [&c1, &c2, &c3, &c4];
-
-
- for card in trick.iter() {
- println!("{}", card)
- }
- println!("\n");
-
- let winner = mode.winning_card(trick);
- println!("\nWinning card: {}", winner);
+ commands.insert_resource(PlayerHandResource {
+ cards: p1.hand().clone(),
+ });
}
+
+fn spawn_player_hand(
+ mut commands: Commands,
+ mut images: ResMut>,
+ atlas: Res,
+ hand: Res,
+) {
+ let spacing = CARD_WORLD_SIZE.x + 5.0;
+ let start_x = -(spacing * (hand.cards.len() as f32 - 1.0) / 2.0);
+ let y = -200.0;
+
+ for (i, card) in hand.cards.iter().enumerate() {
+ let base_handle = create_card_texture(&mut images, card);
+
+ let parent = commands
+ .spawn((Transform::from_xyz(start_x + i as f32 * spacing, y, 0.0)))
+ .id();
+
+ commands.entity(parent).with_children(|c| {
+ c.spawn((
+ Sprite {
+ image: base_handle,
+ custom_size: Some(CARD_WORLD_SIZE),
+ ..default()
+ },
+ Transform::from_xyz(0.0, 0.0, 0.0),
+ Pickable::default(),
+ ))
+ .observe(on_hover())
+ .observe(on_unhover())
+ .observe(on_click(card.clone()));
+
+ c.spawn((
+ Sprite::from_atlas_image(
+ atlas.texture.clone(),
+ TextureAtlas {
+ layout: atlas.layout.clone(),
+ index: atlas.index_for(card.suit),
+ },
+ ),
+ Transform::from_xyz(ICON_OFFSET_TL.x, ICON_OFFSET_TL.y, 0.1), // on top
+ ));
+
+ c.spawn((
+ Sprite::from_atlas_image(
+ atlas.texture.clone(),
+ TextureAtlas {
+ layout: atlas.layout.clone(),
+ index: atlas.index_for(card.suit),
+ },
+ ),
+ Transform::from_xyz(ICON_OFFSET_BR.x, ICON_OFFSET_BR.y, 0.1),
+ ));
+ });
+ }
+}
+fn sort_cards(cards: &mut Vec) {
+ cards.sort_by(|a, b| a.suit.cmp(&b.suit).then(a.rank.cmp(&b.rank)));
+}
+
+
+fn create_card_texture(images: &mut Assets, card: &Card) -> Handle {
+ let mut pixels = vec![0u8; CARD_TEXTURE_WIDTH * CARD_TEXTURE_HEIGHT * 4];
+
+ let background = suit_background(card.suit);
+ for chunk in pixels.chunks_exact_mut(4) {
+ chunk.copy_from_slice(&background);
+ }
+
+ draw_border(&mut pixels, [45, 45, 45, 255]);
+
+ let rank_text = rank_label(card.rank);
+
+ let ink = [15, 15, 15, 255];
+ let rank_text_len = rank_text.chars().count();
+ let rank_text_width = if rank_text_len == 0 {
+ 0
+ } else {
+ (rank_text_len - 1) * GLYPH_STRIDE + GLYPH_WIDTH
+ };
+
+ let top_label_x = LABEL_MARGIN_X;
+ let top_label_y = LABEL_MARGIN_Y + SUIT_ICON_PX + LABEL_TEXT_GAP;
+ let bottom_label_x = CARD_TEXTURE_WIDTH.saturating_sub(LABEL_MARGIN_X + rank_text_width);
+ let bottom_label_y = CARD_TEXTURE_HEIGHT
+ .saturating_sub(LABEL_MARGIN_Y + SUIT_ICON_PX + LABEL_TEXT_GAP + GLYPH_HEIGHT);
+
+ draw_text(&mut pixels, top_label_x, top_label_y, rank_text, ink);
+ draw_text(&mut pixels, bottom_label_x, bottom_label_y, rank_text, ink);
+
+ let extent = Extent3d {
+ width: CARD_TEXTURE_WIDTH as u32,
+ height: CARD_TEXTURE_HEIGHT as u32,
+ depth_or_array_layers: 1,
+ };
+
+ let image = Image::new_fill(
+ extent,
+ TextureDimension::D2,
+ &pixels,
+ TextureFormat::Rgba8UnormSrgb,
+ RenderAssetUsages::default(),
+ );
+
+ images.add(image)
+}
+
+fn draw_border(pixels: &mut [u8], color: [u8; 4]) {
+ for x in 0..CARD_TEXTURE_WIDTH {
+ set_pixel(pixels, x, 0, color);
+ set_pixel(pixels, x, CARD_TEXTURE_HEIGHT - 1, color);
+ }
+
+ for y in 0..CARD_TEXTURE_HEIGHT {
+ set_pixel(pixels, 0, y, color);
+ set_pixel(pixels, CARD_TEXTURE_WIDTH - 1, y, color);
+ }
+}
+
+fn draw_text(pixels: &mut [u8], start_x: usize, start_y: usize, text: &str, color: [u8; 4]) {
+ let mut x = start_x;
+ for ch in text.chars() {
+ if let Some(bitmap) = glyph_bitmap(ch) {
+ draw_glyph(pixels, x, start_y, bitmap, color);
+ }
+ x += GLYPH_STRIDE;
+ }
+}
+
+fn draw_glyph(pixels: &mut [u8], start_x: usize, start_y: usize, glyph: [u8; 7], color: [u8; 4]) {
+ for (row, pattern) in glyph.iter().enumerate() {
+ for col in 0..5 {
+ if (pattern >> (4 - col)) & 1 == 1 {
+ set_pixel(pixels, start_x + col, start_y + row, color);
+ }
+ }
+ }
+}
+
+fn set_pixel(pixels: &mut [u8], x: usize, y: usize, color: [u8; 4]) {
+ if x >= CARD_TEXTURE_WIDTH || y >= CARD_TEXTURE_HEIGHT {
+ return;
+ }
+ let index = (y * CARD_TEXTURE_WIDTH + x) * 4;
+ pixels[index..index + 4].copy_from_slice(&color);
+}
+
+fn glyph_bitmap(ch: char) -> Option<[u8; 7]> {
+ match ch {
+ '0' => Some([0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110]),
+ '1' => Some([0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110]),
+ '7' => Some([0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000]),
+ '8' => Some([0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110]),
+ '9' => Some([0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b11100]),
+ 'A' => Some([0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001]),
+ 'K' => Some([0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001]),
+ 'O' => Some([0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110]),
+ 'U' => Some([0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110]),
+ _ => None,
+ }
+}
+
+fn rank_label(rank: Rank) -> &'static str {
+ match rank {
+ Rank::Ass => "A",
+ Rank::Zehn => "10",
+ Rank::Koenig => "K",
+ Rank::Ober => "O",
+ Rank::Unter => "U",
+ Rank::Neun => "9",
+ Rank::Acht => "8",
+ Rank::Sieben => "7",
+ }
+}
+
+fn suit_background(suit: Suit) -> [u8; 4] {
+ match suit {
+ Suit::Eichel => [245, 235, 220, 255],
+ Suit::Gras => [225, 245, 225, 255],
+ Suit::Herz => [245, 225, 225, 255],
+ Suit::Schell => [245, 240, 210, 255],
+ }
+}
+
+fn suit_color(suit: Suit) -> [u8; 4] {
+ match suit {
+ Suit::Eichel => [131, 100, 56, 255],
+ Suit::Gras => [62, 120, 54, 255],
+ Suit::Herz => [170, 40, 60, 255],
+ Suit::Schell => [204, 142, 30, 255],
+ }
+}
+
+fn on_hover() -> impl Fn(On>, Query<(&mut Sprite, &mut Transform)>) {
+ move |ev, mut q| {
+ if let Ok((mut sprite, mut transform)) = q.get_mut(ev.event_target()) {
+ sprite.color = Color::srgb(0.6, 0.6, 0.6);
+ transform.scale = Vec3::splat(1.1);
+ }
+ }
+}
+
+fn on_unhover() -> impl Fn(On>, Query<(&mut Sprite, &mut Transform)>) {
+ move |ev, mut q| {
+ if let Ok((mut sprite, mut transform)) = q.get_mut(ev.event_target()) {
+ sprite.color = Color::WHITE;
+ transform.scale = Vec3::ONE;
+ }
+ }
+}
+
+
+fn on_click(card: Card) -> impl Fn(On>, Query<(&mut Sprite, &mut Transform)>) {
+ move |ev, cards| {
+ println!("Clicked on card: {:?}", card);
+ }
+}
\ No newline at end of file
diff --git a/schafkopf-logic/src/player/mod.rs b/schafkopf-logic/src/player/mod.rs
new file mode 100644
index 0000000..91026c5
--- /dev/null
+++ b/schafkopf-logic/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