Compare commits

...

10 commits

Author SHA1 Message Date
Tao Tien
5504db4e0f big ass input system 2026-01-12 21:36:41 -08:00
Tao Tien
81cb5c24d4 cleanup 2026-01-12 21:04:21 -08:00
Tao Tien
59399c3590 we start printing again 2026-01-12 03:01:56 -08:00
Tao Tien
65ea256436 fix deal crash 2026-01-12 01:41:03 -08:00
Tao Tien
759ff410c2 basic main menu (change this to generic overlay menu later?) 2026-01-12 01:07:01 -08:00
Tao Tien
bc3421a371 why we crashin here? 2026-01-11 23:54:26 -08:00
Tao Tien
130bb38725 reorder stuff for tui main menu 2026-01-11 23:19:48 -08:00
Tao Tien
3417384b86 detect hand change and render it 2026-01-11 21:52:04 -08:00
Tao Tien
d506a25716 render some tiles 2026-01-11 20:01:30 -08:00
Tao Tien
bea146d439 shuffle 2026-01-11 15:43:03 -08:00
17 changed files with 362 additions and 101 deletions

2
Cargo.lock generated
View file

@ -3274,8 +3274,10 @@ dependencies = [
"bevy_ratatui", "bevy_ratatui",
"clap", "clap",
"log", "log",
"rand 0.9.2",
"ratatui", "ratatui",
"strum", "strum",
"tracing",
"tracing-subscriber", "tracing-subscriber",
"tui-logger", "tui-logger",
] ]

View file

@ -18,8 +18,10 @@ log = { version = "0.4.29", features = [
"release_max_level_error", "release_max_level_error",
"max_level_trace", "max_level_trace",
] } ] }
rand = "0.9.2"
ratatui = "0.30.0" ratatui = "0.30.0"
strum = { version = "0.27.2", features = ["derive"] } strum = { version = "0.27.2", features = ["derive"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.22"
tui-logger = { version = "0.18.0", features = ["tracing-support", "crossterm"] } tui-logger = { version = "0.18.0", features = ["tracing-support", "crossterm"] }

54
src/game/hand.rs Normal file
View file

@ -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<Entity>);
pub(crate) fn deal_hands(
mut commands: Commands,
walltiles: Single<&WallTiles>,
walltiles_entity: Single<Entity, With<WallTiles>>,
) -> Result {
let hand = walltiles.iter().collect::<Vec<_>>();
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<Entity, With<HandTiles>>,
handtiles: Single<&HandTiles, Changed<HandTiles>>,
) -> Result {
let mut hand: Vec<_> = handtiles
.iter()
.map(|e| -> Result<(_, _)> { Ok((tiles.get(e)?, e)) })
.collect::<Result<_>>()?;
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(())
}

View file

@ -1,22 +1,40 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::tiles::{self, *}; use crate::tiles::{self, *};
mod player; pub mod hand;
pub(crate) mod wall; 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; pub struct Riichi;
impl Plugin for Riichi { impl Plugin for Riichi {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<Compass>() app.init_resource::<Compass>()
.add_systems(Startup, init_match) .add_systems(Startup, init_match)
.add_systems(Startup, tiles::init_tiles) .add_systems(Startup, tiles::init_tiles)
.add_systems(Startup, wall::build_wall); .init_state::<GameState>()
.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<NextState<GameState>>) {
next.set(GameState::Play);
trace!("setup_done");
}
#[derive(Component)] #[derive(Component)]
pub(crate) struct Dice(u8, u8); pub(crate) struct Dice(u8, u8);

View file

@ -1,9 +1,17 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{
game::wall::{InWall, Wall},
tiles::Tile,
};
#[derive(Component)] #[derive(Component)]
pub(crate) struct Player { pub(crate) struct Player {
pub(crate) name: String, pub(crate) name: String,
} }
fn spawn_players(mut commands: Commands) {}
#[derive(Component)] #[derive(Component)]
pub(crate) struct Points(pub isize); pub(crate) struct Points(pub isize);

View file

@ -1,11 +1,26 @@
use bevy::prelude::*; use bevy::prelude::*;
use rand::seq::SliceRandom;
use crate::tiles::Tile; use crate::tiles::Tile;
#[derive(Component)] #[derive(Component)]
pub(crate) struct Wall(Vec<Entity>); pub struct Wall;
pub(crate) fn build_wall(_tiles: Query<&Tile>) { #[derive(Component)]
info!("built a wall!") #[relationship_target(relationship = InWall, linked_spawn)]
pub struct WallTiles(Vec<Entity>);
#[derive(Component)]
#[relationship(relationship_target = WallTiles)]
pub struct InWall(pub Entity);
pub(crate) fn build_wall(mut commands: Commands, tiles: Query<Entity, With<Tile>>) {
let mut rng = rand::rng();
let mut shuffled = tiles.iter().collect::<Vec<_>>();
shuffled.shuffle(&mut rng);
commands.spawn((Wall, WallTiles(shuffled)));
trace!("build_wall");
} }

View file

@ -1,4 +1,5 @@
pub mod tiles; #![allow(unused)]
pub mod yakus;
pub mod game; pub mod game;
pub mod tiles;
pub mod yakus;

View file

@ -1,6 +1,9 @@
use bevy::prelude::*; #![allow(unused)]
use bevy::{log::LogPlugin, prelude::*};
use clap::{Parser, Subcommand}; 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 gui;
mod tui; mod tui;
@ -17,25 +20,30 @@ enum Mode {
RunTui, RunTui,
} }
fn main() { const FILTERSTRING: &str = "warn,jong=trace";
// tracing_subscriber::fmt()
// .with_writer(std::io::stderr)
// .with_env_filter("warn,jong=trace")
// .init();
fn main() {
let args = Args::parse(); let args = Args::parse();
let mut app = App::new(); let mut app = App::new();
let app = match args.mode { 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 => { Mode::RunTui => {
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tui_logger::TuiTracingSubscriberLayer) .with(tui_logger::TuiTracingSubscriberLayer)
.init(); .init();
tui_logger::init_logger(tui_logger::LevelFilter::Trace).unwrap(); 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())
} }
}; };

