initialize bevy game

This commit is contained in:
2025-10-31 00:03:35 +01:00
parent 7da9b0484f
commit 6efdc7a0d0
11 changed files with 966 additions and 177 deletions

View File

@@ -1,3 +1,4 @@
target target
Cargo.lock Cargo.lock
shell.nix shell.nix
dist

View File

@@ -7,3 +7,8 @@ edition = "2024"
strum = "0.27" strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
rand = "0.9" rand = "0.9"
bevy = { version = "0.17", features = ["jpeg"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }

View File

@@ -0,0 +1,7 @@
[build]
dist = "dist"
release = true
[serve]
open = false
port = 8080

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Schafkopf Logic</title>
<link data-trunk rel="rust" href="Cargo.toml" />
<link data-trunk rel="copy-dir" href="assets" />
</head>
<body></body>
</html>

View File

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

View File

@@ -0,0 +1,125 @@
use crate::deck::{Card, Suit, Rank};
pub enum Gamemode {
Sauspiel(Suit),
Solo(Suit),
Wenz(Option<Suit>),
Geier(Option<Suit>),
Bettel,
Ramsch
}
impl Gamemode {
pub fn winning_card<'a>(&self, cards: [&'a Card; 4]) -> &'a Card {
match self {
Gamemode::Sauspiel(_) | Gamemode::Ramsch | Gamemode::Bettel =>
winner_for_trump(Suit::Herz, cards),
Gamemode::Solo(solo_suit) => winner_for_trump(*solo_suit, cards),
Gamemode::Wenz(wenz_suit) => winner_for_wenz(Rank::Unter, *wenz_suit, cards),
Gamemode::Geier(geier_suit) => winner_for_wenz(Rank::Ober, *geier_suit, cards),
}
}
}
fn winner_for_wenz<'a>(rank: Rank, trump_suit: Option<Suit>, 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<Suit>) -> 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<Suit>) -> 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;

View File

