Compare commits

...

2 commits

Author SHA1 Message Date
Tao Tien
875c3fb5bb discarding proper now 2026-02-19 00:32:27 -08:00
Tao Tien
c3686221aa more refactor, start using stdb for everything???
4.5th rewrite for tui
2026-02-18 23:50:45 -08:00
30 changed files with 513 additions and 774 deletions

1
.helix/ignore Normal file
View file

@ -0,0 +1 @@
jong-db

View file

@ -1,6 +1,6 @@
# [language-server.lspmux]
# command = "lspmux"
[language-server.lspmux]
command = "lspmux"
# [[language]]
# name = "rust"
# language-servers = ["lspmux"]
[[language]]
name = "rust"
language-servers = ["lspmux"]

View file

@ -4,10 +4,10 @@
...
}: rec {
# https://devenv.sh/processes/
# processes.lspmux.exec = "lspmux server";
processes.lspmux.exec = "lspmux server";
processes.spacetimedb_start.exec = "spacetime start";
processes.spacetimedb_dev = {
exec = "spacetime dev --module-bindings-path jong-db/src/db jong-line --delete-data=always";
exec = "just spacetime_dev";
# notify.enable = true;
# TODO features not yet supp???
# restart = "always";

View file

@ -1,54 +1,56 @@
pub mod db;
pub use db::*;
impl From<GameState> for jong_types::GameState {
fn from(value: GameState) -> Self {
Self::from_repr(value as usize).unwrap()
mod conversions {
impl From<crate::db::GameState> for jong_types::states::GameState {
fn from(value: crate::db::GameState) -> Self {
Self::from_repr(value as usize).unwrap()
}
}
}
impl From<TurnState> for jong_types::TurnState {
fn from(value: TurnState) -> Self {
Self::from_repr(value as usize).unwrap()
impl From<crate::db::TurnState> for jong_types::states::TurnState {
fn from(value: crate::db::TurnState) -> Self {
Self::from_repr(value as usize).unwrap()
}
}
}
impl From<&Tile> for jong_types::Tile {
fn from(value: &tile_type::Tile) -> Self {
Self {
suit: value.suit.clone().into(),
impl From<&crate::db::Tile> for jong_types::tiles::Tile {
fn from(value: &crate::db::Tile) -> Self {
Self {
suit: value.suit.clone().into(),
}
}
}
impl From<crate::db::Suit> for jong_types::tiles::Suit {
fn from(value: crate::db::Suit) -> Self {
match value {
crate::db::Suit::Man(rank) => Self::Man(rank.into()),
crate::db::Suit::Pin(rank) => Self::Pin(rank.into()),
crate::db::Suit::Sou(rank) => Self::Sou(rank.into()),
crate::db::Suit::Wind(wind) => Self::Wind(wind.into()),
crate::db::Suit::Dragon(dragon) => Self::Dragon(dragon.into()),
}
}
}
impl From<crate::db::Rank> for jong_types::tiles::Rank {
fn from(value: crate::db::Rank) -> Self {
Self {
number: value.number,
}
}
}
impl From<crate::db::Wind> for jong_types::tiles::Wind {
fn from(value: crate::db::Wind) -> Self {
Self::from_repr(value as usize).unwrap()
}
}
impl From<crate::db::Dragon> for jong_types::tiles::Dragon {
fn from(value: crate::db::Dragon) -> Self {
Self::from_repr(value as usize).unwrap()
}
}
}
impl From<Suit> for jong_types::Suit {
fn from(value: Suit) -> Self {
match value {
Suit::Man(rank) => Self::Man(rank.into()),
Suit::Pin(rank) => Self::Pin(rank.into()),
Suit::Sou(rank) => Self::Sou(rank.into()),
Suit::Wind(wind) => Self::Wind(wind.into()),
Suit::Dragon(dragon) => Self::Dragon(dragon.into()),
}
}
}
impl From<Rank> for jong_types::Rank {
fn from(value: Rank) -> Self {
Self {
number: value.number,
}
}
}
impl From<Wind> for jong_types::Wind {
fn from(value: Wind) -> Self {
Self::from_repr(value as usize).unwrap()
}
}
impl From<Dragon> for jong_types::Dragon {
fn from(value: Dragon) -> Self {
Self::from_repr(value as usize).unwrap()
}
}

View file

@ -4,7 +4,9 @@ use spacetimedb::{ReducerContext, Table, reducer};
use crate::tables::{player::player, *};
mod reducers {
mod game;
mod deal;
mod hand;
mod lobby;
}
mod tables;
@ -13,6 +15,18 @@ pub fn clear_all(ctx: &ReducerContext) {
for row in ctx.db.player().iter() {
ctx.db.player().delete(row);
}
for row in ctx.db.lobby().iter() {
ctx.db.lobby().delete(row);
}
for row in ctx.db.bot().iter() {
ctx.db.bot().delete(row);
}
for row in ctx.db.wall().iter() {
ctx.db.wall().delete(row);
}
for row in ctx.db.tile().iter() {
ctx.db.tile().delete(row);
}
}
#[reducer(client_connected)]

View file

@ -3,15 +3,14 @@ use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer};
use super::hand::deal_hands;
use crate::tables::*;
use jong_types::*;
#[reducer]
pub fn shuffle_deal(ctx: &ReducerContext, lobby_id: u32) {
debug!("lobby_id: {lobby_id}");
let mut lobby = ctx.db.lobby().id().find(lobby_id).unwrap();
if lobby.game_state == GameState::Setup {
lobby.game_state = GameState::Deal;
if lobby.game_state == jong_types::states::GameState::Setup {
lobby.game_state = jong_types::states::GameState::Deal;
lobby = ctx.db.lobby().id().update(lobby);
let tiles = new_shuffled_wall(ctx);
@ -24,15 +23,15 @@ pub fn shuffle_deal(ctx: &ReducerContext, lobby_id: u32) {
deal_hands(ctx, lobby_id);
lobby.game_state = GameState::Play;
lobby.turn_state = TurnState::Tsumo;
lobby.game_state = jong_types::states::GameState::Play;
lobby.turn_state = jong_types::states::TurnState::Tsumo;
ctx.db.lobby().id().update(lobby);
}
}
pub fn new_shuffled_wall(ctx: &ReducerContext) -> Vec<DbTile> {
let mut rng = ctx.rng();
let mut wall: Vec<_> = tiles()
let mut wall: Vec<_> = jong_types::tiles::tiles()
.into_iter()
.map(|tile| ctx.db.tile().insert(DbTile { id: 0, tile }))
.collect();

View file

@ -1,8 +1,8 @@
use log::{debug, trace};
use spacetimedb::{ReducerContext, Table, reducer};
use spacetimedb::{ReducerContext, reducer};
use crate::tables::{player::player, *};
use jong_types::*;
use jong_types::states::TurnState;
pub fn deal_hands(ctx: &ReducerContext, lobby_id: u32) {
let players = ctx.db.player().lobby_id().filter(lobby_id);
@ -46,17 +46,33 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
let mut player = ctx.db.player().identity().find(ctx.sender).unwrap();
let mut lobby = ctx.db.lobby().id().find(player.lobby_id).unwrap();
let dealt_tile = if let Some(drawn) = player.drawn_tile
&& drawn.id == tile_id
{
drawn
} else if let Some((i, _)) = player
.hand
.iter()
.enumerate()
.find(|(_, t)| t.id == tile_id)
{
player.hand.remove(i)
let dealt_tile = if let Some(dealt) = ctx.db.tile().id().find(tile_id) {
if let Some(drawn) = player.drawn_tile {
if drawn.id == dealt.id {
dealt
} else if let Some((i, _)) = player
.hand
.iter()
.enumerate()
.find(|(_, t)| t.id == tile_id)
{
let dealt = player.hand.remove(i);
player.hand.push(drawn);
player.hand.sort_by_key(|t| t.tile);
dealt
} else {
return Err(format!(
"player {} attempted to deal tile {} not in hand or drawn",
player.id, tile_id
));
}
} else {
return Err(format!(
"player {} attempted to deal tile {} without having drawn",
player.id, tile_id
));
}
} else {
return Err(format!(
"player {} attempted to deal nonexistant tile {}",
@ -79,7 +95,7 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
#[reducer]
pub fn skip_call(ctx: &ReducerContext) {
trace!("skip_call");
let player = ctx.db.player().identity().find(ctx.sender).unwrap();
let mut lobby = ctx.db.lobby().id().find(player.lobby_id).unwrap();

View file

@ -2,10 +2,6 @@ use log::info;
use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer};
use crate::tables::{player::player, *};
use jong_types::*;
mod deal;
mod hand;
#[reducer]
pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> {
@ -22,12 +18,12 @@ pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(
id: 0,
host_player_id: player.id,
players: vec![PlayerOrBot::Player { id: player.id }],
game_state: GameState::Lobby,
turn_state: TurnState::None,
game_state: jong_types::states::GameState::Lobby,
turn_state: jong_types::states::TurnState::None,
dealer_idx: 0,
current_idx: 0,
});
info!("created lobby: {:?}", lobby);
info!("created lobby: {}", lobby.id);
lobby_id = lobby.id;
}
@ -83,7 +79,7 @@ pub fn start_game(ctx: &ReducerContext) {
PlayerOrBot::Bot { id } => ctx.db.bot().id().find(id).is_some(),
})
{
lobby.game_state = GameState::Setup;
lobby.game_state = jong_types::states::GameState::Setup;
lobby.players.shuffle(&mut ctx.rng());
lobby.dealer_idx += 1;
if lobby.dealer_idx > 3 {

View file

@ -1,6 +1,9 @@
use spacetimedb::table;
use spacetimedb::{SpacetimeType, table};
use jong_types::*;
use jong_types::{
tiles::Tile,
states::{GameState, TurnState},
};
pub mod player;
pub use player::*;
@ -14,7 +17,7 @@ pub struct Lobby {
#[unique]
pub host_player_id: u32,
pub players: Vec<player::PlayerOrBot>,
pub players: Vec<PlayerOrBot>,
pub dealer_idx: u8,
pub current_idx: u8,
@ -37,5 +40,11 @@ pub struct DbTile {
#[auto_inc]
pub id: u32,
pub tile: jong_types::Tile,
pub tile: Tile,
}
#[derive(Debug, Clone, SpacetimeType)]
pub enum PlayerOrBot {
Player { id: u32 },
Bot { id: u32 },
}

View file

@ -42,9 +42,3 @@ pub struct Bot {
pub drawn_tile: Option<DbTile>,
}
#[derive(Debug, Clone, SpacetimeType)]
pub enum PlayerOrBot {
Player { id: u32 },
Bot { id: u32 },
}

View file

@ -9,127 +9,8 @@ mod derive_alias {
}
use derive_aliases::derive;
use bevy::prelude::*;
use spacetimedb::SpacetimeType;
use strum::{EnumCount, FromRepr};
pub mod states;
pub mod tiles;
#[derive(..Base, Hash, Default, FromRepr)]
#[derive(States, SpacetimeType)]
pub enum GameState {
#[default]
None,
Lobby,
Setup,
Deal,
Play,
Exit,
}
#[derive(..Base)]
#[derive(Component, SpacetimeType)]
pub struct Tile {
pub suit: Suit,
}
#[derive(..Base)]
#[derive(SpacetimeType)]
pub enum Suit {
Man(Rank),
Pin(Rank),
Sou(Rank),
Wind(Wind),
Dragon(Dragon),
}
impl Suit {
pub fn rank(&self) -> Option<Rank> {
match self {
Suit::Man(rank) => Some(*rank),
Suit::Pin(rank) => Some(*rank),
Suit::Sou(rank) => Some(*rank),
// Suit::Wind(wind) | Suit::Dragon(dragon) => None,
_ => None,
}
}
}
#[derive(
..Base,
Deref,
DerefMut,
)]
#[derive(SpacetimeType)]
pub struct Rank {
pub number: u8,
}
#[derive(
..Base,
FromRepr,
)]
#[derive(SpacetimeType)]
pub enum Wind {
Ton,
Nan,
Shaa,
Pei,
}
#[derive(
..Base,
FromRepr,
)]
#[derive(SpacetimeType)]
pub enum Dragon {
Haku,
Hatsu,
Chun,
}
pub fn tiles() -> Vec<Tile> {
let mut tiles = vec![];
for _ in 0..4 {
for i in 1..=9 {
tiles.push(Tile {
suit: Suit::Pin(Rank { number: i }),
});
tiles.push(Tile {
suit: Suit::Sou(Rank { number: i }),
});
tiles.push(Tile {
suit: Suit::Man(Rank { number: i }),
});
}
for i in 0..4 {
tiles.push(Tile {
suit: Suit::Wind(Wind::from_repr(i).unwrap()),
});
}
for i in 0..3 {
tiles.push(Tile {
suit: Suit::Dragon(Dragon::from_repr(i).unwrap()),
});
}
}
tiles
}
#[derive(
Default,
..Copy,
PartialEq,
Eq,
Hash,
Debug,
)]
#[derive(SubStates, FromRepr, EnumCount, SpacetimeType)]
#[source(GameState = GameState::Play)]
pub enum TurnState {
#[default]
None,
Tsumo,
Menzen,
RiichiKan,
RonChiiPonKan,
End,
}
pub use states::*;
pub use tiles::*;

37
jong-types/src/states.rs Normal file
View file

@ -0,0 +1,37 @@
use bevy::prelude::*;
use spacetimedb::SpacetimeType;
use strum::{EnumCount, FromRepr};
use super::derive;
#[derive(..Base, Hash, Default, FromRepr)]
#[derive(States, SpacetimeType)]
pub enum GameState {
#[default]
None,
Lobby,
Setup,
Deal,
Play,
Exit,
}
#[derive(
Default,
..Copy,
PartialEq,
Eq,
Hash,
Debug,
)]
#[derive(SubStates, FromRepr, EnumCount, SpacetimeType)]
#[source(GameState = GameState::Play)]
pub enum TurnState {
#[default]
None,
Tsumo,
Menzen,
RiichiKan,
RonChiiPonKan,
End,
}

94
jong-types/src/tiles.rs Normal file
View file

@ -0,0 +1,94 @@
use bevy::prelude::*;
use spacetimedb::SpacetimeType;
use strum::{EnumCount, FromRepr};
use super::derive;
#[derive(..Base)]
#[derive(Component, SpacetimeType)]
pub struct Tile {
pub suit: Suit,
}
#[derive(..Base)]
#[derive(SpacetimeType)]
pub enum Suit {
Man(Rank),
Pin(Rank),
Sou(Rank),
Wind(Wind),
Dragon(Dragon),
}
impl Suit {
pub fn rank(&self) -> Option<Rank> {
match self {
Suit::Man(rank) => Some(*rank),
Suit::Pin(rank) => Some(*rank),
Suit::Sou(rank) => Some(*rank),
// Suit::Wind(wind) | Suit::Dragon(dragon) => None,
_ => None,
}
}
}
#[derive(
..Base,
Deref,
DerefMut,
)]
#[derive(SpacetimeType)]
pub struct Rank {
pub number: u8,
}
#[derive(
..Base,
FromRepr,
)]
#[derive(SpacetimeType)]
pub enum Wind {
Ton,
Nan,
Shaa,
Pei,
}
#[derive(
..Base,
FromRepr,
)]
#[derive(SpacetimeType)]
pub enum Dragon {
Haku,
Hatsu,
Chun,
}
pub fn tiles() -> Vec<Tile> {
let mut tiles = vec![];
for _ in 0..4 {
for i in 1..=9 {
tiles.push(Tile {
suit: Suit::Pin(Rank { number: i }),
});
tiles.push(Tile {
suit: Suit::Sou(Rank { number: i }),
});
tiles.push(Tile {
suit: Suit::Man(Rank { number: i }),
});
}
for i in 0..4 {
tiles.push(Tile {
suit: Suit::Wind(Wind::from_repr(i).unwrap()),
});
}
for i in 0..3 {
tiles.push(Tile {
suit: Suit::Dragon(Dragon::from_repr(i).unwrap()),
});
}
}
tiles
}

