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",
"clap",
"log",
"rand 0.9.2",
"ratatui",
"strum",
"tracing",
"tracing-subscriber",
"tui-logger",
]

View file

@ -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"] }

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 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::<Compass>()
.add_systems(Startup, init_match)
.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)]
pub(crate) struct Dice(u8, u8);

View file

@ -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);

View file

@ -1,11 +1,26 @@
use bevy::prelude::*;
use rand::seq::SliceRandom;
use crate::tiles::Tile;
#[derive(Component)]
pub(crate) struct Wall(Vec<Entity>);
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<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;
pub mod yakus;
#![allow(unused)]
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 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())
}
};

View file

@ -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,

View file

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

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 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<Self> {
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::<console::ConsoleState>()
.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::<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
;
}
}
// #[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<KeyMessage>,
pub(crate) fn draw_system(mut tui_ctx: ResMut<RatatuiContext>, 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<State<TuiState>>,
curr_consolestate: Res<State<ConsoleState>>,
curr_gamestate: Res<State<GameState>>,
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()
}