diff --git a/Cargo.lock b/Cargo.lock index 49cf5ed..02ab506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3274,8 +3274,10 @@ dependencies = [ "bevy_ratatui", "clap", "log", + "rand 0.9.2", "ratatui", "strum", + "tracing", "tracing-subscriber", "tui-logger", ] diff --git a/Cargo.toml b/Cargo.toml index fe51bfd..8ba3c20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,10 @@ log = { version = "0.4.29", features = [ "release_max_level_error", "max_level_trace", ] } +rand = "0.9.2" ratatui = "0.30.0" strum = { version = "0.27.2", features = ["derive"] } +tracing = "0.1.44" tracing-subscriber = "0.3.22" tui-logger = { version = "0.18.0", features = ["tracing-support", "crossterm"] } diff --git a/src/game/hand.rs b/src/game/hand.rs new file mode 100644 index 0000000..4daa915 --- /dev/null +++ b/src/game/hand.rs @@ -0,0 +1,54 @@ +use bevy::prelude::*; + +use crate::{game::wall::WallTiles, tiles::Tile}; + +#[derive(Component)] +pub struct Hand; + +#[derive(Component)] +#[relationship(relationship_target = HandTiles)] +pub struct InHand(pub Entity); + +#[derive(Component)] +#[relationship_target(relationship = InHand, linked_spawn)] +pub struct HandTiles(Vec); + +pub(crate) fn deal_hands( + mut commands: Commands, + walltiles: Single<&WallTiles>, + walltiles_entity: Single>, +) -> Result { + let hand = walltiles.iter().collect::>(); + + commands + .get_entity(*walltiles_entity)? + .remove_children(hand.last_chunk::<13>().unwrap()); + + commands.spawn((Hand, HandTiles(hand))); + + trace!("dealt hands"); + Ok(()) +} + +pub(crate) fn sort_hand( + mut commands: Commands, + tiles: Populated<&Tile>, + handtiles_entity: Single>, + handtiles: Single<&HandTiles, Changed>, +) -> Result { + let mut hand: Vec<_> = handtiles + .iter() + .map(|e| -> Result<(_, _)> { Ok((tiles.get(e)?, e)) }) + .collect::>()?; + + hand.sort_by_key(|(t, _)| t.suit); + + let hand: Vec<_> = hand.iter().map(|(_, e)| *e).collect(); + + commands + .get_entity(*handtiles_entity)? + .replace_children(&hand); + + trace!("sort_hand"); + Ok(()) +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 14eeaf2..b099460 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,22 +1,40 @@ - use bevy::prelude::*; use crate::tiles::{self, *}; -mod player; -pub(crate) mod wall; +pub mod hand; +pub mod player; +pub mod wall; + +#[derive(States, Default, Hash, Clone, Eq, Debug, PartialEq, Copy)] +pub enum GameState { + #[default] + None, + Setup, + // Deal, + Play, + Score, +} pub struct Riichi; - impl Plugin for Riichi { fn build(&self, app: &mut App) { app.init_resource::() .add_systems(Startup, init_match) .add_systems(Startup, tiles::init_tiles) - .add_systems(Startup, wall::build_wall); + .init_state::() + .add_systems(OnEnter(GameState::Setup), (wall::build_wall, hand::deal_hands, setup_done).chain()) + .add_systems(Update, (hand::sort_hand).run_if(in_state(GameState::Play))) + // semicolon stopper + ; } } +fn setup_done(mut next: ResMut>) { + next.set(GameState::Play); + trace!("setup_done"); +} + #[derive(Component)] pub(crate) struct Dice(u8, u8); diff --git a/src/game/player.rs b/src/game/player.rs index 45d3c04..e4e3323 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -1,9 +1,17 @@ use bevy::prelude::*; +use crate::{ + game::wall::{InWall, Wall}, + tiles::Tile, +}; + #[derive(Component)] pub(crate) struct Player { pub(crate) name: String, } +fn spawn_players(mut commands: Commands) {} + #[derive(Component)] pub(crate) struct Points(pub isize); + diff --git a/src/game/wall.rs b/src/game/wall.rs index 2692952..69e4fd7 100644 --- a/src/game/wall.rs +++ b/src/game/wall.rs @@ -1,11 +1,26 @@ - use bevy::prelude::*; +use rand::seq::SliceRandom; use crate::tiles::Tile; #[derive(Component)] -pub(crate) struct Wall(Vec); +pub struct Wall; -pub(crate) fn build_wall(_tiles: Query<&Tile>) { - info!("built a wall!") +#[derive(Component)] +#[relationship_target(relationship = InWall, linked_spawn)] +pub struct WallTiles(Vec); + +#[derive(Component)] +#[relationship(relationship_target = WallTiles)] +pub struct InWall(pub Entity); + +pub(crate) fn build_wall(mut commands: Commands, tiles: Query>) { + let mut rng = rand::rng(); + + let mut shuffled = tiles.iter().collect::>(); + shuffled.shuffle(&mut rng); + + commands.spawn((Wall, WallTiles(shuffled))); + + trace!("build_wall"); } diff --git a/src/lib.rs b/src/lib.rs index 8a1fb07..45412c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -pub mod tiles; -pub mod yakus; +#![allow(unused)] pub mod game; +pub mod tiles; +pub mod yakus; diff --git a/src/main.rs b/src/main.rs index 641d6df..0481b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ -use bevy::prelude::*; +#![allow(unused)] + +use bevy::{log::LogPlugin, prelude::*}; use clap::{Parser, Subcommand}; -use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt}; +use tracing::Level; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod gui; mod tui; @@ -17,25 +20,30 @@ enum Mode { RunTui, } -fn main() { - // tracing_subscriber::fmt() - // .with_writer(std::io::stderr) - // .with_env_filter("warn,jong=trace") - // .init(); +const FILTERSTRING: &str = "warn,jong=trace"; +fn main() { let args = Args::parse(); let mut app = App::new(); let app = match args.mode { - Mode::RunGui => app.add_plugins(DefaultPlugins), + Mode::RunGui => { + app.add_plugins(DefaultPlugins.set(LogPlugin { + filter: FILTERSTRING.into(), + level: Level::TRACE, + // custom_layer: todo!(), + // fmt_layer: todo!(), + ..Default::default() + })) + } Mode::RunTui => { tracing_subscriber::registry() .with(tui_logger::TuiTracingSubscriberLayer) .init(); tui_logger::init_logger(tui_logger::LevelFilter::Trace).unwrap(); - tui_logger::set_env_filter_from_string("warn,jong=trace"); + tui_logger::set_env_filter_from_string(FILTERSTRING); - app.add_plugins(tui::RiichiTui) + app.add_plugins(tui::RiichiTui::default()) } }; diff --git a/src/tiles.rs b/src/tiles.rs index c00ef9c..5147233 100644 --- a/src/tiles.rs +++ b/src/tiles.rs @@ -1,36 +1,24 @@ use bevy::{ecs::entity::MapEntities, prelude::*}; use strum::FromRepr; - -// #[derive(Component)] -// #[derive(relasionship(re))] -// pub struct TileEntity { -// #[relationship] -// pub parent: Entity, - -// r#type: Tile, -// } - -struct Hand; - -#[derive(Component)] +#[derive(Component, Debug)] pub struct Tile { - suit: Suit, + pub suit: Suit, } -#[derive(MapEntities)] +#[derive(MapEntities, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] pub enum Suit { + Man(Rank), Pin(Rank), Sou(Rank), - Man(Rank), Wind(Wind), Dragon(Dragon), } -#[derive(Deref, DerefMut)] -pub struct Rank(u8); +#[derive(Deref, DerefMut, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] +pub struct Rank(pub u8); -#[derive(FromRepr)] +#[derive(FromRepr, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] pub enum Wind { Ton, Nan, @@ -38,7 +26,7 @@ pub enum Wind { Pei, } -#[derive(FromRepr)] +#[derive(Debug, FromRepr, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] pub enum Dragon { Haku, Hatsu, diff --git a/src/tui/console.rs b/src/tui/console.rs index db20cf8..f668331 100644 --- a/src/tui/console.rs +++ b/src/tui/console.rs @@ -1,14 +1,13 @@ -use tui_logger::TuiLoggerWidget; - +use bevy::prelude::*; use bevy_ratatui::RatatuiContext; - -use bevy::input::keyboard::Key; +use ratatui::widgets::Block; +use tui_logger::TuiLoggerWidget; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] pub(crate) enum ConsoleState { - Open, #[default] Closed, + Open, } impl std::ops::Not for ConsoleState { @@ -22,20 +21,10 @@ impl std::ops::Not for ConsoleState { } } -pub(crate) fn toggle_console( - input: Res>, - curr_state: Res>, - mut next_state: ResMut>, -) { - if input.just_pressed(Key::Character("`".into())) { - trace!("toggled"); - next_state.set(!*curr_state.get()); - } -} - pub(crate) fn draw_console(mut tui_ctx: ResMut) -> Result { tui_ctx.draw(|frame| { - frame.render_widget(TuiLoggerWidget::default(), frame.area()); + let block = Block::bordered().title("console"); + frame.render_widget(TuiLoggerWidget::default().block(block), frame.area()); })?; Ok(()) diff --git a/src/tui/input.rs b/src/tui/input.rs deleted file mode 100644 index 6d0d6e4..0000000 --- a/src/tui/input.rs +++ /dev/null @@ -1,22 +0,0 @@ -// use bevy::ecs::message::MessageReader; -use bevy::app::AppExit; -use bevy::input::keyboard::{Key, KeyboardInput}; -use bevy::prelude::*; - -use crate::tui::console::ConsoleState; - -pub(crate) fn keyboard_input_system( - // keycode_input: Option>>, - // key_input: Option>>, - // mut next_state: ResMut>, - mut keyboard_events: MessageReader, -) { - // if let Some(keycode_input) = keycode_input { - // if keycode_input.just_pressed(KeyCode::Backquote) { - // // console_state.set; - // } - // } - for keyboard_input in keyboard_events.read() { - trace!("{:?}", keyboard_input); - } -} diff --git a/src/tui/menu.rs b/src/tui/menu.rs new file mode 100644 index 0000000..3ee27e5 --- /dev/null +++ b/src/tui/menu.rs @@ -0,0 +1,29 @@ +use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; +use bevy_ratatui::event::KeyMessage; +use ratatui::crossterm::event::KeyCode; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; + +use jong::game::GameState; + +use crate::tui::TuiState; + +const MAINMENU_OPTIONS: [&str; 2] = ["(p)lay", "(q)uit"]; +// const MAINMENU_INPUTS: [char;2] = ['p', 'q']; + +pub(crate) fn draw_mainmenu( + mut tui_ctx: ResMut, + // mut tui_state: ResMut>, + // mut game_state: ResMut>, +) { + let options = MAINMENU_OPTIONS; + let layout = Layout::vertical(vec![Constraint::Min(1); options.len()]); + + tui_ctx.draw(|frame| { + let areas = layout.split(frame.area()); + for (opt, area) in options.into_iter().zip(areas.iter()) { + frame.render_widget(opt, *area) + } + }); +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index d797779..e90a87f 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,15 +1,42 @@ use std::time::Duration; -use bevy::{app::ScheduleRunnerPlugin, input::keyboard::Key, prelude::*, state::app::StatesPlugin}; -use bevy_ratatui::{RatatuiContext, RatatuiPlugins}; -use jong::tiles::Tile; -use tui_logger::TuiLoggerSmartWidget; +use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; +use bevy_ratatui::RatatuiPlugins; +use bevy_ratatui::event::KeyMessage; +use ratatui::{text::ToSpan, widgets::Paragraph}; + +use jong::game::GameState; +use jong::game::wall::InWall; + +use crate::tui::console::ConsoleState; mod console; -mod input; +mod menu; +mod render; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)] +pub(crate) enum TuiState { + #[default] + MainMenu, + InGame, +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +struct InGame; + +impl ComputedStates for InGame { + type SourceStates = TuiState; + + fn compute(sources: Self::SourceStates) -> Option { + match sources { + TuiState::MainMenu => None, + TuiState::InGame => Some(Self), + } + } +} + +#[derive(Default)] pub struct RiichiTui; - impl Plugin for RiichiTui { fn build(&self, app: &mut App) { app.add_plugins(( @@ -18,33 +45,78 @@ impl Plugin for RiichiTui { ))), RatatuiPlugins { // enable_kitty_protocol: todo!(), - // enable_mouse_capture: todo!(), + enable_mouse_capture: true, enable_input_forwarding: true, ..Default::default() }, )) .add_plugins(StatesPlugin) + + // console .init_state::() - .add_systems(Update, console::toggle_console) .add_systems(Update, console::draw_console.run_if(in_state(console::ConsoleState::Open))) - .add_systems(Update, input::keyboard_input_system) - .add_systems(Update, draw_system) + + // general setup + .init_state::() + .add_computed_state::() + .add_systems(Update, input_system) + + // main menu + .add_systems(Update, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) + + // gaming + .init_resource::() + .add_systems(Update, render::ingame::draw_ingame.run_if(in_state(InGame))) + .add_systems(Update, render::hand::render_changed_hand.run_if(in_state(InGame).and(in_state(GameState::Play)))) + // semicolon stopper ; } } -// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] -// enum TuiState { -// MainMenu, -// InGame, -// } +#[allow(clippy::too_many_arguments)] +pub(crate) fn input_system( + mut messages: MessageReader, -pub(crate) fn draw_system(mut tui_ctx: ResMut, query: Query<&Tile>) -> Result { - tui_ctx.draw(|frame| { - let text = ratatui::text::Text::raw("tiny riichi"); - frame.render_widget(text, frame.area()); - })?; + curr_tuistate: Res>, + curr_consolestate: Res>, + curr_gamestate: Res>, - Ok(()) + mut next_tuistate: ResMut>, + mut next_consolestate: ResMut>, + mut next_gamestate: ResMut>, + + mut exit: MessageWriter, +) { + use bevy_ratatui::crossterm::event::KeyCode; + + let (ts, cs, gs) = (curr_tuistate.get(), curr_consolestate.get(), curr_gamestate.get()); + + for message in messages.read() { + if let KeyCode::Char('`') = message.code { + next_consolestate.set(!*curr_consolestate.get()); + continue + } + + match ts { + TuiState::MainMenu => match message.code { + KeyCode::Char('p') => { + next_tuistate.set(TuiState::InGame); + next_gamestate.set(GameState::Setup); + } + KeyCode::Char('q') => { + exit.write_default(); + } + _ => {} + }, + TuiState::InGame => match gs { + // GameState::None => todo!(), + GameState::Setup => todo!(), + GameState::Play => todo!(), + GameState::Score => todo!(), + _ => todo!() + }, + } + } } + diff --git a/src/tui/render/hand.rs b/src/tui/render/hand.rs new file mode 100644 index 0000000..9c76106 --- /dev/null +++ b/src/tui/render/hand.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; +use ratatui::widgets::Paragraph; + +use jong::game::hand::HandTiles; +use jong::tiles::Tile; + +use crate::tui::render::tiles; + +#[derive(Resource, Default)] +pub(crate) struct RenderedHand(pub(crate) Vec>); + +pub(crate) fn render_changed_hand( + hand: Single<&HandTiles, Changed>, + tiles: Populated<&Tile>, + mut target: ResMut, +) -> Result { + trace!("render_changed_hand"); + + render_hand(hand, tiles, target)?; + + Ok(()) +} + +pub(crate) fn render_hand( + hand: Single<&HandTiles, Changed>, + tiles: Populated<&Tile>, + mut target: ResMut, +) -> Result { + trace!("render_hand"); + + let hand_tiles = hand + .iter() + .map(|inhand| -> Result<_> { Ok(tiles.get(inhand).map(tiles::draw_tile)?) }) + .collect::>>()?; + target.0 = hand_tiles; + + Ok(()) +} diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs new file mode 100644 index 0000000..6a30d4e --- /dev/null +++ b/src/tui/render/ingame.rs @@ -0,0 +1,26 @@ +use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; + +use crate::tui::render::hand; + +pub(crate) fn draw_ingame( + rendered_hand: Res, + mut tui_ctx: ResMut, +) -> Result { + use ratatui::layout::Flex; + use ratatui::prelude::*; + + tui_ctx.draw(|frame| { + // debug!("{}", frame.area()); + + let layout = Layout::horizontal(vec![Constraint::Max(5); 13]).flex(Flex::Start); + let mut area = frame.area(); + area.height = 4; + let areas = layout.areas::<13>(area); + for (tile, area) in rendered_hand.0.iter().zip(areas.iter()) { + frame.render_widget(tile, *area); + } + })?; + + Ok(()) +} diff --git a/src/tui/render/mod.rs b/src/tui/render/mod.rs new file mode 100644 index 0000000..fc6612a --- /dev/null +++ b/src/tui/render/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod hand; +pub(crate) mod ingame; +mod tiles; diff --git a/src/tui/render/tiles.rs b/src/tui/render/tiles.rs new file mode 100644 index 0000000..620a2d1 --- /dev/null +++ b/src/tui/render/tiles.rs @@ -0,0 +1,30 @@ +use ratatui::widgets::Paragraph; + +use jong::tiles::Tile; + +pub(crate) fn draw_tile(tile: &Tile) -> Paragraph<'static> { + use ratatui::prelude::*; + + let block = ratatui::widgets::Block::bordered(); + + Paragraph::new(match &tile.suit { + jong::tiles::Suit::Pin(rank) => format!("{}\np", rank.0), + jong::tiles::Suit::Sou(rank) => format!("{}\ns", rank.0), + jong::tiles::Suit::Man(rank) => format!("{}\nm", rank.0), + jong::tiles::Suit::Wind(wind) => (match wind { + jong::tiles::Wind::Ton => "e\nw", + jong::tiles::Wind::Nan => "s\nw", + jong::tiles::Wind::Shaa => "w\nw", + jong::tiles::Wind::Pei => "n\nw", + }) + .into(), + jong::tiles::Suit::Dragon(dragon) => (match dragon { + jong::tiles::Dragon::Haku => "w\nd", + jong::tiles::Dragon::Hatsu => "g\nd", + jong::tiles::Dragon::Chun => "r\nd", + }) + .into(), + }) + .block(block) + .centered() +}