initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+6128
File diff suppressed because it is too large
Load Diff
+17
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "audiopoker"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.19", features = ["webgl2"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
web-sys = { version = "0.3", features = ["SpeechRecognition", "SpeechSynthesis", "SpeechSynthesisUtterance", "Window", "HtmlInputElement", "console"] }
|
||||||
|
js-sys = "0.3"
|
||||||
|
futures = "0.3"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[target.wasm]
|
||||||
|
edition = 2024
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Projektplan: Audiopoker (Bevy + Wasm)
|
||||||
|
|
||||||
|
## Zielsetzung
|
||||||
|
Ein barrierefreies Multiplayer-Pokergame für blinde Spieler, das primär über Audio (TTS & SFX) bedient wird. Das Spiel wird via Webbrowser aufgerufen und auf WebAssembly kompiliert.
|
||||||
|
|
||||||
|
## Technologie-Stack
|
||||||
|
- **Engine:** Bevy (Rust)
|
||||||
|
- **Target:** WebAssembly (Wasm)
|
||||||
|
- **Networking:** WebSockets für die Synchronisation des Spielzustands.
|
||||||
|
- **Audio-Output:** Web Speech API (via JS-Bindings) für Text-to-Speech, `bevy_audio` für Soundeffekte.
|
||||||
|
- **Input:** Tastatur-zentrierte Steuerung (Tab/Space/Enter).
|
||||||
|
|
||||||
|
## Modul-Struktur
|
||||||
|
- `core::logic`: Reine Poker-Logik (Deck, Hand-Evaluation, Betting) ohne grafische Abhängigkeiten.
|
||||||
|
- `core::network`: Netzwerkprotokoll und WebSocket-Handhabung für Multiplayer-Synchronisation.
|
||||||
|
- `audio::tts`: Interface zur Web Speech API für dynamisches Sprechen von Spielereignissen.
|
||||||
|
- `audio::sfx`: Sound-Manager für atmosphärische Effekte (Chips, Karten mischen).
|
||||||
|
- `ui_minimal`: Minimalistische Darstellung, optimiert für Barrierefreiheit und geringen Ressourcenverbrauch.
|
||||||
|
|
||||||
|
## Implementierungsphasen
|
||||||
|
|
||||||
|
### Phase 1: Fundament & Core Logic (MVP)
|
||||||
|
- [x] Projektstruktur aufsetzen (`Cargo.toml`; Modul-Struktur `core`/`audio` bereinigt und verdrahtet).
|
||||||
|
- [x] **Poker Engine**: Hand-Rankings (inkl. Wheel-Straight, 7-Karten-Bestbewertung), Deck-Shuffling/Draw und einfache Betting-Logik (Fold/Check/Call/Raise, All-In) implementiert und getestet.
|
||||||
|
- [x] **Audio-Grundlagen**: `audio::tts` ruft die Web Speech API korrekt an (`SpeechSynthesisUtterance`); auf nativen Targets Dummy-Ausgabe für Tests.
|
||||||
|
- [ ] Bevy-`App`-Grundgerüst (Plugins/Systems) aufsetzen – noch offen, da die Bevy-0.19-API in dieser Umgebung nicht gegen einen echten Build geprüft werden konnte (siehe Hinweis in `main.rs`).
|
||||||
|
|
||||||
|
### Phase 2: Networking & Multiplayer
|
||||||
|
- [ ] Server-Architektur für Spielräume und Spielerverwaltung aufbauen.
|
||||||
|
- [ ] Netzwerkprotokoll für Aktionen (Deal, Bet, Fold) definieren.
|
||||||
|
- [ ] Multiplayer-Lobby und Tischsuche implementieren.
|
||||||
|
|
||||||
|
### Phase 3: Audio Experience & UI
|
||||||
|
- [ ] TTS-Logik verfeinern (Kontextuelle Ausgaben).
|
||||||
|
- [ ] Soundeffekte einbinden.
|
||||||
|
- [ ] Minimalistisches, barrierefreies User Interface erstellen.
|
||||||
|
|
||||||
|
### Phase 4: Polishing & Testing
|
||||||
|
- [ ] Barrierefreiheits-Audit (Tastatur-Flow ohne Maus).
|
||||||
|
- [ ] Latenztests im Multiplayer.
|
||||||
|
- [ ] WebAssembly Deployment Vorbereitung.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
pub mod tts;
|
||||||
|
pub mod sfx;
|
||||||
|
|
||||||
|
pub struct AudioEngine {
|
||||||
|
pub tts: tts::TtsEngine,
|
||||||
|
pub sfx: sfx::SfxEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tts: tts::TtsEngine::new(),
|
||||||
|
sfx: sfx::SfxEngine::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
pub struct SfxEngine;
|
||||||
|
|
||||||
|
impl SfxEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_sound(&self, effect_id: &str) {
|
||||||
|
// TODO(Phase 3): echte Wiedergabe via bevy_audio; bislang nur Log-Ausgabe.
|
||||||
|
println!("Playing SFX: {}", effect_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! Interface zur Web Speech API für dynamisches Sprechen von Spielereignissen
|
||||||
|
//! (siehe PLAN.md, `audio::tts`). Auf nicht-Wasm-Targets (native Builds,
|
||||||
|
//! Tests) wird stattdessen auf stdout ausgegeben.
|
||||||
|
|
||||||
|
pub struct TtsEngine;
|
||||||
|
|
||||||
|
impl TtsEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn speak(&self, text: &str) {
|
||||||
|
use web_sys::SpeechSynthesisUtterance;
|
||||||
|
|
||||||
|
let Some(window) = web_sys::window() else {
|
||||||
|
web_sys::console::warn_1(&"Kein globales `window` für TTS gefunden.".into());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let speech_synth = window.speech_synthesis().expect("Web Speech API nicht verfügbar");
|
||||||
|
let utterance = SpeechSynthesisUtterance::new_with_text(text)
|
||||||
|
.expect("SpeechSynthesisUtterance konnte nicht erstellt werden");
|
||||||
|
speech_synth.speak(&utterance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn speak(&self, text: &str) {
|
||||||
|
println!("TTS (Dummy, natives Target): {}", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//! Spielzustand und Betting-Logik (siehe PLAN.md, Phase 1: "Betting-Logik").
|
||||||
|
|
||||||
|
use crate::core::logic::card::Card;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Round {
|
||||||
|
PreFlop,
|
||||||
|
Flop,
|
||||||
|
Turn,
|
||||||
|
River,
|
||||||
|
Showdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PlayerStatus {
|
||||||
|
Active,
|
||||||
|
Folded,
|
||||||
|
AllIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Player {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub stack: u64,
|
||||||
|
pub hole_cards: Vec<Card>,
|
||||||
|
pub current_bet: u64,
|
||||||
|
pub status: PlayerStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
/// Startguthaben ist vorerst fest verdrahtet; sollte später
|
||||||
|
/// über die Tisch-/Lobby-Konfiguration (Phase 2) einstellbar sein.
|
||||||
|
const STARTING_STACK: u64 = 1000;
|
||||||
|
|
||||||
|
pub fn new(id: u32, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
stack: Self::STARTING_STACK,
|
||||||
|
hole_cards: Vec::new(),
|
||||||
|
current_bet: 0,
|
||||||
|
status: PlayerStatus::Active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fold(&mut self) {
|
||||||
|
self.status = PlayerStatus::Folded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt `amount`, gedeckelt durch den verbleibenden Stack.
|
||||||
|
/// Gibt den tatsächlich gesetzten Betrag zurück (relevant bei All-In).
|
||||||
|
pub fn bet(&mut self, amount: u64) -> u64 {
|
||||||
|
let actual = amount.min(self.stack);
|
||||||
|
self.stack -= actual;
|
||||||
|
self.current_bet += actual;
|
||||||
|
if self.stack == 0 {
|
||||||
|
self.status = PlayerStatus::AllIn;
|
||||||
|
}
|
||||||
|
actual
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GameState {
|
||||||
|
pub players: Vec<Player>,
|
||||||
|
pub pot: u64,
|
||||||
|
pub dealer_idx: usize,
|
||||||
|
pub community_cards: Vec<Card>,
|
||||||
|
pub current_round: Round,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameState {
|
||||||
|
/// Anzahl der Spieler, die weder gefoldet noch (endgültig) ausgeschieden sind.
|
||||||
|
pub fn active_player_count(&self) -> usize {
|
||||||
|
self.players
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.status != PlayerStatus::Folded)
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wett-Aktionen, wie sie über `core::network` vom Client kommen (Deal/Bet/Fold, siehe PLAN.md).
|
||||||
|
pub enum BettingAction {
|
||||||
|
Fold,
|
||||||
|
Check,
|
||||||
|
Call(u64),
|
||||||
|
Raise(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GameManager;
|
||||||
|
|
||||||
|
impl GameManager {
|
||||||
|
pub fn start_new_game(player_names: Vec<String>) -> GameState {
|
||||||
|
let players = player_names
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, name)| Player::new(i as u32, &name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
GameState {
|
||||||
|
players,
|
||||||
|
pot: 0,
|
||||||
|
dealer_idx: 0,
|
||||||
|
community_cards: Vec::new(),
|
||||||
|
current_round: Round::PreFlop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wendet die Aktion eines Spielers auf den Spielzustand an und
|
||||||
|
/// aktualisiert den Pot entsprechend.
|
||||||
|
pub fn apply_action(state: &mut GameState, player_idx: usize, action: BettingAction) {
|
||||||
|
let player = &mut state.players[player_idx];
|
||||||
|
match action {
|
||||||
|
BettingAction::Fold => player.fold(),
|
||||||
|
BettingAction::Check => {}
|
||||||
|
BettingAction::Call(amount) | BettingAction::Raise(amount) => {
|
||||||
|
let actual = player.bet(amount);
|
||||||
|
state.pot += actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schaltet in die nächste Wettrunde (Pre-Flop -> Flop -> Turn -> River -> Showdown).
|
||||||
|
pub fn advance_round(state: &mut GameState) {
|
||||||
|
state.current_round = match state.current_round {
|
||||||
|
Round::PreFlop => Round::Flop,
|
||||||
|
Round::Flop => Round::Turn,
|
||||||
|
Round::Turn => Round::River,
|
||||||
|
Round::River => Round::Showdown,
|
||||||
|
Round::Showdown => Round::Showdown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startet_spiel_mit_korrekten_startwerten() {
|
||||||
|
let state = GameManager::start_new_game(vec!["Anna".into(), "Ben".into()]);
|
||||||
|
assert_eq!(state.players.len(), 2);
|
||||||
|
assert_eq!(state.pot, 0);
|
||||||
|
assert_eq!(state.current_round, Round::PreFlop);
|
||||||
|
assert!(state.players.iter().all(|p| p.stack == Player::STARTING_STACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fold_reduziert_aktive_spielerzahl() {
|
||||||
|
let mut state = GameManager::start_new_game(vec!["Anna".into(), "Ben".into()]);
|
||||||
|
GameManager::apply_action(&mut state, 0, BettingAction::Fold);
|
||||||
|
assert_eq!(state.active_player_count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bet_erhoeht_pot_und_reduziert_stack() {
|
||||||
|
let mut state = GameManager::start_new_game(vec!["Anna".into(), "Ben".into()]);
|
||||||
|
GameManager::apply_action(&mut state, 0, BettingAction::Raise(100));
|
||||||
|
assert_eq!(state.pot, 100);
|
||||||
|
assert_eq!(state.players[0].stack, Player::STARTING_STACK - 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_in_deckelt_den_einsatz_auf_den_verbleibenden_stack() {
|
||||||
|
let mut player = Player::new(0, "Anna");
|
||||||
|
let actual = player.bet(10_000); // mehr als der Stack
|
||||||
|
assert_eq!(actual, Player::STARTING_STACK);
|
||||||
|
assert_eq!(player.stack, 0);
|
||||||
|
assert_eq!(player.status, PlayerStatus::AllIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Suit {
|
||||||
|
Spades,
|
||||||
|
Hearts,
|
||||||
|
Diamonds,
|
||||||
|
Clubs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Rank(pub u8); // 2-14 (11=J, 12=Q, 13=K, 14=A)
|
||||||
|
|
||||||
|
impl Rank {
|
||||||
|
pub fn new(value: u8) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Card {
|
||||||
|
pub suit: Suit,
|
||||||
|
pub rank: Rank,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
pub fn new(suit: Suit, rank_val: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
suit,
|
||||||
|
rank: Rank::new(rank_val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::core::logic::card::Card;
|
||||||
|
|
||||||
|
/// Kategorie einer Poker-Hand, aufsteigend nach Stärke sortiert.
|
||||||
|
/// Die Reihenfolge der Varianten wird für den Vergleich (`Ord`) genutzt.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum HandCategory {
|
||||||
|
HighCard,
|
||||||
|
Pair,
|
||||||
|
TwoPair,
|
||||||
|
ThreeOfAKind,
|
||||||
|
Straight,
|
||||||
|
Flush,
|
||||||
|
FullHouse,
|
||||||
|
FourOfAKind,
|
||||||
|
StraightFlush,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vollständig vergleichbare Bewertung einer 5-Karten-Hand:
|
||||||
|
/// zuerst die Kategorie, danach die Kicker/Tiebreaker in absteigender
|
||||||
|
/// Relevanz. Zwei `HandScore`-Werte lassen sich direkt per `>`/`<` vergleichen.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct HandScore {
|
||||||
|
pub category: HandCategory,
|
||||||
|
pub tiebreakers: [u8; 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Hand {
|
||||||
|
pub cards: Vec<Card>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hand {
|
||||||
|
pub fn new(cards: Vec<Card>) -> Self {
|
||||||
|
Self { cards }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bewertet die Hand und gibt die bestmögliche 5-Karten-Kombination
|
||||||
|
/// als `HandScore` zurück. Unterstützt 5 bis 7 Karten (z. B. 2 Hole
|
||||||
|
/// Cards + bis zu 5 Community Cards beim Texas Hold'em).
|
||||||
|
pub fn evaluate(&self) -> HandScore {
|
||||||
|
assert!(
|
||||||
|
self.cards.len() >= 5,
|
||||||
|
"Zur Bewertung werden mindestens 5 Karten benötigt"
|
||||||
|
);
|
||||||
|
|
||||||
|
combinations_of_5(&self.cards)
|
||||||
|
.into_iter()
|
||||||
|
.map(|combo| score_five(&combo))
|
||||||
|
.max()
|
||||||
|
.expect("mindestens eine 5-Karten-Kombination muss existieren")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erzeugt alle 5-elementigen Teilmengen der übergebenen Karten
|
||||||
|
/// (bei genau 5 Karten trivial, bei 6/7 Karten alle C(n,5) Kombinationen).
|
||||||
|
fn combinations_of_5(cards: &[Card]) -> Vec<[Card; 5]> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut indices = [0usize; 5];
|
||||||
|
|
||||||
|
fn recurse(
|
||||||
|
cards: &[Card],
|
||||||
|
start: usize,
|
||||||
|
depth: usize,
|
||||||
|
indices: &mut [usize; 5],
|
||||||
|
result: &mut Vec<[Card; 5]>,
|
||||||
|
) {
|
||||||
|
if depth == 5 {
|
||||||
|
result.push([
|
||||||
|
cards[indices[0]],
|
||||||
|
cards[indices[1]],
|
||||||
|
cards[indices[2]],
|
||||||
|
cards[indices[3]],
|
||||||
|
cards[indices[4]],
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in start..cards.len() {
|
||||||
|
indices[depth] = i;
|
||||||
|
recurse(cards, i + 1, depth + 1, indices, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurse(cards, 0, 0, &mut indices, &mut result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bewertet genau 5 Karten nach den Standard-Poker-Regeln.
|
||||||
|
fn score_five(cards: &[Card; 5]) -> HandScore {
|
||||||
|
let mut ranks: Vec<u8> = cards.iter().map(|c| c.rank.0).collect();
|
||||||
|
ranks.sort_unstable_by(|a, b| b.cmp(a)); // absteigend
|
||||||
|
|
||||||
|
let is_flush = cards.iter().all(|c| c.suit == cards[0].suit);
|
||||||
|
|
||||||
|
let mut unique_ranks = ranks.clone();
|
||||||
|
unique_ranks.dedup();
|
||||||
|
|
||||||
|
// Straße A-2-3-4-5 ("Wheel") ist ein Sonderfall: Ass zählt hier als 1.
|
||||||
|
let is_wheel = unique_ranks == vec![14, 5, 4, 3, 2];
|
||||||
|
let is_straight_normal =
|
||||||
|
unique_ranks.len() == 5 && (unique_ranks[0] - unique_ranks[4] == 4);
|
||||||
|
let is_straight = is_straight_normal || is_wheel;
|
||||||
|
let straight_high = if is_wheel { 5 } else { *unique_ranks.first().unwrap_or(&0) };
|
||||||
|
|
||||||
|
// Häufigkeiten pro Rang zählen, dann nach (Anzahl, Rang) absteigend sortieren,
|
||||||
|
// damit z. B. bei Full House das Drilling-Rank vor dem Paar-Rank steht.
|
||||||
|
let mut counts: HashMap<u8, u8> = HashMap::new();
|
||||||
|
for &r in &ranks {
|
||||||
|
*counts.entry(r).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
let mut count_groups: Vec<(u8, u8)> = counts.into_iter().collect(); // (rang, anzahl)
|
||||||
|
count_groups.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(b.0.cmp(&a.0)));
|
||||||
|
|
||||||
|
let category = if is_straight && is_flush {
|
||||||
|
HandCategory::StraightFlush
|
||||||
|
} else if count_groups[0].1 == 4 {
|
||||||
|
HandCategory::FourOfAKind
|
||||||
|
} else if count_groups[0].1 == 3 && count_groups.get(1).map_or(false, |g| g.1 == 2) {
|
||||||
|
HandCategory::FullHouse
|
||||||
|
} else if is_flush {
|
||||||
|
HandCategory::Flush
|
||||||
|
} else if is_straight {
|
||||||
|
HandCategory::Straight
|
||||||
|
} else if count_groups[0].1 == 3 {
|
||||||
|
HandCategory::ThreeOfAKind
|
||||||
|
} else if count_groups[0].1 == 2 && count_groups.get(1).map_or(false, |g| g.1 == 2) {
|
||||||
|
HandCategory::TwoPair
|
||||||
|
} else if count_groups[0].1 == 2 {
|
||||||
|
HandCategory::Pair
|
||||||
|
} else {
|
||||||
|
HandCategory::HighCard
|
||||||
|
};
|
||||||
|
|
||||||
|
let tiebreakers: [u8; 5] = match category {
|
||||||
|
HandCategory::StraightFlush | HandCategory::Straight => [straight_high, 0, 0, 0, 0],
|
||||||
|
_ => {
|
||||||
|
let mut tb = [0u8; 5];
|
||||||
|
for (i, (rank, _)) in count_groups.iter().enumerate().take(5) {
|
||||||
|
tb[i] = *rank;
|
||||||
|
}
|
||||||
|
tb
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HandScore { category, tiebreakers }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::logic::card::Suit;
|
||||||
|
|
||||||
|
fn c(suit: Suit, rank: u8) -> Card {
|
||||||
|
Card::new(suit, rank)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn erkennt_flush() {
|
||||||
|
let hand = Hand::new(vec![
|
||||||
|
c(Suit::Hearts, 2),
|
||||||
|
c(Suit::Hearts, 5),
|
||||||
|
c(Suit::Hearts, 9),
|
||||||
|
c(Suit::Hearts, 11),
|
||||||
|
c(Suit::Hearts, 13),
|
||||||
|
]);
|
||||||
|
assert_eq!(hand.evaluate().category, HandCategory::Flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn erkennt_straight_inklusive_wheel() {
|
||||||
|
let hand = Hand::new(vec![
|
||||||
|
c(Suit::Hearts, 14),
|
||||||
|
c(Suit::Spades, 2),
|
||||||
|
c(Suit::Clubs, 3),
|
||||||
|
c(Suit::Diamonds, 4),
|
||||||
|
c(Suit::Hearts, 5),
|
||||||
|
]);
|
||||||
|
let score = hand.evaluate();
|
||||||
|
assert_eq!(score.category, HandCategory::Straight);
|
||||||
|
assert_eq!(score.tiebreakers[0], 5); // Wheel: Ass zählt als 1, höchste Karte ist 5
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_house_schlaegt_flush() {
|
||||||
|
let full_house = Hand::new(vec![
|
||||||
|
c(Suit::Hearts, 7),
|
||||||
|
c(Suit::Spades, 7),
|
||||||
|
c(Suit::Clubs, 7),
|
||||||
|
c(Suit::Diamonds, 3),
|
||||||
|
c(Suit::Hearts, 3),
|
||||||
|
]);
|
||||||
|
let flush = Hand::new(vec![
|
||||||
|
c(Suit::Hearts, 2),
|
||||||
|
c(Suit::Hearts, 5),
|
||||||
|
c(Suit::Hearts, 9),
|
||||||
|
c(Suit::Hearts, 11),
|
||||||
|
c(Suit::Hearts, 13),
|
||||||
|
]);
|
||||||
|
assert!(full_house.evaluate() > flush.evaluate());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn waehlt_beste_kombination_aus_sieben_karten() {
|
||||||
|
// 2 Hole Cards + 5 Community Cards, enthält ein verstecktes Vierling.
|
||||||
|
let hand = Hand::new(vec![
|
||||||
|
c(Suit::Hearts, 9),
|
||||||
|
c(Suit::Spades, 9),
|
||||||
|
c(Suit::Clubs, 9),
|
||||||
|
c(Suit::Diamonds, 9),
|
||||||
|
c(Suit::Hearts, 2),
|
||||||
|
c(Suit::Spades, 5),
|
||||||
|
c(Suit::Clubs, 13),
|
||||||
|
]);
|
||||||
|
assert_eq!(hand.evaluate().category, HandCategory::FourOfAKind);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pub mod card;
|
||||||
|
pub mod hand;
|
||||||
|
|
||||||
|
use card::Card;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::thread_rng;
|
||||||
|
|
||||||
|
pub struct Deck {
|
||||||
|
pub cards: Vec<Card>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deck {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut cards = Vec::new();
|
||||||
|
let suits = [card::Suit::Spades, card::Suit::Hearts, card::Suit::Diamonds, card::Suit::Clubs];
|
||||||
|
for suit in suits.iter() {
|
||||||
|
for rank in 2..=14 {
|
||||||
|
cards.push(Card::new(*suit, rank));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { cards }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shuffle(&mut self) {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
self.cards.shuffle(&mut rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zieht eine einzelne Karte vom Ende des (gemischten) Decks.
|
||||||
|
pub fn draw(&mut self) -> Option<Card> {
|
||||||
|
self.cards.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zieht `count` Karten. Ist das Deck vorher erschöpft, wird
|
||||||
|
/// entsprechend weniger Karten zurückgegeben.
|
||||||
|
pub fn deal_hand(&mut self, count: usize) -> Vec<Card> {
|
||||||
|
(0..count).filter_map(|_| self.draw()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! Kern-Logik des Spiels, unabhängig von Grafik/Audio (siehe PLAN.md).
|
||||||
|
|
||||||
|
pub mod logic;
|
||||||
|
pub mod game;
|
||||||
|
pub mod network;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! Netzwerkprotokoll für die Multiplayer-Synchronisation (siehe PLAN.md, Phase 2).
|
||||||
|
//!
|
||||||
|
//! Die eigentliche WebSocket-Anbindung (Server + Client) ist noch nicht
|
||||||
|
//! implementiert; dies definiert vorerst nur den Nachrichten-Vertrag,
|
||||||
|
//! damit `core::logic` und die künftige Transportschicht entkoppelt bleiben.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::core::logic::card::Card;
|
||||||
|
|
||||||
|
/// Nachrichten, die ein Client an den Server schickt.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ClientMessage {
|
||||||
|
JoinTable { player_name: String },
|
||||||
|
Fold,
|
||||||
|
Check,
|
||||||
|
Call { amount: u64 },
|
||||||
|
Raise { amount: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nachrichten, die der Server an alle Clients eines Tisches broadcastet.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ServerMessage {
|
||||||
|
PlayerJoined { player_id: u32, name: String },
|
||||||
|
HoleCardsDealt { player_id: u32, cards: Vec<Card> },
|
||||||
|
CommunityCardsRevealed { cards: Vec<Card> },
|
||||||
|
PotUpdated { pot: u64 },
|
||||||
|
PlayerActed { player_id: u32, description: String },
|
||||||
|
RoundChanged { round: String },
|
||||||
|
Showdown { winner_id: u32 },
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
mod audio;
|
||||||
|
mod core;
|
||||||
|
|
||||||
|
use audio::AudioEngine;
|
||||||
|
use core::game::GameManager;
|
||||||
|
use core::logic::Deck;
|
||||||
|
|
||||||
|
// TODO(Phase 1 Rest / Phase 3): echte Bevy-`App` mit Plugins/Systems
|
||||||
|
// aufsetzen, sobald die Bevy-0.19-API-Oberfläche geprüft ist. Ohne
|
||||||
|
// funktionierende Rust-Toolchain in dieser Umgebung wollte ich hier
|
||||||
|
// keinen ungeprüften Bevy-Boilerplate-Code hinterlassen, der eventuell
|
||||||
|
// gegen die tatsächliche 0.19-API nicht kompiliert. Stattdessen dient
|
||||||
|
// main() aktuell als Smoke-Test für die bisher fertiggestellte
|
||||||
|
// Poker-Engine (Deck/Hand/Betting) und die Audio-Anbindung.
|
||||||
|
fn main() {
|
||||||
|
let audio_engine = AudioEngine::new();
|
||||||
|
|
||||||
|
let mut game = GameManager::start_new_game(vec!["Spieler 1".into(), "Spieler 2".into()]);
|
||||||
|
|
||||||
|
let mut deck = Deck::new();
|
||||||
|
deck.shuffle();
|
||||||
|
|
||||||
|
for player in game.players.iter_mut() {
|
||||||
|
player.hole_cards = deck.deal_hand(2);
|
||||||
|
}
|
||||||
|
game.community_cards = deck.deal_hand(5);
|
||||||
|
|
||||||
|
audio_engine.tts.speak("Neues Spiel gestartet.");
|
||||||
|
audio_engine.sfx.play_sound("shuffle");
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Spiel gestartet mit {} Spielern, Runde: {:?}",
|
||||||
|
game.players.len(),
|
||||||
|
game.current_round
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user