View file

@ -1,41 +0,0 @@
use bevy::prelude::*;
use jong_types::*;
#[derive(Component)]
pub struct Hand;
#[derive(Component)]
pub struct Pond;
#[derive(Component)]
pub struct Drawn;
#[derive(Component)]
pub struct Discarded;
// #[derive(Component, Default)]
// enum SortHand {
// #[default]
// Unsorted,
// Sort,
// Manual,
// }
/// assumes hand is sorted
pub(crate) fn check_wincon(_hand: &[Tile; 14], _melds: &[&[Tile]]) -> bool {
// 4x3 + pair
// assume sorted
//
// let melds = hand.iter().array_chunks::<3>().all(|tiles| {
// let suit = discriminant(&tiles[0].suit);
// let starting_rank = tiles[0].suit
// // tiles.iter().all(|t| discriminant(&t.suit) == suit) && tiles.iter().zip(tiles[0].suit.rank())
// }) && melds.iter().all(|meld| todo!());
// let eyeball = todo!();
todo!();
// melds && eyeball
}

View file

@ -1,21 +0,0 @@
use bevy::prelude::*;
#[derive(Component, Debug, PartialEq)]
pub struct Player {
pub name: String,
}
#[derive(Component)]
pub struct Points(pub isize);
#[derive(Component)]
pub struct MainPlayer;
#[derive(Component)]
pub struct CurrentPlayer;
#[derive(Component)]
pub struct Dealer;
#[derive(Component)]
pub struct Tsumo;

