Compare commits

...

2 commits

Author SHA1 Message Date
Tao Tien
1b5b7c9e77 start render other 2026-03-09 05:21:03 -07:00
Tao Tien
8f606543f5 order views after player anyways 2026-03-09 03:29:51 -07:00
10 changed files with 245 additions and 90 deletions

View file

@ -3,6 +3,9 @@
lib, lib,
... ...
}: rec { }: rec {
env.RUST_LOG = "jong=trace";
env.RUST_BACKTRACE = "1";
# https://devenv.sh/processes/ # https://devenv.sh/processes/
processes.lspmux.exec = "lspmux server"; processes.lspmux.exec = "lspmux server";
processes.spacetimedb_start.exec = "spacetime start"; processes.spacetimedb_start.exec = "spacetime start";
@ -44,8 +47,6 @@
wayland wayland
]; ];
env.LD_LIBRARY_PATH = lib.makeLibraryPath packages; env.LD_LIBRARY_PATH = lib.makeLibraryPath packages;
env.RUST_LOG = "jong=trace";
env.RUST_BACKTRACE = "full";
# https://devenv.sh/languages/ # https://devenv.sh/languages/
languages.rust = { languages.rust = {

View file

@ -125,10 +125,41 @@ impl<'ctx> BotIdUnique<'ctx> {
} }
} }
/// Access to the `config_id` unique index on the table `bot`,
/// which allows point queries on the field of the same name
/// via the [`BotConfigIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bot().config_id().find(...)`.
pub struct BotConfigIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<Bot, u32>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BotTableHandle<'ctx> {
/// Get a handle on the `config_id` unique index on the table `bot`.
pub fn config_id(&self) -> BotConfigIdUnique<'ctx> {
BotConfigIdUnique {
imp: self.imp.get_unique_constraint::<u32>("config_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BotConfigIdUnique<'ctx> {
/// Find the subscribed row whose `config_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &u32) -> Option<Bot> {
self.imp.find(col_val)
}
}
#[doc(hidden)] #[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) { pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<Bot>("bot"); let _table = client_cache.get_or_make_table::<Bot>("bot");
_table.add_unique_constraint::<u32>("id", |row| &row.id); _table.add_unique_constraint::<u32>("id", |row| &row.id);
_table.add_unique_constraint::<u32>("config_id", |row| &row.config_id);
} }
#[doc(hidden)] #[doc(hidden)]

View file