View file

@ -1,36 +1,24 @@
use bevy::{ecs::entity::MapEntities, prelude::*}; use bevy::{ecs::entity::MapEntities, prelude::*};
use strum::FromRepr; use strum::FromRepr;
#[derive(Component, Debug)]
// #[derive(Component)]
// #[derive(relasionship(re))]
// pub struct TileEntity {
// #[relationship]
// pub parent: Entity,
// r#type: Tile,
// }
struct Hand;
#[derive(Component)]
pub struct Tile { pub struct Tile {
suit: Suit, pub suit: Suit,
} }
#[derive(MapEntities)] #[derive(MapEntities, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub enum Suit { pub enum Suit {
Man(Rank),
Pin(Rank), Pin(Rank),
Sou(Rank), Sou(Rank),
Man(Rank),
Wind(Wind), Wind(Wind),
Dragon(Dragon), Dragon(Dragon),
} }
#[derive(Deref, DerefMut)] #[derive(Deref, DerefMut, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub struct Rank(u8); pub struct Rank(pub u8);
#[derive(FromRepr)] #[derive(FromRepr, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub enum Wind { pub enum Wind {
Ton, Ton,
Nan, Nan,
@ -38,7 +26,7 @@ pub enum Wind {
Pei, Pei,
} }
#[derive(FromRepr)] #[derive(Debug, FromRepr, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub enum Dragon { pub enum Dragon {
Haku, Haku,
Hatsu, Hatsu,

View file

@ -1,14 +1,13 @@
use tui_logger::TuiLoggerWidget; use bevy::prelude::*;
use bevy_ratatui::RatatuiContext; use bevy_ratatui::RatatuiContext;
use ratatui::widgets::Block;
use bevy::input::keyboard::Key; use tui_logger::TuiLoggerWidget;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
pub(crate) enum ConsoleState { pub(crate) enum ConsoleState {
Open,
#[default] #[default]
Closed, Closed,
Open,
} }
impl std::ops::Not for ConsoleState { impl std::ops::Not for ConsoleState {
@ -22,20 +21,10 @@ impl std::ops::Not for ConsoleState {
} }
} }
pub(crate) fn toggle_console(
input: Res<ButtonInput<Key>>,
curr_state: Res<State<ConsoleState>>,
mut next_state: ResMut<NextState<ConsoleState>>,
) {
if input.just_pressed(Key::Character("`".into())) {
trace!("toggled");
next_state.set(!*curr_state.get());
}
}
pub(crate) fn draw_console(mut tui_ctx: ResMut<RatatuiContext>) -> Result { pub(crate) fn draw_console(mut tui_ctx: ResMut<RatatuiContext>) -> Result {
tui_ctx.draw(|frame| { 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(()) Ok(())

View file

@ -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<Res<ButtonInput<KeyCode>>>,
// key_input: Option<Res<ButtonInput<Key>>>,
// mut next_state: ResMut<State<TuiState>>,
mut keyboard_events: MessageReader<KeyboardInput>,
) {
// 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);
}
}

29
src/tui/menu.rs Normal file
View file

@ -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<RatatuiContext>,
// mut tui_state: ResMut<NextState<TuiState>>,
// mut game_state: ResMut<NextState<GameState>>,
) {
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)
}
});
}

