//! Verbindet die (bewusst Bevy-unabhängige) Kernlogik aus `core` und `audio` //! mit einer echten Bevy-`App` (siehe PLAN.md, Phase 1 & 3). //! //! Hinweis zur Bevy-Version: Seit Bevy 0.17 sind gepufferte Events als //! `Message` (statt `Event`) modelliert und laufen über `MessageWriter` / //! `MessageReader` / `app.add_message::()`. `Event` bezeichnet seither //! ausschließlich Observer-Events. Dieser Code ist auf Bevy 0.19 ausgelegt. //! //! Steuerung (barrierefrei, tastaturzentriert, siehe PLAN.md): //! - `Tab`: wechselt die ausgewählte Aktion (Fold / Call-Check / Raise). //! - `Leertaste`/`Enter`: bestätigt die ausgewählte Aktion. use bevy::prelude::*; use crate::audio::AudioEngine; use audiopoker_core::game::{BettingAction, GameManager, GameState, Round, RAISE_INCREMENT}; use audiopoker_core::logic::Deck; /// Bevy-Resource-Wrapper um den Engine-unabhängigen Spielzustand. /// Bewusst als Newtype, damit `core::game` selbst keine Bevy-Abhängigkeit braucht. #[derive(Resource)] pub struct Table(pub GameState); /// Bevy-Resource-Wrapper um die Engine-unabhängige Audio-Engine (`audio::AudioEngine`). #[derive(Resource)] pub struct AudioRes(pub AudioEngine); /// Welche Aktion aktuell per `Tab` ausgewählt ist (minimalistisches, /// tastaturzentriertes UI für Screenreader-/Audio-Nutzung). #[derive(Resource, Default)] pub struct ActionCursor { pub selected: usize, } const ACTION_LABELS: [&str; 3] = ["Fold", "Call/Check", "Raise"]; /// Message: Ein Text soll per TTS angesagt werden (z. B. "Spieler 1 setzt 50"). #[derive(Message, Debug, Clone)] pub struct Announce(pub String); /// Message: Ein Soundeffekt soll abgespielt werden (z. B. "shuffle", "chip"). #[derive(Message, Debug, Clone)] pub struct PlaySfx(pub &'static str); pub struct AudiopokerPlugin; impl Plugin for AudiopokerPlugin { fn build(&self, app: &mut App) { app.insert_resource(AudioRes(AudioEngine::new())) .init_resource::() .add_message::() .add_message::() .add_systems(Startup, setup_game) .add_systems(Update, (handle_player_input, speak_announcements, play_sfx_cues)); } } /// Startet eine neue Partie, teilt Karten aus und kündigt beides per /// TTS/SFX-Message an. Läuft einmalig im `Startup`-Schedule. fn setup_game( mut commands: Commands, mut announcements: MessageWriter, mut sfx_cues: MessageWriter, ) { 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); sfx_cues.write(PlaySfx("shuffle")); announcements.write(Announce(format!( "Neues Spiel gestartet mit {} Spielern.", game.players.len() ))); announcements.write(Announce( "Steuerung: Tab wählt eine Aktion aus, Leertaste oder Eingabetaste bestätigt.".into(), )); announcements.write(Announce(format!("{} ist am Zug.", game.players[game.to_act].name))); commands.insert_resource(Table(game)); } /// Liest Tab/Space/Enter und wendet die ausgewählte Aktion auf den aktuell /// am Zug befindlichen Spieler an. Die eigentliche Spiellogik (Straßen- /// Fortschritt, Showdown) steckt komplett in `GameManager::take_action`. fn handle_player_input( keys: Res>, mut cursor: ResMut, mut table: ResMut, mut announcements: MessageWriter, mut sfx_cues: MessageWriter, ) { if table.0.current_round == Round::Showdown { // Hand ist beendet. Eine neue Hand starten ist noch nicht implementiert // (nächster sinnvoller Schritt, z. B. über eine eigene "Neue Hand"-Taste). return; } if keys.just_pressed(KeyCode::Tab) { cursor.selected = (cursor.selected + 1) % ACTION_LABELS.len(); announcements.write(Announce(format!("Ausgewählt: {}", ACTION_LABELS[cursor.selected]))); } if keys.just_pressed(KeyCode::Space) || keys.just_pressed(KeyCode::Enter) { let state = &mut table.0; let player_idx = state.to_act; let to_call = GameManager::amount_to_call(state, player_idx); let action = match cursor.selected { 0 => BettingAction::Fold, 1 if to_call == 0 => BettingAction::Check, 1 => BettingAction::Call(to_call), _ => BettingAction::Raise(to_call + RAISE_INCREMENT), }; let outcome = GameManager::take_action(state, player_idx, action); for line in outcome.announcements { announcements.write(Announce(line)); } for cue in outcome.sfx_cues { sfx_cues.write(PlaySfx(cue)); } } } /// Liest `Announce`-Messages und spricht sie über die TTS-Engine. fn speak_announcements(mut reader: MessageReader, audio: Res) { for Announce(text) in reader.read() { audio.0.tts.speak(text); } } /// Liest `PlaySfx`-Messages und spielt den zugehörigen Soundeffekt. fn play_sfx_cues(mut reader: MessageReader, audio: Res) { for PlaySfx(id) in reader.read() { audio.0.sfx.play_sound(id); } }