client/server

This commit is contained in:
2026-07-24 14:07:18 +02:00
parent 520e9a1320
commit 730855ae5d
22 changed files with 507 additions and 6199 deletions
+142
View File
@@ -0,0 +1,142 @@
//! 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::<T>()`. `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::<ActionCursor>()
.add_message::<Announce>()
.add_message::<PlaySfx>()
.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<Announce>,
mut sfx_cues: MessageWriter<PlaySfx>,
) {
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<ButtonInput<KeyCode>>,
mut cursor: ResMut<ActionCursor>,
mut table: ResMut<Table>,
mut announcements: MessageWriter<Announce>,
mut sfx_cues: MessageWriter<PlaySfx>,
) {
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<Announce>, audio: Res<AudioRes>) {
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<PlaySfx>, audio: Res<AudioRes>) {
for PlaySfx(id) in reader.read() {
audio.0.sfx.play_sound(id);
}
}