View file

@ -1,15 +1,42 @@
use std::time::Duration; use std::time::Duration;
use bevy::{app::ScheduleRunnerPlugin, input::keyboard::Key, prelude::*, state::app::StatesPlugin}; use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin};
use bevy_ratatui::{RatatuiContext, RatatuiPlugins}; use bevy_ratatui::RatatuiPlugins;
use jong::tiles::Tile; use bevy_ratatui::event::KeyMessage;
use tui_logger::TuiLoggerSmartWidget; use ratatui::{text::ToSpan, widgets::Paragraph};
use jong::game::GameState;
use jong::game::wall::InWall;
use crate::tui::console::ConsoleState;
mod console; 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<Self> {
match sources {
TuiState::MainMenu => None,
TuiState::InGame => Some(Self),
}
}
}
#[derive(Default)]
pub struct RiichiTui; pub struct RiichiTui;
impl Plugin for RiichiTui { impl Plugin for RiichiTui {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(( app.add_plugins((
@ -18,33 +45,78 @@ impl Plugin for RiichiTui {
))), ))),
RatatuiPlugins { RatatuiPlugins {
// enable_kitty_protocol: todo!(), // enable_kitty_protocol: todo!(),
// enable_mouse_capture: todo!(), enable_mouse_capture: true,
enable_input_forwarding: true, enable_input_forwarding: true,
..Default::default() ..Default::default()
}, },
)) ))
.add_plugins(StatesPlugin) .add_plugins(StatesPlugin)
// console
.init_state::<console::ConsoleState>() .init_state::<console::ConsoleState>()
.add_systems(Update, console::toggle_console)
.add_systems(Update, console::draw_console.run_if(in_state(console::ConsoleState::Open))) .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::<TuiState>()
.add_computed_state::<InGame>()
.add_systems(Update, input_system)
// main menu
.add_systems(Update, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu)))
// gaming
.init_resource::<render::hand::RenderedHand>()
.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 // semicolon stopper
; ;
} }
} }
// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] #[allow(clippy::too_many_arguments)]
// enum TuiState { pub(crate) fn input_system(
// MainMenu, mut messages: MessageReader<KeyMessage>,
// InGame,
// }
pub(crate) fn draw_system(mut tui_ctx: ResMut<RatatuiContext>, query: Query<&Tile>) -> Result { curr_tuistate: Res<State<TuiState>>,
tui_ctx.draw(|frame| { curr_consolestate: Res<State<ConsoleState>>,
let text = ratatui::text::Text::raw("tiny riichi"); curr_gamestate: Res<State<GameState>>,
frame.render_widget(text, frame.area());
})?;
Ok(()) mut next_tuistate: ResMut<NextState<TuiState>>,
mut next_consolestate: ResMut<NextState<ConsoleState>>,
mut next_gamestate: ResMut<NextState<GameState>>,
mut exit: MessageWriter<AppExit>,
) {
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!()
},
}
}
} }

38
src/tui/render/hand.rs Normal file
View file

@ -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<Paragraph<'static>>);
pub(crate) fn render_changed_hand(
hand: Single<&HandTiles, Changed<HandTiles>>,
tiles: Populated<&Tile>,
mut target: ResMut<RenderedHand>,
) -> Result {
trace!("render_changed_hand");
render_hand(hand, tiles, target)?;
Ok(())
}
pub(crate) fn render_hand(
hand: Single<&HandTiles, Changed<HandTiles>>,
tiles: Populated<&Tile>,
mut target: ResMut<RenderedHand>,
) -> Result {
trace!("render_hand");
let hand_tiles = hand
.iter()
.map(|inhand| -> Result<_> { Ok(tiles.get(inhand).map(tiles::draw_tile)?) })
.collect::<Result<Vec<_>>>()?;
target.0 = hand_tiles;
Ok(())
}

26
src/tui/render/ingame.rs Normal file
View file

@ -0,0 +1,26 @@
use bevy::prelude::*;
use bevy_ratatui::RatatuiContext;
use crate::tui::render::hand;
pub(crate) fn draw_ingame(
rendered_hand: Res<hand::RenderedHand>,
mut tui_ctx: ResMut<RatatuiContext>,
) -> 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(())
}

3
src/tui/render/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub(crate) mod hand;
pub(crate) mod ingame;
mod tiles;

30
src/tui/render/tiles.rs Normal file
View file

@ -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()
}