mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-11 09:59:33 +01:00
use schafkopf-logic crate
This commit is contained in:
@@ -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"] }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod deck;
|
||||
pub mod gamemode;
|
||||
pub mod player;
|
||||
@@ -43,6 +43,11 @@ struct SuitAtlas {
|
||||
layout: Handle<TextureAtlasLayout>,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct SauImage {
|
||||
texture: Handle<Image>,
|
||||
}
|
||||
|
||||
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::<CardsSaved>()
|
||||
.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<AssetServer>) {
|
||||
));
|
||||
}
|
||||
|
||||
fn save_all_cards(
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
atlas: Option<Res<SuitAtlas>>,
|
||||
sau_image: Option<Res<SauImage>>,
|
||||
mut cards_saved: ResMut<CardsSaved>,
|
||||
) {
|
||||
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<Assets<Image>>,
|
||||
atlas: Res<SuitAtlas>,
|
||||
sau_image: Res<SauImage>,
|
||||
hand: Res<PlayerHandResource>,
|
||||
q_existing: Query<(), With<BaseCardSprite>>, // 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<Card>) {
|
||||
}
|
||||
|
||||
|
||||
fn create_card_texture(images: &mut Assets<Image>, atlas: &SuitAtlas, card: &Card) -> Handle<Image> {
|
||||
fn create_card_texture(images: &mut Assets<Image>, atlas: &SuitAtlas, sau_image: &SauImage, card: &Card) -> Handle<Image> {
|
||||
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<Image>, 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,
|
||||
|
||||
@@ -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<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()
|
||||
&& let Ok(idx) = input.trim().parse::<usize>()
|
||||
&& 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