mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-11 09:59:33 +01:00
initialize bevy game
This commit is contained in:
3
schafkopf-logic/.gitignore
vendored
3
schafkopf-logic/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
target
|
||||
Cargo.lock
|
||||
shell.nix
|
||||
shell.nix
|
||||
dist
|
||||
@@ -6,4 +6,9 @@ edition = "2024"
|
||||
[dependencies]
|
||||
strum = "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"] }
|
||||
7
schafkopf-logic/Trunk.toml
Normal file
7
schafkopf-logic/Trunk.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[build]
|
||||
dist = "dist"
|
||||
release = true
|
||||
|
||||
[serve]
|
||||
open = false
|
||||
port = 8080
|
||||
BIN
schafkopf-logic/assets/symbole.png
Normal file
BIN
schafkopf-logic/assets/symbole.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
11
schafkopf-logic/index.html
Normal file
11
schafkopf-logic/index.html
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
125
schafkopf-logic/src/gamemode/mod.rs
Normal file
125
schafkopf-logic/src/gamemode/mod.rs
Normal 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;
|
||||
313
schafkopf-logic/src/gamemode/tests.rs
Normal file
313
schafkopf-logic/src/gamemode/tests.rs
Normal 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);
|
||||
}
|
||||
3
schafkopf-logic/src/lib.rs
Normal file
3
schafkopf-logic/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod deck;
|
||||
pub mod gamemode;
|
||||
pub mod player;
|
||||
@@ -1,51 +1,335 @@
|
||||
mod deck;
|
||||
mod gamemode;
|
||||
use bevy::{
|
||||
asset::{AssetMetaCheck, AssetPlugin, RenderAssetUsages},
|
||||
prelude::*,
|
||||
sprite::Anchor,
|
||||
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
};
|
||||
use schafkopf_logic::{
|
||||
deck::{Card, Deck, Rank, Suit},
|
||||
gamemode::Gamemode,
|
||||
player::{HumanPlayer, InternalPlayer},
|
||||
};
|
||||
|
||||
use deck::{Deck, Suit, Card, Rank};
|
||||
use gamemode::Gamemode;
|
||||
use std::fmt::Debug;
|
||||
|
||||
|
||||
const CARD_TEXTURE_WIDTH: usize = 96;
|
||||
const CARD_TEXTURE_HEIGHT: usize = 135;
|
||||
const CARD_WORLD_SIZE: Vec2 = Vec2::new(96.0, 135.0);
|
||||
const ICON_OFFSET_TL: Vec2 = Vec2::new(-CARD_WORLD_SIZE.x * 0.5 + 16.0, CARD_WORLD_SIZE.y * 0.5 - 20.0);
|
||||
const ICON_OFFSET_BR: Vec2 = Vec2::new( CARD_WORLD_SIZE.x * 0.5 - 16.0, -CARD_WORLD_SIZE.y * 0.5 + 20.0);
|
||||
const GLYPH_WIDTH: usize = 5;
|
||||
const GLYPH_HEIGHT: usize = 7;
|
||||
const GLYPH_STRIDE: usize = 6;
|
||||
const SUIT_ICON_PX: usize = 32;
|
||||
const LABEL_MARGIN_X: usize = 14;
|
||||
const LABEL_MARGIN_Y: usize = 8;
|
||||
const LABEL_TEXT_GAP: usize = 4;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct CurrentGamemode(Gamemode);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct SuitAtlas {
|
||||
texture: Handle<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() {
|
||||
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();
|
||||
deck.shuffle();
|
||||
let [mut hand1, mut hand2, mut hand3, mut hand4] =
|
||||
deck.deal_4x8().expect("expected a full deck to deal four hands");
|
||||
|
||||
let [mut hand1, mut hand2, mut hand3, mut hand4] = deck.deal_4x8().unwrap();
|
||||
sort_cards(&mut hand1);
|
||||
|
||||
println!("Player 1 has:");
|
||||
for card in hand1.iter() {
|
||||
println!("{}", card)
|
||||
}
|
||||
let mut p1 = HumanPlayer::new(1, "Alice");
|
||||
let mut p2 = HumanPlayer::new(2, "Bob");
|
||||
let mut p3 = HumanPlayer::new(3, "Clara");
|
||||
let mut p4 = HumanPlayer::new(4, "Max");
|
||||
|
||||
println!("\nPlayer 2 has:");
|
||||
for card in hand2.iter() {
|
||||
println!("{}", card)
|
||||
}
|
||||
p1.set_hand(hand1);
|
||||
p2.set_hand(hand2);
|
||||
p3.set_hand(hand3);
|
||||
p4.set_hand(hand4);
|
||||
|
||||
println!("\nPlayer 3 has:");
|
||||
for card in hand3.iter() {
|
||||
println!("{}", card)
|
||||
}
|
||||
let mode = Gamemode::Wenz(None);
|
||||
commands.insert_resource(CurrentGamemode(mode));
|
||||
|
||||
println!("\nPlayer 4 has:");
|
||||
for card in hand4.iter() {
|
||||
println!("{}", card)
|
||||
}
|
||||
|
||||
let mode = Gamemode::Solo(Suit::Gras);
|
||||
|
||||
//let trick: [&Card; 4] = [&hand1[0], &hand2[0], &hand3[0], &hand4[0]];
|
||||
let c1 = Card { suit: Suit::Schell, rank: Rank::Zehn };
|
||||
let c2 = Card { suit: Suit::Schell, rank: Rank::Ass };
|
||||
let c3 = Card { suit: Suit::Schell, rank: Rank::Unter };
|
||||
let c4 = Card { suit: Suit::Gras, rank: Rank::Sieben };
|
||||
|
||||
let trick: [&Card; 4] = [&c1, &c2, &c3, &c4];
|
||||
|
||||
|
||||
for card in trick.iter() {
|
||||
println!("{}", card)
|
||||
}
|
||||
println!("\n");
|
||||
|
||||
let winner = mode.winning_card(trick);
|
||||
println!("\nWinning card: {}", winner);
|
||||
commands.insert_resource(PlayerHandResource {
|
||||
cards: p1.hand().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_player_hand(
|
||||
mut commands: Commands,
|
||||
mut images: ResMut<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)));
|
||||
}
|
||||
|
||||
|
||||
fn create_card_texture(images: &mut Assets<Image>, card: &Card) -> Handle<Image> {
|
||||
let mut pixels = vec![0u8; CARD_TEXTURE_WIDTH * CARD_TEXTURE_HEIGHT * 4];
|
||||
|
||||
let background = suit_background(card.suit);
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk.copy_from_slice(&background);
|
||||
}
|
||||
|
||||
draw_border(&mut pixels, [45, 45, 45, 255]);
|
||||
|
||||
let rank_text = rank_label(card.rank);
|
||||
|
||||
let ink = [15, 15, 15, 255];
|
||||
let rank_text_len = rank_text.chars().count();
|
||||
let rank_text_width = if rank_text_len == 0 {
|
||||
0
|
||||
} else {
|
||||
(rank_text_len - 1) * GLYPH_STRIDE + GLYPH_WIDTH
|
||||
};
|
||||
|
||||
let top_label_x = LABEL_MARGIN_X;
|
||||
let top_label_y = LABEL_MARGIN_Y + SUIT_ICON_PX + LABEL_TEXT_GAP;
|
||||
let bottom_label_x = CARD_TEXTURE_WIDTH.saturating_sub(LABEL_MARGIN_X + rank_text_width);
|
||||
let bottom_label_y = CARD_TEXTURE_HEIGHT
|
||||
.saturating_sub(LABEL_MARGIN_Y + SUIT_ICON_PX + LABEL_TEXT_GAP + GLYPH_HEIGHT);
|
||||
|
||||
draw_text(&mut pixels, top_label_x, top_label_y, rank_text, ink);
|
||||
draw_text(&mut pixels, bottom_label_x, bottom_label_y, rank_text, ink);
|
||||
|
||||
let extent = Extent3d {
|
||||
width: CARD_TEXTURE_WIDTH as u32,
|
||||
height: CARD_TEXTURE_HEIGHT as u32,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let image = Image::new_fill(
|
||||
extent,
|
||||
TextureDimension::D2,
|
||||
&pixels,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetUsages::default(),
|
||||
);
|
||||
|
||||
images.add(image)
|
||||
}
|
||||
|
||||
fn draw_border(pixels: &mut [u8], color: [u8; 4]) {
|
||||
for x in 0..CARD_TEXTURE_WIDTH {
|
||||
set_pixel(pixels, x, 0, color);
|
||||
set_pixel(pixels, x, CARD_TEXTURE_HEIGHT - 1, color);
|
||||
}
|
||||
|
||||
for y in 0..CARD_TEXTURE_HEIGHT {
|
||||
set_pixel(pixels, 0, y, color);
|
||||
set_pixel(pixels, CARD_TEXTURE_WIDTH - 1, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(pixels: &mut [u8], start_x: usize, start_y: usize, text: &str, color: [u8; 4]) {
|
||||
let mut x = start_x;
|
||||
for ch in text.chars() {
|
||||
if let Some(bitmap) = glyph_bitmap(ch) {
|
||||
draw_glyph(pixels, x, start_y, bitmap, color);
|
||||
}
|
||||
x += GLYPH_STRIDE;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_glyph(pixels: &mut [u8], start_x: usize, start_y: usize, glyph: [u8; 7], color: [u8; 4]) {
|
||||
for (row, pattern) in glyph.iter().enumerate() {
|
||||
for col in 0..5 {
|
||||
if (pattern >> (4 - col)) & 1 == 1 {
|
||||
set_pixel(pixels, start_x + col, start_y + row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_pixel(pixels: &mut [u8], x: usize, y: usize, color: [u8; 4]) {
|
||||
if x >= CARD_TEXTURE_WIDTH || y >= CARD_TEXTURE_HEIGHT {
|
||||
return;
|
||||
}
|
||||
let index = (y * CARD_TEXTURE_WIDTH + x) * 4;
|
||||
pixels[index..index + 4].copy_from_slice(&color);
|
||||
}
|
||||
|
||||
fn glyph_bitmap(ch: char) -> Option<[u8; 7]> {
|
||||
match ch {
|
||||
'0' => Some([0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110]),
|
||||
'1' => Some([0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110]),
|
||||
'7' => Some([0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000]),
|
||||
'8' => Some([0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110]),
|
||||
'9' => Some([0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b11100]),
|
||||
'A' => Some([0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001]),
|
||||
'K' => Some([0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001]),
|
||||
'O' => Some([0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110]),
|
||||
'U' => Some([0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn rank_label(rank: Rank) -> &'static str {
|
||||
match rank {
|
||||
Rank::Ass => "A",
|
||||
Rank::Zehn => "10",
|
||||
Rank::Koenig => "K",
|
||||
Rank::Ober => "O",
|
||||
Rank::Unter => "U",
|
||||
Rank::Neun => "9",
|
||||
Rank::Acht => "8",
|
||||
Rank::Sieben => "7",
|
||||
}
|
||||
}
|
||||
|
||||
fn suit_background(suit: Suit) -> [u8; 4] {
|
||||
match suit {
|
||||
Suit::Eichel => [245, 235, 220, 255],
|
||||
Suit::Gras => [225, 245, 225, 255],
|
||||
Suit::Herz => [245, 225, 225, 255],
|
||||
Suit::Schell => [245, 240, 210, 255],
|
||||
}
|
||||
}
|
||||
|
||||
fn suit_color(suit: Suit) -> [u8; 4] {
|
||||
match suit {
|
||||
Suit::Eichel => [131, 100, 56, 255],
|
||||
Suit::Gras => [62, 120, 54, 255],
|
||||
Suit::Herz => [170, 40, 60, 255],
|
||||
Suit::Schell => [204, 142, 30, 255],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover() -> impl Fn(On<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);
|
||||
}
|
||||
}
|
||||
175
schafkopf-logic/src/player/mod.rs
Normal file
175
schafkopf-logic/src/player/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user