client/server
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "audiopoker_client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
audiopoker_core = { path = "../core" }
|
||||
bevy = { version = "0.19", features = ["webgl2"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
# wasm-bindgen/web-sys/js-sys/futures: aktuell nur für audio::tts (Web Speech
|
||||
# API) genutzt. Werden zusätzlich gebraucht, sobald der Client per
|
||||
# web_sys::WebSocket an den `server` (siehe ../server) angebunden wird -
|
||||
# das ist noch offen, siehe PLAN.md.
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["SpeechRecognition", "SpeechSynthesis", "SpeechSynthesisUtterance", "Window", "HtmlInputElement", "console", "WebSocket", "MessageEvent"] }
|
||||
js-sys = "0.3"
|
||||
futures = "0.3"
|
||||
rand = "0.8.5"
|
||||
@@ -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,24 @@
|
||||
//! Sound-Manager für atmosphärische Effekte (siehe PLAN.md, `audio::sfx`).
|
||||
//!
|
||||
//! Es gibt noch keine Audio-Assets im Projekt; sobald welche existieren,
|
||||
//! kann `play_sound` über `bevy_audio` (`AudioPlayer`/`AssetServer`) echte
|
||||
//! Dateien abspielen, gemappt über die Effekt-IDs unten.
|
||||
|
||||
pub struct SfxEngine;
|
||||
|
||||
impl SfxEngine {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Effekt-IDs, die aktuell im Spiel ausgelöst werden:
|
||||
/// - "shuffle": Karten werden gemischt (Spielstart).
|
||||
/// - "chip": Einsatz/Call/Raise/Gewinn (Chip-Geräusch).
|
||||
/// - "fold": Spieler foldet (Karten werden weggelegt).
|
||||
/// - "flip": Community Cards werden aufgedeckt (Flop/Turn/River).
|
||||
pub fn play_sound(&self, effect_id: &str) {
|
||||
// TODO(Phase 3 Rest): echte Wiedergabe via bevy_audio, sobald
|
||||
// Audio-Assets vorhanden sind. 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,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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mod audio;
|
||||
mod game_plugin;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use game_plugin::AudiopokerPlugin;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins(AudiopokerPlugin)
|
||||
.run();
|
||||
}
|
||||
Reference in New Issue
Block a user