@ -40,6 +40,7 @@ impl __sdk::__query_builder::HasCols for Bot {
/// ///
/// Provides typed access to indexed columns for query building. /// Provides typed access to indexed columns for query building.
pub struct BotIxCols { pub struct BotIxCols {
pub config_id: __sdk::__query_builder::IxCol<Bot, u32>,
pub id: __sdk::__query_builder::IxCol<Bot, u32>, pub id: __sdk::__query_builder::IxCol<Bot, u32>,
pub lobby_id: __sdk::__query_builder::IxCol<Bot, u32>, pub lobby_id: __sdk::__query_builder::IxCol<Bot, u32>,
} }
@ -48,6 +49,7 @@ impl __sdk::__query_builder::HasIxCols for Bot {
type IxCols = BotIxCols; type IxCols = BotIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols { fn ix_cols(table_name: &'static str) -> Self::IxCols {
BotIxCols { BotIxCols {
config_id: __sdk::__query_builder::IxCol::new(table_name, "config_id"),
id: __sdk::__query_builder::IxCol::new(table_name, "id"), id: __sdk::__query_builder::IxCol::new(table_name, "id"),
lobby_id: __sdk::__query_builder::IxCol::new(table_name, "lobby_id"), lobby_id: __sdk::__query_builder::IxCol::new(table_name, "lobby_id"),
} }

View file

@ -72,6 +72,7 @@ pub struct Bot {
#[index(btree)] #[index(btree)]
pub lobby_id: u32, pub lobby_id: u32,
#[unique]
pub config_id: u32, pub config_id: u32,
} }
@ -149,6 +150,9 @@ fn view_closed_hands(ctx: &ViewContext) -> Vec<HandView> {
.players .players
.iter() .iter()
.filter_map(|id| { .filter_map(|id| {
if *id == this_player.config_id {
None
} else {
ctx.db ctx.db
.player_hand() .player_hand()
.player_id() .player_id()
@ -157,8 +161,10 @@ fn view_closed_hands(ctx: &ViewContext) -> Vec<HandView> {
player_id: hand.player_id, player_id: hand.player_id,
hand_length: hand.hand.len() as u8, hand_length: hand.hand.len() as u8,
pond: hand.pond, pond: hand.pond,
drawn: hand.turn_state == TurnState::Tsumo && hand.working_tile.is_some(), drawn: hand.turn_state == TurnState::Tsumo
&& hand.working_tile.is_some(),
}) })
}
}) })
.collect() .collect()
} else { } else {

View file

@ -5,9 +5,9 @@ use bevy::render::render_resource::AsBindGroupShaderType;
use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin}; use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin};
use jong_db::{ use jong_db::{
self, PlayerClockTableAccess, PlayerConfigTableAccess, UserTableAccess, add_bot, advance_game, self, HandView, PlayerClockTableAccess, PlayerConfigTableAccess, UserTableAccess, add_bot,
join_or_create_lobby, lobbyQueryTableAccess, player_configQueryTableAccess, set_ready, advance_game, join_or_create_lobby, lobbyQueryTableAccess, player_configQueryTableAccess,
userQueryTableAccess, set_ready, userQueryTableAccess,
}; };
use jong_db::{ use jong_db::{
BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, RemoteTables, BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, RemoteTables,
@ -61,7 +61,13 @@ impl Plugin for Riichi {
) )
.add_systems( .add_systems(
Update, Update,
(on_view_hand_insert, on_view_hand_update) (
on_view_hand_insert,
on_view_hand_update,
on_view_closed_insert,
on_view_closed_update,
)
.after(on_player_config_insert_update)
.run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))),
); );
} }
@ -165,13 +171,16 @@ fn on_player_config_insert_update(
players: Query<(Entity, &Player)>, players: Query<(Entity, &Player)>,
) { ) {
for msg in messages.read() { for msg in messages.read() {
trace!("on_player_insert_update"); trace!("on_player_config_insert_update");
let player = &msg.new; let player = &msg.new;
let player_ent = players let player_ent = players
.iter() .iter()
.find_map(|(e, p)| (p.id == player.id).then_some(e)) .find_map(|(e, p)| (p.id == player.id).then_some(e))
.unwrap_or_else(|| commands.spawn(Player { id: player.id }).id()); .unwrap_or_else(|| {
trace!("spawn_player");
commands.spawn(Player { id: player.id }).id()
});
if player.id if player.id
== stdb == stdb
@ -191,6 +200,7 @@ fn on_player_config_insert_update(
fn on_view_hand_insert( fn on_view_hand_insert(
mut messages: ReadInsertMessage<jong_db::PlayerHand>, mut messages: ReadInsertMessage<jong_db::PlayerHand>,
mut commands: Commands, mut commands: Commands,
players: Populated<(Entity, &Player, Option<&Children>)>,
tiles: Query<(Entity, &TileId)>, tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>, hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>, ponds: Query<(Entity, &Pond)>,
@ -200,6 +210,7 @@ fn on_view_hand_insert(
trace!("on_view_hand_insert"); trace!("on_view_hand_insert");
sync_open_hand( sync_open_hand(
&mut commands, &mut commands,
&players,
tiles, tiles,
hands, hands,
ponds, ponds,
@ -212,6 +223,7 @@ fn on_view_hand_insert(
fn on_view_hand_update( fn on_view_hand_update(
mut messages: ReadUpdateMessage<jong_db::PlayerHand>, mut messages: ReadUpdateMessage<jong_db::PlayerHand>,
mut commands: Commands, mut commands: Commands,
players: Populated<(Entity, &Player, Option<&Children>)>,
tiles: Query<(Entity, &TileId)>, tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>, hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>, ponds: Query<(Entity, &Pond)>,
@ -221,6 +233,7 @@ fn on_view_hand_update(
trace!("on_view_hand_update"); trace!("on_view_hand_update");
sync_open_hand( sync_open_hand(
&mut commands, &mut commands,
&players,
tiles, tiles,
hands, hands,
ponds, ponds,
@ -232,6 +245,7 @@ fn on_view_hand_update(
fn sync_open_hand( fn sync_open_hand(
commands: &mut Commands, commands: &mut Commands,
players: &Populated<(Entity, &Player, Option<&Children>)>,
tiles: Query<(Entity, &TileId)>, tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>, hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>, ponds: Query<(Entity, &Pond)>,
@ -239,14 +253,28 @@ fn sync_open_hand(
player_hand: &PlayerHand, player_hand: &PlayerHand,
) { ) {
let player_id = player_hand.player_id; let player_id = player_hand.player_id;
let (player_ent, player_children) = players
.iter()
.find_map(|(e, p, c)| (p.id == player_id).then_some((e, c)))
.unwrap();
let hand_ent = hands let hand_ent = hands
.iter() .iter()
.find_map(|(e, h)| (h.player_id == player_id).then_some(e)) .find_map(|(e, _)| player_children.is_some_and(|c| c.contains(&e)).then_some(e))
.unwrap_or_else(|| commands.spawn(Hand { player_id }).id()); .unwrap_or_else(|| {
let id = commands.spawn(Hand).id();
commands.entity(player_ent).add_child(id);
id
});
let pond_ent = ponds let pond_ent = ponds
.iter() .iter()
.find_map(|(e, p)| (p.player_id == player_id).then_some(e)) .find_map(|(e, _)| player_children.is_some_and(|c| c.contains(&e)).then_some(e))
.unwrap_or_else(|| commands.spawn(Pond { player_id }).id()); .unwrap_or_else(|| {
let id = commands.spawn(Pond).id();
commands.entity(player_ent).add_child(id);
id
});
// hand and pond both still need ability to spawn for the reconnect case // hand and pond both still need ability to spawn for the reconnect case
let hand: Vec<Entity> = player_hand let hand: Vec<Entity> = player_hand
@ -276,60 +304,97 @@ fn sync_open_hand(
if let Some(dbt) = &player_hand.working_tile if let Some(dbt) = &player_hand.working_tile
&& player_hand.turn_state == jong_db::TurnState::Tsumo && player_hand.turn_state == jong_db::TurnState::Tsumo
{ {
commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id))); let id = commands
.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)))
.id();
commands.entity(player_ent).add_child(id);
} else {
// TODO clear Drawn from auto-discarded tile
} }
next_turnstate.set(player_hand.turn_state.into()); next_turnstate.set(player_hand.turn_state.into());
} }
// fn sync_closed_hand( fn on_view_closed_insert(
// stdb: SpacetimeDB, mut messages: ReadInsertMessage<jong_db::HandView>,
mut commands: Commands,
players: Populated<(Entity, &Player, Option<&Children>)>,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
) {
for msg in messages.read() {
trace!("on_view_closed_insert");
sync_closed_hand(&mut commands, &players, tiles, hands, ponds, &msg.row);
}
}
// mut messages: MessageReader<SyncClosedHand>, fn on_view_closed_update(
// mut commands: Commands, mut messages: ReadUpdateMessage<jong_db::HandView>,
mut commands: Commands,
players: Populated<(Entity, &Player, Option<&Children>)>,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
) {
for msg in messages.read() {
trace!("on_view_closed_update");
sync_closed_hand(&mut commands, &players, tiles, hands, ponds, &msg.new);
}
}
// tiles: Query<(Entity, &TileId)>, fn sync_closed_hand(
// hands: Query<(Entity, &mut Closed, &Hand)>, commands: &mut Commands,
// ponds: Query<(Entity, &Pond)>, players: &Populated<(Entity, &Player, Option<&Children>)>,
// ) { tiles: Query<(Entity, &TileId)>,
// for SyncClosedHand(id) in messages.read() { hands: Query<(Entity, &Hand)>,
// let Some(hand_view) = stdb ponds: Query<(Entity, &Pond)>,
// .db() hand_view: &HandView,
// .view_closed_hands() ) {
// .iter() let player_id = hand_view.player_id;
// .find(|hand| PlayerOrBot::from(&hand.player) == *id)
// else {
// todo!()
// };
// let hand_ent = hands let (player_ent, player_children) = players
// .iter() .iter()
// .find_map(|(e, c, h)| (h.owner == *id).then_some(e)) .find_map(|(e, p, c)| (p.id == player_id).then_some((e, c)))
// .unwrap_or_else(|| commands.spawn(Hand { owner: *id }).id()); .unwrap();
// let pond_ent = ponds
// .iter()
// .find_map(|(e, p)| (p.owner == *id).then_some(e))
// .unwrap_or_else(|| commands.spawn(Pond { owner: *id }).id());
// let pond: Vec<Entity> = hand_view let hand_ent = hands
// .pond .iter()
// .iter() .find_map(|(e, _)| player_children.is_some_and(|c| c.contains(&e)).then_some(e))
// .map(|dbt| { .unwrap_or_else(|| {
// tiles let id = commands.spawn(Hand).id();
// .iter() commands.entity(player_ent).add_child(id);
// .find_map(|(e, i)| (i.0 == dbt.id).then_some(e)) id
// .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) });
// }) let pond_ent = ponds
// .collect(); .iter()
.find_map(|(e, _)| player_children.is_some_and(|c| c.contains(&e)).then_some(e))
.unwrap_or_else(|| {
let id = commands.spawn(Pond).id();
commands.entity(player_ent).add_child(id);
id
});
// commands.entity(hand_ent).insert(Closed { let pond: Vec<Entity> = hand_view
// length: hand_view.hand_length, .pond
// }); .iter()
// commands.entity(pond_ent).replace_children(&pond); .map(|dbt| {
tiles
.iter()
.find_map(|(e, i)| (i.0 == dbt.id).then_some(e))
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
// if hand_view.drawn { commands.entity(hand_ent).insert(Closed {
// commands.spawn(Drawn); length: hand_view.hand_length,
// } });
// } commands.entity(pond_ent).replace_children(&pond);
// }
if hand_view.drawn {
let id = commands.spawn(Drawn).id();
commands.entity(player_ent).add_child(id);
} else {
// TODO clear Drawn from auto-discarded tile
}
}

View file

@ -8,7 +8,7 @@ pub(crate) fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessa
for msg in messages.read() { for msg in messages.read() {
info!("you're now jongline"); info!("you're now jongline");
debug!("with identity: {}", stdb.identity()); // debug!("with identity: {}", stdb.identity());
creds_store() creds_store()
.save(&msg.access_token) .save(&msg.access_token)
.expect("i/o error saving token"); .expect("i/o error saving token");

View file

@ -22,20 +22,15 @@ pub struct LeftLord;
pub struct TileId(pub u32); pub struct TileId(pub u32);
#[derive(Component)] #[derive(Component)]
pub struct Hand { pub struct Hand;
pub player_id: u32,
}
#[derive(Component)] #[derive(Component)]
pub struct Closed { pub struct Closed {
pub player_id: u32,
pub(crate) length: u8, pub(crate) length: u8,
} }
#[derive(Component)] #[derive(Component)]
pub struct Pond { pub struct Pond;
pub player_id: u32,
}
#[derive(Component)] #[derive(Component)]
pub struct Drawn; pub struct Drawn;

View file

@ -1,5 +1,3 @@
#![allow(unused)]
use std::time::Duration; use std::time::Duration;
use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin};
@ -84,7 +82,7 @@ impl Plugin for TuiPlugin {
.add_systems( .add_systems(
Update, Update,
( (
(render::render_main_hand, render::render_main_pond) (render::render_main_hand, render::render_main_pond, render::render_other_hands)
.run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))),
render::render, render::render,
) )

View file

@ -7,9 +7,9 @@ use tui_logger::TuiWidgetEvent;
use jong::SpacetimeDB; use jong::SpacetimeDB;
use jong_db::{ use jong_db::{
LobbyTableAccess, UserTableAccess, botQueryTableAccess, LobbyTableAccess, UserTableAccess, botQueryTableAccess, join_or_create_lobby,
join_or_create_lobby, lobbyQueryTableAccess, player_clockQueryTableAccess, lobbyQueryTableAccess, player_clockQueryTableAccess, player_configQueryTableAccess,
player_configQueryTableAccess, view_closed_handsQueryTableAccess, view_handQueryTableAccess, view_closed_handsQueryTableAccess, view_handQueryTableAccess,
}; };
use jong_types::GameState; use jong_types::GameState;
@ -78,6 +78,14 @@ pub(crate) fn keyboard(
.player_config() .player_config()
.r#where(|pc| pc.id.eq(user.config_id)) .r#where(|pc| pc.id.eq(user.config_id))
}) })
.add_query(|q| {
q.from
.bot()
.r#where(|b| b.lobby_id.eq(lobby_id))
.right_semijoin(q.from.player_config(), |b, pc| {
b.config_id.eq(pc.id)
})
})
.add_query(|q| { .add_query(|q| {
q.from q.from
.player_clock() .player_clock()

View file

@ -7,7 +7,7 @@ use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::text::{Line, Span, Text}; use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use jong::riichi::player::*; use jong::riichi::{Lobby, player::*};
use jong_types::*; use jong_types::*;
use crate::tui::input::Hovered; use crate::tui::input::Hovered;
@ -150,9 +150,9 @@ pub(crate) fn render_main_hand(
tiles: Query<&jong_types::Tile>, tiles: Query<&jong_types::Tile>,
hovered: Query<Entity, With<Hovered>>, hovered: Query<Entity, With<Hovered>>,
main_player: Single<&Player, With<MainPlayer>>, main_player: Single<(&Player, &Children), With<MainPlayer>>,
hands: Query<(&Hand, &Children)>, hands: Query<(Entity, &Hand, &Children)>,
drawn_tile: Option<Single<Entity, With<Drawn>>>, drawn_tile: Option<Single<Entity, With<Drawn>>>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
@ -164,7 +164,7 @@ pub(crate) fn render_main_hand(
let hand: Vec<_> = hands let hand: Vec<_> = hands
.iter() .iter()
.find_map(|(h, c)| (main_player.id == h.player_id).then_some(c)) .find_map(|(e, h, c)| (main_player.1.contains(&e)).then_some(c))
.unwrap() .unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
@ -217,7 +217,9 @@ pub(crate) fn render_main_hand(
} }
// tsumo tile // tsumo tile
if let Some(drawn_tile) = drawn_tile { if let Some(drawn_tile) = drawn_tile
&& main_player.1.contains(&*drawn_tile)
{
let mut area = drawn_area.resize(Size { let mut area = drawn_area.resize(Size {
width: 5, width: 5,
height: 4, height: 4,
@ -259,9 +261,9 @@ pub(crate) fn render_main_pond(
tiles: Query<&Tile>, tiles: Query<&Tile>,
hovered: Query<Entity, With<Hovered>>, hovered: Query<Entity, With<Hovered>>,
main_player: Single<&Player, With<MainPlayer>>, main_player: Single<(&Player, &Children), With<MainPlayer>>,
pond: Query<(&Pond, &Children)>, pond: Query<(Entity, &Pond, &Children)>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
@ -271,7 +273,7 @@ pub(crate) fn render_main_pond(
let pond: Vec<_> = pond let pond: Vec<_> = pond
.iter() .iter()
.find_map(|(p, c)| (main_player.id == p.player_id).then_some(c)) .find_map(|(e, p, c)| (main_player.1.contains(&e)).then_some(c))
.unwrap() .unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
@ -289,4 +291,51 @@ pub(crate) fn render_main_pond(
Ok(()) Ok(())
} }
pub(crate) fn render_other_hands() {} pub(crate) fn render_other_hands(
mut tui: ResMut<RatatuiContext>,
layouts: Res<HandLayouts>,
lobby: Res<Lobby>,
main_player: Single<&Player, With<MainPlayer>>,
players: Query<(&Player, &Children), Without<MainPlayer>>,
hands: Query<(Entity, &Closed)>,
ponds: Query<(Entity, &Children), With<Pond>>,
drawn: Option<Single<Entity, With<Drawn>>>,
) {
let mut frame = tui.get_frame();
let main_idx = lobby
.players
.iter()
.position(|id| *id == main_player.id)
.unwrap();
let mut player_it = lobby.players.iter().copied().cycle().skip(main_idx + 1);
let right_id = player_it.next().unwrap();
let top_id = player_it.next().unwrap();
let left_id = player_it.next().unwrap();
{
let children = players
.iter()
.find_map(|(p, c)| (p.id == right_id).then_some(c))
.unwrap();
let hand = hands
.iter()
.find_map(|(e, h)| children.contains(&e).then_some(h))
.unwrap();
// let pond = ponds
// .iter()
// .find_map(|(e, c)| children.contains(&e).then_some(c))
// .unwrap();
let drawn = drawn.is_some_and(|e| children.contains(&*e));
// let hand_draw_meld = Layout::vertical([
// Constraint::()
// ])
}
{}
{}
}