use schafkopf-logic crate

This commit is contained in:
2025-11-10 18:06:40 +01:00
parent 7f2fcd9ba0
commit a4a3b19f59
10 changed files with 146 additions and 759 deletions

View File

@@ -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"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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