jong/jong/src/riichi.rs
2026-02-28 20:55:20 -08:00

385 lines
13 KiB
Rust

use bevy::prelude::*;
use bevy_spacetimedb::{
ReadInsertMessage, ReadInsertUpdateMessage, ReadStdbConnectedMessage,
ReadStdbDisconnectedMessage, ReadUpdateMessage, StdbPlugin,
};
use jong_db::{self, GameTimerTableAccess, add_bot, advance_game, set_ready};
use jong_db::{
BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables,
ViewClosedHandsTableAccess, ViewHandTableAccess,
};
use jong_types::*;
use spacetimedb_sdk::Table;
pub mod player;
use crate::riichi::player::*;
use crate::{SpacetimeDB, creds_store};
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("jong-line")
.with_run_fn(DbConnection::run_threaded)
.add_table(RemoteTables::player)
.add_table(RemoteTables::lobby)
// TODO check bevy_spacetimedb PR status
.add_view_with_pk(RemoteTables::view_hand, |p| p.id)
.add_view_with_pk(RemoteTables::view_closed_hands, |p| {
PlayerOrBot::from(&p.player)
});
let plugins =
if let Some(token) = creds_store().load().expect("i/o error loading credentials") {
// FIXME patch plugin so this takes Option?
plugins.with_token(&token)
} else {
plugins
};
app.add_plugins(plugins)
.init_state::<jong_types::states::GameState>()
.add_sub_state::<jong_types::states::TurnState>()
.add_systems(Startup, subscriptions)
.add_observer(on_subscribed) // TODO fire once then get removed? or keep around for reconnect logic?
.add_systems(Update, (on_connect, on_disconnect))
.add_systems(Update, (on_lobby_insert_update, on_player_insert_update))
.add_systems(
Update,
(on_view_hand_insert, on_view_hand_update)
.run_if(in_state(GameState::Play).or(in_state(GameState::Deal))),
);
}
}
fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage) {
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());
creds_store()
.save(&msg.access_token)
.expect("i/o error saving token");
}
}
// TODO how reconnect?
fn on_disconnect(_stdb: SpacetimeDB, mut messages: ReadStdbDisconnectedMessage) {
for msg in messages.read() {
warn!("lost connection: {:#?}", msg.err);
}
}
// TODO we can make this hold more info in the future
#[derive(Event)]
struct Subscribed;
fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
// commands.queue(command);
let (send, recv) = std::sync::mpsc::channel::<Subscribed>();
stdb.subscription_builder()
.on_applied(move |_| {
trace!("subs succeeded");
send.send(Subscribed).unwrap();
})
.on_error(|_, err| {
error!("subs failed: {err}");
})
.subscribe([
// TODO change these to sub/unsub based on being in lobby and some such
format!(
"SELECT p.* FROM player p WHERE p.identity = '{}'",
stdb.identity()
),
"SELECT p.* FROM player p JOIN lobby l ON p.lobby_id = l.id".to_string(),
"SELECT l.* FROM lobby l JOIN player p ON l.id = p.lobby_id".to_string(),
"SELECT c.* FROM player_clock c JOIN player p ON c.player_id = p.id".to_string(),
"SELECT b.* FROM bot b JOIN lobby l ON l.id = b.lobby_id".to_string(),
"SELECT * FROM view_hand".to_string(),
"SELECT * FROM view_closed_hands".to_string(),
]);
while let Ok(event) = recv.recv() {
commands.trigger(event);
}
}
/// spawns entities to be consistent with server state
// TODO figure out a way to call this for later changes in the various on_ins_upd systems
fn on_subscribed(
_event: On<Subscribed>,
stdb: SpacetimeDB,
mut commands: Commands,
mut next_gamestate: ResMut<NextState<GameState>>,
mut next_turnstate: ResMut<NextState<TurnState>>,
) {
trace!("on_subscribed");
for player in stdb.db().player().iter() {
if player.identity == stdb.identity() {
// trace!("spawn_main_player");
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &player);
} else {
// trace!("spawn_other_player");
spawn_other_player(&stdb, &mut commands, &player);
}
}
for bot in stdb.db().bot().iter() {
let id = PlayerOrBot::Bot { id: bot.id };
let hand_view = stdb
.db()
.view_closed_hands()
.iter()
.find(|v| PlayerOrBot::from(&v.player) == id)
.unwrap();
let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id();
commands.spawn(Player { id }).add_child(hand_ent);
}
if let Some(lobby) = stdb.db().lobby().iter().next() {
next_gamestate.set(lobby.game_state.into());
}
}
fn spawn_main_player(
stdb: &SpacetimeDB,
commands: &mut Commands,
next_turnstate: &mut ResMut<NextState<TurnState>>,
player: &jong_db::Player,
) {
// trace!("spawn_main_player");
let main_player = commands
.spawn((
Player {
id: PlayerOrBot::Player { id: player.id },
},
MainPlayer,
))
.id();
if let Some(player_hand) = stdb.db().view_hand().iter().next() {
spawn_main_hand(commands, next_turnstate, main_player, &player_hand);
}
}
fn spawn_main_hand(
commands: &mut Commands,
next_turnstate: &mut ResMut<NextState<TurnState>>,
main_player: Entity,
player_hand: &PlayerHand,
) {
let hand_tiles: Vec<_> = player_hand
.hand
.iter()
.map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
.collect();
let pond_tiles: Vec<_> = player_hand
.pond
.iter()
.map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
.collect();
let hand = commands.spawn(Hand).add_children(&hand_tiles).id();
let pond = commands.spawn(Pond).add_children(&pond_tiles).id();
commands.entity(main_player).add_children(&[hand, pond]);
debug!("main_hand: {:?}\n main_pond: {:?}", hand_tiles, pond_tiles);
if player_hand.turn_state == jong_db::TurnState::Tsumo
&& let Some(drawn_dbt) = &player_hand.working_tile
{
let drawn = commands
.spawn((Drawn, Tile::from(&drawn_dbt.tile), TileId(drawn_dbt.id)))
.id();
commands.entity(main_player).add_child(drawn);
}
next_turnstate.set(player_hand.turn_state.into());
}
fn spawn_other_player(stdb: &SpacetimeDB, commands: &mut Commands, player: &jong_db::Player) {
let id = PlayerOrBot::Player { id: player.id };
if let Some(hand_view) = stdb
.db()
.view_closed_hands()
.iter()
.find(|v| PlayerOrBot::from(&v.player) == id)
{
let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id();
commands.spawn(Player { id }).add_child(hand_ent);
}
}
fn on_player_insert_update(
stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Player>,
mut commands: Commands,
main_player: Option<Single<&MainPlayer>>,
other_players: Query<&Player, Without<MainPlayer>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
for msg in messages.read() {
debug!("on_player_insert_update: {:?}", msg.new);
assert_eq!(msg.new.identity, stdb.identity());
if main_player.is_none() && msg.new.identity == stdb.identity() {
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &msg.new);
} else if other_players.iter().any(|p| {
if let PlayerOrBot::Player { id } = &p.id {
*id == msg.new.id
} else {
false
}
}) {
trace!("spawn_other_player");
spawn_other_player(&stdb, &mut commands, &msg.new);
} else {
// TODO update case
}
}
}
fn on_lobby_insert_update(
stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Lobby>,
mut commands: Commands,
mut next_gamestate: ResMut<NextState<jong_types::states::GameState>>,
) {
for msg in messages.read() {
// trace!("on_lobby_insert_update msg:\n{:#?}", msg.new);
let player = stdb
.db()
.player()
.identity()
.find(&stdb.identity())
.unwrap();
next_gamestate.set(msg.new.game_state.into());
match msg.new.game_state {
jong_db::GameState::None => {
trace!("game entered none");
}
jong_db::GameState::Lobby => {
trace!("game entered lobby");
if !player.ready {
for _ in 0..3 {
stdb.reducers().add_bot(player.lobby_id).unwrap();
}
stdb.reducers().set_ready(true).unwrap();
// stdb.reducers().advance_game().unwrap();
}
}
jong_db::GameState::Setup => {
trace!("game entered setup");
}
jong_db::GameState::Deal => {
trace!("game entered deal");
}
jong_db::GameState::Play => {
trace!("game entered play");
}
jong_db::GameState::Exit => {
trace!("game enetered exit");
}
}
next_gamestate.set(msg.new.game_state.into());
}
}
fn on_view_hand_insert(
mut messages: ReadInsertMessage<jong_db::PlayerHand>,
mut commands: Commands,
main_player: Single<(Entity, Option<&Children>), With<MainPlayer>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
for msg in messages.read() {
trace!("insert hand: {:?}", msg.row);
if main_player.1.is_none() {
spawn_main_hand(&mut commands, &mut next_turnstate, main_player.0, &msg.row);
}
}
}
fn on_view_hand_update(
stdb: SpacetimeDB,
mut messages: ReadUpdateMessage<jong_db::PlayerHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
main_player: Single<(Entity, &Children), With<MainPlayer>>,
hand: Query<Entity, With<Hand>>,
pond: Query<Entity, With<Pond>>,
// drawn: Option<Single<Entity, With<Drawn>>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
// TODO can this and similar run at startup or on play/reconnect?
for msg in messages.read() {
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 })
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
let pond_tiles: Vec<_> = msg
.new
.pond
.iter()
.map(|dbt| {
tiles
.iter()
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None })
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
commands
.entity(hand.iter().find(|e| main_player.1.contains(e)).unwrap())
.replace_children(&hand_tiles);
commands
.entity(pond.iter().find(|e| main_player.1.contains(e)).unwrap())
.replace_children(&pond_tiles);
match msg.new.turn_state {
jong_db::TurnState::None => {
trace!("turnstate none");
// TODO do we reconcile hand state here or in ::End?
}
jong_db::TurnState::Tsumo => {
trace!("turnstate tsumo");
let dbt = msg
.new
.working_tile
.as_ref()
.expect("entered tsumo without a drawn tile");
commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)));
}
jong_db::TurnState::Menzen => todo!(),
jong_db::TurnState::RiichiKan => todo!(),
jong_db::TurnState::RonChiiPonKan => todo!(),
jong_db::TurnState::End => todo!(),
}
next_turnstate.set(msg.new.turn_state.into());
}
}