@@ -0,0 +1,313 @@
use super::*;
use crate::deck::{Card, Suit, Rank};
fn card(suit: Suit, rank: Rank) -> Card {
Card { suit, rank }
}
#[test]
fn winner_test_1() {
let c1 = card(Suit::Herz, Rank::Ober);
let c2 = card(Suit::Gras, Rank::Ober);
let c3 = card(Suit::Schell, Rank::Ass);
let c4 = card(Suit::Gras, Rank::Koenig);
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c1);
let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c1);
let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c4);
let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// Extra: Solo and Herz-trump modes behave consistently with O/U > suit trumps
let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
}
#[test]
fn sauspiel_trump_hierarchy() {
// In Sauspiel, trump is always Herz; Obers > Unters > Herz-suit trumps
let c1 = card(Suit::Eichel, Rank::Neun); // led suit, non-trump
let c2 = card(Suit::Gras, Rank::Ober); // trump (Ober)
let c3 = card(Suit::Herz, Rank::Ass); // trump (trump suit)
let c4 = card(Suit::Schell, Rank::Unter); // trump (Unter)
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// More checks on the same trick:
// Wenz: only Unters are trump -> Unter wins
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c4);
// Geier: only Obers are trump -> Ober wins
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// Solo (any suit): O/U outrank suit trumps -> Ober wins
let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// Herz-trump modes equivalent
let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
}
#[test]
fn sauspiel_ober_suit_precedence() {
// Among Obers: Eichel > Gras > Herz > Schell
let c1 = card(Suit::Gras, Rank::Koenig); // led
let c2 = card(Suit::Eichel, Rank::Ober); // highest Ober
let c3 = card(Suit::Herz, Rank::Ober); // lower Ober
let c4 = card(Suit::Schell, Rank::Unter); // trump but below any Ober
let winner = Gamemode::Sauspiel(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// More checks:
let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // O/U trump
assert_eq!(winner, &c2);
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // Obers trump
assert_eq!(winner, &c2);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // only Unter trump
assert_eq!(winner, &c4);
let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); // Herz-trump
assert_eq!(winner, &c2);
}
#[test]
fn sauspiel_no_trump_led_suit_highest() {
// No Obers/Unters and no Herz cards: highest of led suit wins (A > 10 > K > 9 > 8 > 7)
let c1 = card(Suit::Eichel, Rank::Koenig); // led suit
let c2 = card(Suit::Gras, Rank::Ass);
let c3 = card(Suit::Eichel, Rank::Zehn); // higher than König
let c4 = card(Suit::Schell, Rank::Neun);
let winner = Gamemode::Sauspiel(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters
assert_eq!(winner, &c3);
let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // Gras suit trump
assert_eq!(winner, &c2);
let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // Schell suit trump
assert_eq!(winner, &c4);
let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // both Eichel trumps; A>10>K...
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell
assert_eq!(winner, &c4);
}
#[test]
fn solo_suit_trumps_only_internal_order() {
// In Solo, chosen suit is trump plus all Obers/Unters; with only suit trumps present, A > 10 > K > 9 > 8 > 7
let c1 = card(Suit::Schell, Rank::Zehn); // trump suit
let c2 = card(Suit::Gras, Rank::Koenig);
let c3 = card(Suit::Schell, Rank::Ass); // highest among suit trumps
let c4 = card(Suit::Eichel, Rank::Neun);
let winner = Gamemode::Solo(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // no O/U, Herz not present -> follow suit
assert_eq!(winner, &c3);
let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // only Gras becomes trump
assert_eq!(winner, &c2);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters -> follow suit
assert_eq!(winner, &c3);
let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell
assert_eq!(winner, &c3);
}
#[test]
fn wenz_unter_trumps_over_optional_suit_trump() {
// In Wenz with extra suit trump, Unters outrank any suit trumps
let c1 = card(Suit::Eichel, Rank::Ass); // led
let c2 = card(Suit::Gras, Rank::Koenig); // trump by suit (Gras) if chosen
let c3 = card(Suit::Schell, Rank::Unter); // trump by Unter (beats suit trumps)
let c4 = card(Suit::Gras, Rank::Ass); // trump by suit (Gras) if chosen
let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // only Unter trump
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit
assert_eq!(winner, &c1);
let winner = Gamemode::Geier(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras
assert_eq!(winner, &c4);
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> Unter wins
assert_eq!(winner, &c3);
}
#[test]
fn wenz_unter_precedence_between_suits() {
// Unter precedence: Eichel > Gras > Herz > Schell
let c1 = card(Suit::Herz, Rank::Neun); // led
let c2 = card(Suit::Gras, Rank::Unter);
let c3 = card(Suit::Eichel, Rank::Unter); // highest Unter
let c4 = card(Suit::Schell, Rank::Unter);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // Unters still outrank suit trumps
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit
assert_eq!(winner, &c1);
let winner = Gamemode::Geier(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras
assert_eq!(winner, &c2);
let winner = Gamemode::Sauspiel(Suit::Schell).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> highest Unter by suit
assert_eq!(winner, &c3);
}
#[test]
fn wenz_no_trump_led_suit_highest() {
// No Unters and no optional suit trumps: highest of led suit wins
let c1 = card(Suit::Eichel, Rank::Koenig); // led suit
let c2 = card(Suit::Gras, Rank::Ass);
let c3 = card(Suit::Eichel, Rank::Zehn); // higher than König
let c4 = card(Suit::Schell, Rank::Neun);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Wenz(Some(Suit::Gras)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Gras
assert_eq!(winner, &c2);
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // no Obers -> follow suit
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell
assert_eq!(winner, &c4);
let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Eichel suit trump
assert_eq!(winner, &c3);
}
#[test]
fn geier_ober_trumps_over_optional_suit_trump() {
// In Geier with extra suit trump, Obers outrank any suit trumps
let c1 = card(Suit::Gras, Rank::Ass); // led
let c2 = card(Suit::Schell, Rank::Koenig); // trump by suit (optional)
let c3 = card(Suit::Eichel, Rank::Ober); // trump by Ober (beats suit trumps)
let c4 = card(Suit::Schell, Rank::Ass); // trump by suit (optional)
let winner = Gamemode::Geier(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Geier(None).winning_card([&c1, &c2, &c3, &c4]); // Obers trump
assert_eq!(winner, &c3);
let winner = Gamemode::Wenz(None).winning_card([&c1, &c2, &c3, &c4]); // no Unters -> follow suit
assert_eq!(winner, &c1);
let winner = Gamemode::Wenz(Some(Suit::Schell)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Schell
assert_eq!(winner, &c4);
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // O/U trump -> Ober wins
assert_eq!(winner, &c3);
}
#[test]
fn bettel_behaves_like_herz_trump() {
// Current implementation treats Bettel like Herz-trump
let c1 = card(Suit::Gras, Rank::Ass); // led
let c2 = card(Suit::Herz, Rank::Neun); // trump by Herz suit
let c3 = card(Suit::Schell, Rank::Ass);
let c4 = card(Suit::Eichel, Rank::Koenig);
let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c2);
// More checks:
let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]); // same as Bettel currently
assert_eq!(winner, &c2);
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Herz trump
assert_eq!(winner, &c2);
let winner = Gamemode::Solo(Suit::Herz).winning_card([&c1, &c2, &c3, &c4]); // Herz trump in Solo
assert_eq!(winner, &c2);
let winner = Gamemode::Solo(Suit::Gras).winning_card([&c1, &c2, &c3, &c4]); // Gras trump; no O/U
assert_eq!(winner, &c1);
let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz
assert_eq!(winner, &c2);
let winner = Gamemode::Geier(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz
assert_eq!(winner, &c2);
}
#[test]
fn ramsch_behaves_like_herz_trump() {
// Current implementation treats Ramsch like Herz-trump
let c1 = card(Suit::Eichel, Rank::Ass); // led
let c2 = card(Suit::Schell, Rank::Koenig);
let c3 = card(Suit::Herz, Rank::Zehn); // trump by Herz suit
let c4 = card(Suit::Gras, Rank::Neun);
let winner = Gamemode::Ramsch.winning_card([&c1, &c2, &c3, &c4]);
assert_eq!(winner, &c3);
// More checks:
let winner = Gamemode::Bettel.winning_card([&c1, &c2, &c3, &c4]); // same as Ramsch currently
assert_eq!(winner, &c3);
let winner = Gamemode::Sauspiel(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Herz trump
assert_eq!(winner, &c3);
let winner = Gamemode::Solo(Suit::Herz).winning_card([&c1, &c2, &c3, &c4]); // Herz trump in Solo
assert_eq!(winner, &c3);
let winner = Gamemode::Solo(Suit::Eichel).winning_card([&c1, &c2, &c3, &c4]); // Eichel trump; no O/U
assert_eq!(winner, &c1);
let winner = Gamemode::Wenz(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz
assert_eq!(winner, &c3);
let winner = Gamemode::Geier(Some(Suit::Herz)).winning_card([&c1, &c2, &c3, &c4]); // suit trump Herz
assert_eq!(winner, &c3);
}

View File

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

View File

@@ -1,51 +1,335 @@
mod deck; use bevy::{
mod gamemode; 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 std::fmt::Debug;
use gamemode::Gamemode;
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<Image>,
layout: Handle<TextureAtlasLayout>,
}
impl SuitAtlas {
fn load(
asset_server: &AssetServer,
layouts: &mut Assets<TextureAtlasLayout>,
) -> Self {
let texture: Handle<Image> = 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<Card>,
}
#[derive(Component)]
struct PlayerCardVisual {
card: Card,
index: usize,
}
fn main() { 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<AssetServer>,
mut texture_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
commands.spawn(Camera2d);
let atlas = SuitAtlas::load(&asset_server, &mut texture_layouts);
commands.insert_resource(atlas);
let mut deck = Deck::new(); let mut deck = Deck::new();
deck.shuffle(); 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:"); let mut p1 = HumanPlayer::new(1, "Alice");
for card in hand1.iter() { let mut p2 = HumanPlayer::new(2, "Bob");
println!("{}", card) let mut p3 = HumanPlayer::new(3, "Clara");
let mut p4 = HumanPlayer::new(4, "Max");
p1.set_hand(hand1);
p2.set_hand(hand2);
p3.set_hand(hand3);
p4.set_hand(hand4);
let mode = Gamemode::Wenz(None);
commands.insert_resource(CurrentGamemode(mode));
commands.insert_resource(PlayerHandResource {
cards: p1.hand().clone(),
});
} }
println!("\nPlayer 2 has:"); fn spawn_player_hand(
for card in hand2.iter() { mut commands: Commands,
println!("{}", card) mut images: ResMut<Assets<Image>>,
atlas: Res<SuitAtlas>,
hand: Res<PlayerHandResource>,
) {
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<Card>) {
cards.sort_by(|a, b| a.suit.cmp(&b.suit).then(a.rank.cmp(&b.rank)));
} }
println!("\nPlayer 3 has:");
for card in hand3.iter() { fn create_card_texture(images: &mut Assets<Image>, card: &Card) -> Handle<Image> {
println!("{}", card) 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);
} }
println!("\nPlayer 4 has:"); draw_border(&mut pixels, [45, 45, 45, 255]);
for card in hand4.iter() {
println!("{}", card) 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)
} }
let mode = Gamemode::Solo(Suit::Gras); fn draw_border(pixels: &mut [u8], color: [u8; 4]) {
for x in 0..CARD_TEXTURE_WIDTH {
//let trick: [&Card; 4] = [&hand1[0], &hand2[0], &hand3[0], &hand4[0]]; set_pixel(pixels, x, 0, color);
let c1 = Card { suit: Suit::Schell, rank: Rank::Zehn }; set_pixel(pixels, x, CARD_TEXTURE_HEIGHT - 1, color);
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); for y in 0..CARD_TEXTURE_HEIGHT {
println!("\nWinning card: {}", winner); 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<Pointer<Over>>, 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<Pointer<Out>>, 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<Pointer<Press>>, Query<(&mut Sprite, &mut Transform)>) {
move |ev, cards| {
println!("Clicked on card: {:?}", card);
}
} }

View File

@@ -0,0 +1,175 @@
use std::io::{self, Write};
use crate::deck::Card;
use std::fmt;
#[derive(Debug)]
pub enum PlayerError {
NoCards,
}
impl fmt::Display for PlayerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PlayerError::NoCards => write!(f, "no cards available to play"),
}
}
}
impl std::error::Error for PlayerError {}
pub struct PlayerBase {
pub id: u32,
pub name: String,
}
impl PlayerBase {
pub fn new(id: u32, name: impl Into<String>) -> Self {
Self { id, name: name.into() }
}
pub fn id(&self) -> u32 {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = name.into();
}
pub fn play_card(&mut self, hand: &mut Vec<Card>) -> Result<Card, PlayerError> {
hand.pop().ok_or(PlayerError::NoCards)
}
}
pub trait PlayerBaseAccess {
fn base(&self) -> &PlayerBase;
fn base_mut(&mut self) -> &mut PlayerBase;
fn id(&self) -> u32 {
self.base().id()
}
fn name(&self) -> &str {
self.base().name()
}
fn set_name(&mut self, name: impl Into<String>) {
self.base_mut().set_name(name);
}
}
pub trait ExternalPlayer: PlayerBaseAccess {
fn play_card(&mut self, hand: &mut Vec<Card>) -> Result<Card, PlayerError> {
self.base_mut().play_card(hand)
}
}
pub trait InternalPlayer: PlayerBaseAccess {
fn play_card_from_hand(&mut self) -> Result<Card, PlayerError>;
fn receive_card(&mut self, card: Card);
fn set_hand(&mut self, hand: Vec<Card>);
fn hand(&self) -> &Vec<Card>;
}
pub struct HumanPlayer {
pub base: PlayerBase,
pub hand: Vec<Card>,
}
impl HumanPlayer {
pub fn new(id: u32, name: impl Into<String>) -> Self {
Self {
base: PlayerBase::new(id, name),
hand: Vec::with_capacity(8),
}
}
}
impl PlayerBaseAccess for HumanPlayer {
fn base(&self) -> &PlayerBase {
&self.base
}
fn base_mut(&mut self) -> &mut PlayerBase {
&mut self.base
}
}
impl InternalPlayer for HumanPlayer {
fn play_card_from_hand(&mut self) -> Result<Card, PlayerError> {
if self.hand.is_empty() {
return Err(PlayerError::NoCards);
}
println!("{}'s hand:", self.name());
for (i, c) in self.hand.iter().enumerate() {
println!(" {}: {}", i, c);
}
print!("Select card index to play: ");
let _ = io::stdout().flush();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
if let Ok(idx) = input.trim().parse::<usize>() {
if idx < self.hand.len() {
return Ok(self.hand.remove(idx));
}
}
}
// fallback: pop last
self.hand.pop().ok_or(PlayerError::NoCards)
}
fn receive_card(&mut self, card: Card) {
self.hand.push(card);
}
fn set_hand(&mut self, hand: Vec<Card>) {
self.hand = hand;
}
fn hand(&self) -> &Vec<Card> {
&self.hand
}
}
pub struct NpcPlayer {
pub base: PlayerBase,
pub hand: Vec<Card>,
}
impl NpcPlayer {
pub fn new(id: u32, name: impl Into<String>) -> Self {
Self { base: PlayerBase::new(id, name), hand: Vec::with_capacity(8) }
}
}
impl PlayerBaseAccess for NpcPlayer {
fn base(&self) -> &PlayerBase { &self.base }
fn base_mut(&mut self) -> &mut PlayerBase { &mut self.base }
}
impl InternalPlayer for NpcPlayer {
fn play_card_from_hand(&mut self) -> Result<Card, PlayerError> {
if self.hand.is_empty() {
Err(PlayerError::NoCards)
} else {
Ok(self.hand.remove(0))
}
}
fn receive_card(&mut self, card: Card) {
self.hand.push(card);
}
fn set_hand(&mut self, hand: Vec<Card>) {
self.hand = hand;
}
fn hand(&self) -> &Vec<Card> {
&self.hand
}
}