View file

@ -1,232 +0,0 @@
use bevy::{platform::collections::HashMap, prelude::*};
use strum::{EnumCount, FromRepr};
use crate::EnumNextCycle;
use jong_types::TurnState;
// #[derive(Resource)]
// pub struct CurrentPlayer(pub Entity);
#[derive(Resource)]
pub(crate) struct MatchSettings {
pub(crate) starting_points: isize,
pub(crate) player_count: u8,
}
#[derive(Component)]
pub(crate) struct Dice(u8, u8);
#[derive(Resource)]
pub(crate) struct Compass {
pub(crate) prevalent_wind: Wind,
pub(crate) round: u8,
pub(crate) dealer_wind: Wind,
pub(crate) riichi: usize,
pub(crate) honba: usize,
}
#[derive(Component, Clone, Copy, FromRepr, EnumCount, PartialEq)]
pub enum Wind {
Ton,
Nan,
Shaa,
Pei,
}
pub enum WindRelation {
Shimocha,
Toimen,
Kamicha,
}
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CallType {
Skip,
Ron,
Chii,
Pon,
Kan,
}
impl Default for MatchSettings {
fn default() -> Self {
Self {
starting_points: 25000,
player_count: 4,
}
}
}
impl Default for Compass {
fn default() -> Self {
Self {
prevalent_wind: Wind::Ton,
round: 1,
dealer_wind: Wind::Ton,
riichi: 0,
honba: 0,
}
}
}
impl EnumNextCycle for Wind {
fn next(&self) -> Self {
if (*self as usize + 1) >= Self::COUNT {
Self::from_repr(0).unwrap()
} else {
Self::from_repr(*self as usize + 1).unwrap()
}
}
}
impl Wind {
pub fn relate(&self, other: &Self) -> WindRelation {
if self.next() == *other {
WindRelation::Shimocha
} else if other.next() == *self {
WindRelation::Kamicha
} else {
WindRelation::Toimen
}
}
}
impl EnumNextCycle for TurnState {
fn next(&self) -> Self {
if (*self as usize + 1) >= Self::COUNT {
Self::from_repr(0).unwrap()
} else {
Self::from_repr(*self as usize + 1).unwrap()
}
}
}
// pub(crate) fn tsumo(
// mut commands: Commands,
// // curr_player: Res<CurrentPlayer>,
// curr_player: Single<Entity, With<CurrentPlayer>>,
// wall: Single<Entity, With<Wall>>,
// walltiles: Single<&Children, With<Wall>>,
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) {
// let drawn = walltiles.last().unwrap();
// commands.entity(*wall).remove_child(*drawn);
// let drawn = commands.entity(*drawn).insert(Drawn).id();
// commands.entity(*curr_player).add_child(drawn);
// debug!("tsumo for: {:?}, tile: {:?}", *curr_player, drawn);
// next_turnstate.set(curr_turnstate.next());
// }
// pub(crate) fn menzen(
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) {
// trace!("menzen check");
// next_turnstate.set(curr_turnstate.next());
// }
// pub(crate) fn riichi_kan(
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) {
// trace!("riichi_kan");
// next_turnstate.set(curr_turnstate.next());
// }
// #[allow(clippy::too_many_arguments, irrefutable_let_patterns)]
// pub(crate) fn discard(
// mut commands: Commands,
// mut reader: MessageReader<GameMessage>,
// curr_player: Single<Entity, With<CurrentPlayer>>,
// players: Query<&Children, With<Player>>,
// mut hands: Query<(&Children, Entity), (With<Hand>, Without<Player>)>,
// drawn: Single<Entity, With<Drawn>>,
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) -> Result {
// // trace!("discard");
// let (handtiles, hand) = hands.get_mut(players.get(*curr_player)?.iter().next().unwrap())?;
// let mut done = false;
// while let Some(message) = reader.read().next() {
// if let GameMessage::Discarded(discarded) = message {
// debug!("discarded: {discarded:?}");
// if *discarded == *drawn {
// } else if handtiles.contains(discarded) {
// commands
// .entity(hand)
// .remove_child(*discarded)
// .add_child(*drawn);
// } else {
// panic!("current hand nor drawn tile contains discarded tile")
// }
// commands.entity(*drawn).remove::<Drawn>();
// commands.entity(*discarded).insert(Discarded);
// done = true;
// break;
// }
// }
// if done {
// next_turnstate.set(curr_turnstate.next());
// }
// Ok(())
// }
#[derive(Resource)]
pub struct PendingCalls {
eligible: Vec<Entity>,
calls: HashMap<Entity, CallType>,
}
// pub(crate) fn notify_callable() {}
// pub(crate) fn ron_chi_pon_kan(
// mut commands: Commands,
// mut reader: MessageReader<GameMessage>,
// discarded: Single<Entity, With<Discarded>>,
// mut ponds: Query<(&Children, Entity), (With<Pond>, Without<Player>)>,
// calls: Query<&CallType>,
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) {
// // check if can call?
// // message players?
// // collect then prioritize
// // let mut received = vec![];
// let mut received: Vec<_> = reader
// .read()
// .filter_map(|m| {
// if let GameMessage::Called { player, calltype } = m
// && let Ok(calltype) = calls.get(*calltype)
// {
// Some((calltype, player))
// } else {
// None
// }
// })
// .collect();
// // received.sort_unstable_by_key(|(c, t)| c);
// // received.sort_unstable_by_key(|m| m.);
// next_turnstate.set(curr_turnstate.next());
// }
// pub(crate) fn end(
// curr_turnstate: Res<State<TurnState>>,
// mut next_turnstate: ResMut<NextState<TurnState>>,
// ) {
// next_turnstate.set(curr_turnstate.next());
// }

View file

@ -1,7 +0,0 @@
use bevy::prelude::*;
#[derive(Component)]
pub struct Wall;
#[derive(Component)]
pub struct Dead;

View file

@ -1,29 +0,0 @@
use bevy::{color::palettes::css::GREEN, prelude::*};
pub(crate) fn init_environment(mut commands: Commands) {
commands.spawn((
DirectionalLight {
shadows_enabled: true,
..default()
},
// Transform::from_xyz(),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-200.5, 100., 0.).looking_at(Vec3::ZERO, Vec3::Y),
));
}
pub(crate) fn init_table(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let green: Color = GREEN.into();
let table = Cuboid::new(1000., 5., 1000.);
commands.spawn((
Mesh3d(meshes.add(table)),
MeshMaterial3d(materials.add(green)),
));
}

View file

@ -4,9 +4,7 @@ use bevy::prelude::*;
use bevy_spacetimedb::StdbConnection;
use spacetimedb_sdk::credentials;
pub mod game;
pub mod tile;
pub mod yakus;
pub mod riichi;
trait EnumNextCycle {
fn next(&self) -> Self;
@ -15,6 +13,5 @@ trait EnumNextCycle {
pub type SpacetimeDB<'a> = Res<'a, StdbConnection<jong_db::DbConnection>>;
fn creds_store() -> credentials::File {
credentials::File::new("jongline")
credentials::File::new("jong-line")
}

View file

@ -3,7 +3,7 @@ use clap::{Parser, Subcommand};
use tracing::Level;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod gui;
// mod gui;
mod tui;
#[derive(Parser)]
@ -25,15 +25,11 @@ fn main() {
let mut app = App::new();
let app = match args.mode {
Mode::RunGui => {
app.add_plugins(DefaultPlugins.set(LogPlugin {
filter: FILTERSTRING.into(),
level: Level::TRACE,
// custom_layer: todo!(),
// fmt_layer: todo!(),
..Default::default()
}))
}
Mode::RunGui => app.add_plugins(DefaultPlugins.set(LogPlugin {
filter: FILTERSTRING.into(),
level: Level::TRACE,
..Default::default()
})),
Mode::RunTui => {
tracing_subscriber::registry()
.with(tui_logger::TuiTracingSubscriberLayer)
@ -45,7 +41,7 @@ fn main() {
}
};
app.add_plugins(jong::game::Riichi);
app.add_plugins(jong::riichi::Riichi);
app.run();
}

View file

@ -2,27 +2,22 @@ use bevy::prelude::*;
use bevy_spacetimedb::{
ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage, StdbPlugin,
};
use spacetimedb_sdk::{DbContext, Table};
use crate::{
SpacetimeDB, creds_store,
game::hand::{Drawn, Hand, Pond},
};
use jong_db::{self, DbConnection, LobbyTableAccess, PlayerTableAccess, RemoteTables};
use jong_db::{add_bot, draw_tile, set_ready, shuffle_deal, skip_call, start_game};
use jong_types::*;
pub mod hand;
pub mod player;
pub mod round;
pub mod wall;
use crate::riichi::player::*;
use crate::{SpacetimeDB, creds_store};
// pub mod round;
pub struct Riichi;
impl Plugin for Riichi {
fn build(&self, app: &mut App) {
let plugins = StdbPlugin::default()
.with_uri("http://localhost:3000")
.with_module_name("jongline")
.with_module_name("jong-line")
.with_run_fn(DbConnection::run_threaded)
// TODO why don't I need to call add_reducer?
@ -41,8 +36,8 @@ impl Plugin for Riichi {
};
app.add_plugins(plugins)
.init_state::<GameState>()
.add_sub_state::<TurnState>()
.init_state::<jong_types::states::GameState>()
.add_sub_state::<jong_types::states::TurnState>()
// .init_resource::<round::MatchSettings>()
// .init_resource::<round::Compass>()
// .add_systems(Startup, tile::init_tiles)
@ -70,6 +65,7 @@ fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage, _comman
for msg in messages.read() {
info!("you're now jongline");
// FIXME hack that doesn't work for startup crash?
while stdb.try_identity().is_none() {}
debug!("with identity: {}", stdb.identity());
@ -91,6 +87,7 @@ fn subscriptions(stdb: SpacetimeDB) {
.on_applied(|_| trace!("made all subs!"))
.on_error(|_, err| error!("sub failed: {err}"))
.subscribe([
// TODO until views work
format!(
"SELECT * FROM player p WHERE p.identity = '{}'",
stdb.identity()
@ -100,61 +97,40 @@ fn subscriptions(stdb: SpacetimeDB) {
// .subscribe_to_all_tables();
}
#[derive(Component)]
pub struct TileId(pub u32);
fn on_player_insert_update(
_stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Player>,
mut commands: Commands,
tiles: Query<(&Tile, &TileId, Entity)>,
player: Option<Single<&mut player::Player>>,
hand_ent: Option<Single<Entity, With<Hand>>>,
hand: Option<Single<Entity, With<Hand>>>,
tiles: Query<(Entity, &TileId)>,
) {
use player::*;
let hand = if hand.is_none() {
let hand = commands.spawn(Hand).id();
hand
} else {
*hand.unwrap()
};
for msg in messages.read() {
// debug!("player_insert_update msg:\n{:#?}", msg.new);
if let (Some(_player), Some(hand_ent)) = (player.as_ref(), hand_ent.as_ref()) {
// if msg.old.as_ref().is_some_and(|m| !m.ready) && msg.new.ready {
// trace!("entered ready");
// // TODO add a start game button in the future
// stdb.reducers().start_game().unwrap();
// }
let tiles: Vec<_> = msg
.new
.hand
.iter()
.map(|dbt| {
// TODO this seems a lil expensive
if let Some(ent) = tiles
.iter()
.find(|(_, id, _)| id.0 == dbt.id)
.map(|(_, _, e)| e)
{
ent
} else {
commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()
}
})
.collect();
commands.entity(**hand_ent).replace_children(&tiles);
if let Some(dbt) = &msg.new.drawn_tile {
commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id), Drawn));
}
} else {
let player = Player {
name: msg
.new
.name
.as_ref()
.unwrap_or(&"nameless".to_string())
.clone(),
};
let bundle = (player, Hand, Pond, MainPlayer, CurrentPlayer);
commands.spawn(bundle);
let hand_tiles: Vec<_> = msg
.new
.hand
.iter()
.map(|dbt| {
tiles
.iter()
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None })
.or_else(|| Some(commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()))
.unwrap()
})
.collect();
debug!("hand_tiles: {hand_tiles:?}");
commands.entity(hand).replace_children(&hand_tiles);
if let Some(dbt) = &msg.new.drawn_tile {
commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id), Drawn));
}
}
}
@ -163,9 +139,9 @@ fn on_lobby_insert_update(
stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Lobby>,
_commands: Commands,
mut next_gamestate: ResMut<NextState<GameState>>,
mut next_turnstate: ResMut<NextState<TurnState>>,
commands: Commands,
mut next_gamestate: ResMut<NextState<jong_types::states::GameState>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
for msg in messages.read() {
// trace!("on_lobby_insert_update msg:\n{:#?}", msg.new);
@ -212,7 +188,6 @@ fn on_lobby_insert_update(
stdb.reducers().skip_call().unwrap();
}
jong_db::TurnState::End => todo!(),
// _ => todo!(),
}
next_turnstate.set(msg.new.turn_state.into());
}

25
jong/src/riichi/player.rs Normal file
View file

@ -0,0 +1,25 @@
use bevy::prelude::*;
#[derive(Component)]
pub struct Player;
#[derive(Component)]
pub struct MainPlayer;
#[derive(Component)]
pub struct CurrentPlayer;
#[derive(Component, PartialEq, Eq)]
pub struct TileId(pub u32);
#[derive(Component)]
pub struct Hand;
#[derive(Component)]
pub struct Pond;
#[derive(Component)]
pub struct Drawn;
#[derive(Component)]
pub struct Discarded;

100
jong/src/riichi/round.rs Normal file
View file

@ -0,0 +1,100 @@
use bevy::{platform::collections::HashMap, prelude::*};
use strum::{EnumCount, FromRepr};
use crate::EnumNextCycle;
use jong_types::states::TurnState;
// #[derive(Resource)]
// pub struct CurrentPlayer(pub Entity);
#[derive(Resource)]
pub(crate) struct MatchSettings {
pub(crate) starting_points: isize,
pub(crate) player_count: u8,
}
#[derive(Component)]
pub(crate) struct Dice(u8, u8);
#[derive(Resource)]
pub(crate) struct Compass {
pub(crate) prevalent_wind: Wind,
pub(crate) round: u8,
pub(crate) dealer_wind: Wind,
pub(crate) riichi: usize,
pub(crate) honba: usize,
}
pub enum WindRelation {
Shimocha,
Toimen,
Kamicha,
}
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CallType {
Skip,
Ron,
Chii,
Pon,
Kan,
}
impl Default for MatchSettings {
fn default() -> Self {
Self {
starting_points: 25000,
player_count: 4,
}
}
}
impl Default for Compass {
fn default() -> Self {
Self {
prevalent_wind: Wind::Ton,
round: 1,
dealer_wind: Wind::Ton,
riichi: 0,
honba: 0,
}
}
}
impl EnumNextCycle for Wind {
fn next(&self) -> Self {
if (*self as usize + 1) >= Self::COUNT {
Self::from_repr(0).unwrap()
} else {
Self::from_repr(*self as usize + 1).unwrap()
}
}
}
impl Wind {
pub fn relate(&self, other: &Self) -> WindRelation {
if self.next() == *other {
WindRelation::Shimocha
} else if other.next() == *self {
WindRelation::Kamicha
} else {
WindRelation::Toimen
}
}
}
impl EnumNextCycle for TurnState {
fn next(&self) -> Self {
if (*self as usize + 1) >= Self::COUNT {
Self::from_repr(0).unwrap()
} else {
Self::from_repr(*self as usize + 1).unwrap()
}
}
}
#[derive(Resource)]
pub struct PendingCalls {
eligible: Vec<Entity>,
calls: HashMap<Entity, CallType>,
}

View file

@ -1,11 +0,0 @@
use bevy::prelude::*;
use jong_types::*;
#[derive(Component)]
pub struct Dora;
pub fn init_tiles(mut commands: Commands) {
let tiles = tiles();
commands.spawn_batch(tiles);
}

View file

@ -4,35 +4,17 @@ use std::time::Duration;
use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin};
use bevy_ratatui::RatatuiPlugins;
use jong::game::TileId;
use jong::game::player::MainPlayer;
use tui_logger::TuiWidgetState;
use crate::tui::{input::ConfirmSelect, states::ConsoleWidget};
use jong::{
SpacetimeDB,
game::{
hand::{Drawn, Hand},
player::{CurrentPlayer, Player},
},
};
use jong::{SpacetimeDB, riichi::player::*};
use jong_db::{self, discard_tile as _};
use jong_types::{GameState, TurnState};
use jong_types::states::{GameState, TurnState};
mod input;
mod layout;
mod render;
#[derive(Default)]
pub struct TuiPlugin;
#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
pub enum TuiSet {
Input,
Layout,
Render,
}
use input::ConfirmSelect;
use states::ConsoleWidget;
mod states {
use bevy::prelude::*;
use tui_logger::TuiWidgetState;
@ -59,6 +41,16 @@ mod states {
// }
}
#[derive(Default)]
pub struct TuiPlugin;
#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
pub enum TuiSet {
Input,
Layout,
Render,
}
impl Plugin for TuiPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
@ -93,7 +85,7 @@ impl Plugin for TuiPlugin {
.add_systems(
Update,
(
render::render_hands.run_if(in_state(GameState::Play)),
render::render_hand.run_if(in_state(GameState::Play)),
render::render,
)
.chain()
@ -109,20 +101,9 @@ fn discard_tile(
mut commands: Commands,
drawn: Single<(Entity, &TileId), With<Drawn>>,
// curr_player: Single<Entity, With<CurrentPlayer>>,
// player_hands: Populated<(&Player, &Children), With<Hand>>,
// hands: Populated<&Children, (With<Hand>, Without<Player>)>,
main_player: Single<(&Player, Entity), With<MainPlayer>>,
hands: Query<(&Children, Entity), With<Hand>>,
tiles: Query<&TileId>,
) {
// trace!("discard_tile");
let (hand, hand_ent) = hands.iter().next().unwrap();
while let Some(message) = selected.read().next()
// && (message.0 == drawn.0 || hand.contains(&message.0))
{
while let Some(message) = selected.read().next() {
if let Ok(tile_id) = tiles.get(message.0) {
stdb.reducers().discard_tile(tile_id.0).unwrap();
commands.get_entity(drawn.0).unwrap().despawn();

View file

@ -7,7 +7,6 @@ use jong_db::start_game;
use tui_logger::TuiWidgetEvent;
use jong::SpacetimeDB;
use jong_types::GameState;
use crate::tui::layout::Overlays;
use crate::tui::states::ConsoleWidget;
@ -21,9 +20,9 @@ pub(crate) fn keyboard(
mut consolewidget: ResMut<ConsoleWidget>,
mut exit: MessageWriter<AppExit>,
curr_gamestate: Res<State<GameState>>,
curr_gamestate: Res<State<jong_types::states::GameState>>,
curr_tuistate: Res<State<TuiState>>,
mut next_gamestate: ResMut<NextState<GameState>>,
mut next_gamestate: ResMut<NextState<jong_types::states::GameState>>,
mut next_tuistate: ResMut<NextState<TuiState>>,
) {
'message: for message in messages.read() {

View file

@ -6,9 +6,9 @@ use ratatui::layout::{Constraint, Flex, Layout, Offset, Rect, Size};
use ratatui::style::{Modifier, Stylize};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use jong::game::hand::{Drawn, Hand};
use jong::game::player::{CurrentPlayer, MainPlayer, Player};
use jong::game::round::Wind;
use jong::riichi::player::{CurrentPlayer, MainPlayer, Player};
use jong::riichi::player::{Drawn, Hand};
// use jong::riichi::round::Wind;
// use jong_types::*;
use crate::tui::input::Hovered;
@ -95,26 +95,9 @@ pub(crate) fn render(
Ok(())
}
// pub(crate) fn render_arg_check(
// mut commands: Commands,
// mut tui: ResMut<RatatuiContext>,
// hovered: Query<Entity, With<Hovered>>,
// layouts: Res<HandLayouts>,
// tiles: Query<&jong_types::Tile>,
// // main_player: Single<(&Player, Entity, &Wind), With<MainPlayer>>,
// curr_player: Single<Entity, With<CurrentPlayer>>,
// players: Query<(&Player, Entity, &Children)>,
// hands: Query<(&Children, Entity), (With<Hand>, Without<Player>)>,
// // drawn_tile: Single<Entity, With<Drawn>>,
// ) {
// // trace!("arg!");
// }
// FIXME we don't care about other players atm
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn render_hands(
pub(crate) fn render_hand(
mut commands: Commands,
mut tui: ResMut<RatatuiContext>,
@ -122,80 +105,67 @@ pub(crate) fn render_hands(
layouts: Res<HandLayouts>,
tiles: Query<&jong_types::Tile>,
main_player: Single<(&Player, Entity /* , &Wind */), With<MainPlayer>>,
curr_player: Single<Entity, With<CurrentPlayer>>,
players: Query<(&Player, Entity, &Children)>,
hands: Query<(&Children, Entity), With<Hand>>,
drawn_tile: Single<Entity, With<Drawn>>,
// main_player: Single<(&Player, Entity /* , &Wind */), With<MainPlayer>>,
hand: Single<(&Children, Entity), With<Hand>>,
drawn_tile: Option<Single<Entity, With<Drawn>>>,
) -> Result {
let mut frame = tui.get_frame();
debug_blocks(*layouts, &mut frame);
for (hand, hand_ent) in hands {
// debug!("{hand:?}");
// let (player, player_ent, _) = players
// .iter()
// .find(|(_, e, c)| c.contains(&hand_ent))
// .unwrap();
let hand: Vec<_> = hand
.iter()
.map(|entity| -> Result<_> {
let tile = tiles.get(entity).unwrap_or_else(|_| panic!("{entity:?}"));
let hovered = hovered.contains(entity);
let widget = render_tile(tile, hovered);
let hand: Vec<_> = hand
.0
.iter()
.map(|entity| -> Result<_> {
let tile = tiles.get(entity).unwrap_or_else(|_| panic!("{entity:?}"));
let hovered = hovered.contains(entity);
let widget = render_tile(tile, hovered);
Ok((entity, widget, hovered))
})
.collect::<Result<_>>()?;
Ok((entity, widget, hovered))
})
.collect::<Result<_>>()?;
let (player, player_ent) = *main_player;
// if player == main_player.0 {
// split main box into thirds
let mut this_hand = layouts.this_hand;
// let this_drawer = drawn_tile..is_some_and(|dt| dt.0 == player);
let this_drawer = player_ent == *curr_player;
let tile_drawn = if this_drawer { 7 } else { 0 };
let hand_draw_meld = Layout::horizontal([
Constraint::Max(hand.len() as u16 * 5),
Constraint::Max(tile_drawn),
Constraint::Fill(1),
])
.flex(Flex::SpaceBetween);
this_hand = this_hand.offset(Offset {
x: 0,
y: this_hand.height.abs_diff(5) as i32 + 1,
});
this_hand = this_hand.resize(Size {
width: this_hand.width,
height: 4,
});
let [hand_area, drawn_area, meld_area] = hand_draw_meld.areas::<3>(this_hand);
// let (player, player_ent) = *main_player;
// split main box into thirds
let mut this_hand = layouts.this_hand;
let tile_drawn = if drawn_tile.is_some() { 7 } else { 0 };
let hand_draw_meld = Layout::horizontal([
Constraint::Max(hand.len() as u16 * 5),
Constraint::Max(tile_drawn),
Constraint::Fill(1),
])
.flex(Flex::SpaceBetween);
this_hand = this_hand.offset(Offset {
x: 0,
y: this_hand.height.abs_diff(5) as i32 + 1,
});
this_hand = this_hand.resize(Size {
width: this_hand.width,
height: 4,
});
let [hand_area, drawn_area, meld_area] = hand_draw_meld.areas::<3>(this_hand);
// split hand area into tile areas
let mut constraints = vec![Constraint::Max(5); hand.len()];
constraints.push(Constraint::Fill(1));
let layout = Layout::horizontal(constraints).flex(Flex::Start);
let tile_areas = layout.split(hand_area);
// split hand area into tile areas
let mut constraints = vec![Constraint::Max(5); hand.len()];
constraints.push(Constraint::Fill(1));
let layout = Layout::horizontal(constraints).flex(Flex::Start);
let tile_areas = layout.split(hand_area);
for ((entity, widget, hovered), mut area) in
hand.into_iter().zip(tile_areas.iter().copied())
{
if hovered {
area = area.offset(Offset { x: 0, y: -1 });
let mut hitbox = area.as_size();
hitbox.height += 1;
commands.entity(entity).insert(PickRegion {
area: area.resize(hitbox),
});
} else {
commands.entity(entity).insert(PickRegion { area });
}
frame.render_widget(widget, area);
for ((entity, widget, hovered), mut area) in hand.into_iter().zip(tile_areas.iter().copied()) {
if hovered {
area = area.offset(Offset { x: 0, y: -1 });
let mut hitbox = area.as_size();
hitbox.height += 1;
commands.entity(entity).insert(PickRegion {
area: area.resize(hitbox),
});
} else {
commands.entity(entity).insert(PickRegion { area });
}
frame.render_widget(widget, area);
}
// tsumo tile
// if this_drawer {
// // trace!("this_drawer");
// tsumo tile
if let Some(drawn_tile) = drawn_tile {
let mut area = drawn_area.resize(Size {
width: 5,
height: 4,
@ -214,16 +184,16 @@ pub(crate) fn render_hands(
commands.entity(*drawn_tile).insert(PickRegion { area });
}
frame.render_widget(widget, area);
// }
// TODO draw melds
// } else {
// match mainplayer.1.relate(wind) {
// jong::game::round::WindRelation::Shimocha => todo!(),
// jong::game::round::WindRelation::Toimen => todo!(),
// jong::game::round::WindRelation::Kamicha => todo!(),
// }
// }
}
// TODO draw melds
// } else {
// match mainplayer.1.relate(wind) {
// jong::game::round::WindRelation::Shimocha => todo!(),
// jong::game::round::WindRelation::Toimen => todo!(),
// jong::game::round::WindRelation::Kamicha => todo!(),
// }
// }
Ok(())
}

View file

@ -1 +0,0 @@
// const TSUMO;

View file

@ -8,7 +8,8 @@ default:
just --list
run-tui:
spacetime call jong-line "clear_all"
just spacetime_restart_dev
sleep 3sec
cargo run -- run-tui
update:
@ -19,16 +20,10 @@ spacetime:
devenv up
spacetime_dev:
spacetime dev --module-bindings-path jong-db/src/db jong-line --delete-data=always
spacetime dev --module-project-path jong-line --module-bindings-path jong-db/src/db jong-line --delete-data=always
spacetime_generate-bindings:
spacetime generate --lang rust --out-dir jong-db/src/db --project-path jong-line
spacetime_restart_dev:
mprocs -s localhost:4050 --ctl $"({c: restart-proc, name: spacetimedb_dev} | to yaml)"
rrt:
just spacetime_restart_dev
sleep 3sec
just run-tui