summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-12-18 15:33:02 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-12-18 15:33:02 -0500
commit27f74d8c366be675e7ab64ca746496a66b3cf024 (patch)
tree7bee8fee0b8753d1c3974e33bcb155b509bae100
parent4512c5ead9406206de32e37490b7a4ac792d93bf (diff)
Add bidding from webapp
-rw-r--r--protocol/src/bridge_engine.rs16
-rw-r--r--server/Cargo.toml1
-rw-r--r--server/src/main.rs2
-rw-r--r--server/src/play.rs15
-rw-r--r--webapp/src/components/table.rs79
5 files changed, 90 insertions, 23 deletions
diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs
index ec88183..925676e 100644
--- a/protocol/src/bridge_engine.rs
+++ b/protocol/src/bridge_engine.rs
@@ -440,6 +440,7 @@ impl Bidding {
}
fn passed_out(&self) -> bool {
+ if self.bids.len() < 4 { return false; }
let mut passes = 0;
for b in self.bids.iter().rev().take(3) {
if b == &Bid::Pass {
@@ -717,6 +718,7 @@ impl GameStatePlayerView {
mod tests {
use super::*;
use log::info;
+ use rand::random;
fn as_bidding(r: BiddingResult) -> Bidding {
match r {
@@ -992,6 +994,20 @@ mod tests {
}
#[test]
+ fn pass_out_bid() {
+ let mut bidding = Bidding::new(random());
+ for i in 0..3 {
+ bidding = as_bidding(bidding.bid(Bid::Pass).unwrap());
+ assert!(!bidding.passed_out());
+ }
+ bidding = match bidding.bid(Bid::Pass).unwrap() {
+ BiddingResult::Contract(None, bidding) => bidding,
+ _ => panic!("should be passed out"),
+ };
+ assert!(bidding.passed_out());
+ }
+
+ #[test]
fn play_hand() {
let deal = DealInPlay::new(Player::West, mini_deal());
assert_eq!(deal.tricks_played, vec!());
diff --git a/server/Cargo.toml b/server/Cargo.toml
index ca3475d..9d5f1a8 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+# TODO Upgrade to 0.6: https://github.com/tokio-rs/axum/blob/main/axum/CHANGELOG.md
axum = "0.5"
dotenv = "0.15.0"
serde = { version = "1.0.145", features = ["derive"] }
diff --git a/server/src/main.rs b/server/src/main.rs
index a8429ec..5b81fc0 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -172,7 +172,7 @@ async fn post_bid(
) -> Result<Json<()>, BridgeError> {
info!("Getting table state for {id:}");
let jnl = DbJournal::new(extension.db.clone(), id);
- let mut table = play::Table::new_or_replay(jnl).await?;
+ let mut table = play::Table::replay(jnl).await?;
if !table.game().is_bidding() {
return Err(BridgeError::InvalidRequest(
"Posting a bid requires that the game is in the bidding phase"
diff --git a/server/src/play.rs b/server/src/play.rs
index e78a19e..79d241c 100644
--- a/server/src/play.rs
+++ b/server/src/play.rs
@@ -4,6 +4,7 @@ use rand::random;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{query, PgPool};
+use tracing::info;
use uuid::Uuid;
use crate::error::BridgeError;
@@ -56,9 +57,9 @@ impl Journal for DbJournal {
}
async fn replay(&mut self, seq: i64) -> Result<Vec<serde_json::Value>, BridgeError> {
- let results = query!(
+ let rows =query!(
r#"
- select payload from object_journal
+ select seq, payload from object_journal
where id = $1 and seq >= $2
order by seq
"#,
@@ -66,8 +67,13 @@ impl Journal for DbJournal {
seq
)
.fetch_all(&self.db)
- .await?;
- Ok(results.into_iter().map(|v| v.payload).collect())
+ .await?;
+ let mut payloads = vec!();
+ for v in rows {
+ payloads.push(v.payload);
+ self.seq = v.seq;
+ }
+ Ok(payloads)
}
fn next(&self) -> i64 {
@@ -145,6 +151,7 @@ pub async fn advance_play<J: Journal>(table: &mut Table<J>) -> Result<(), Bridge
let current_player = match table.game().current_player() {
Some(player) => player,
None => {
+ info!("Could not make play. Game: {:#?}", table.game());
return Err(BridgeError::InvalidRequest(format!("No play to make for game")));
},
};
diff --git a/webapp/src/components/table.rs b/webapp/src/components/table.rs
index 7c0b3a3..17c37b1 100644
--- a/webapp/src/components/table.rs
+++ b/webapp/src/components/table.rs
@@ -1,31 +1,51 @@
+use std::future::Future;
+use std::pin::Pin;
+
use crate::components::{BiddingBox, BiddingTable, Hand};
use crate::use_app_context;
use crate::utils::ok_json;
use anyhow::Context;
use gloo_net::http::Request;
use log::info;
-use protocol::bridge_engine::{GameStatePlayerView, BiddingState, BiddingStatePlayerView};
+use protocol::bridge_engine::{
+ Bid, BiddingState, BiddingStatePlayerView, GameStatePlayerView,
+};
use yew::prelude::*;
#[function_component(OnlineTable)]
pub fn online_table(props: &OnlineTableProps) -> Html {
let ctx = use_app_context();
- let table_state: UseStateHandle<Option<GameStatePlayerView>> = use_state(|| None);
- {
- // TODO update this from server state
+ let table_state: UseStateHandle<Option<GameStatePlayerView>> =
+ use_state(|| None);
+
+ let update_table_state = {
let table_state = table_state.clone();
let props = props.clone();
+ || {
+ Box::pin(async move {
+ // let table_state = table_state.clone();
+ let props = props.clone();
+ let response =
+ Request::get(&format!("/api/table/{}", props.table.id))
+ .send()
+ .await
+ .context("fetching table data")?;
+ let table = ok_json(response).await?;
+ table_state.set(Some(table));
+ Ok(())
+ })
+ as Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>
+ }
+ };
+
+ {
let ctx = ctx.clone();
+ let update_table_state = update_table_state.clone();
use_effect_with_deps(
move |_| {
ctx.spawn_async(async move {
- let response = Request::get(&format!("/api/table/{}", props.table.id))
- .send()
- .await
- .context("fetching table data")?;
- let table = ok_json(response).await?;
- table_state.set(Some(table));
+ update_table_state().await?;
Ok(())
});
|| ()
@@ -34,6 +54,29 @@ pub fn online_table(props: &OnlineTableProps) -> Html {
);
}
+ let on_bid = {
+ let ctx = ctx.clone();
+ let props = props.clone();
+ let update_table_state = update_table_state.clone();
+ Callback::from(move |bid| {
+ let update_table_state = update_table_state.clone();
+ info!("Bid clicked: {:?}", bid);
+ ctx.spawn_async(async move {
+ let bid_response = Request::post(&format!(
+ "/api/table/{}/bid",
+ props.table.id
+ ))
+ .json(&bid)?
+ .send()
+ .await
+ .context("submitting bid")?;
+ let () = ok_json(bid_response).await?;
+ update_table_state().await?;
+ Ok(())
+ });
+ })
+ };
+
let leave_table = {
let ctx = ctx.clone();
Callback::from(move |_| {
@@ -48,7 +91,7 @@ pub fn online_table(props: &OnlineTableProps) -> Html {
{ "Leave table" }
</button>
if let Some(table_state) = &*table_state {
- <Table table={ table_state.clone() }/>
+ <Table table={ table_state.clone() } { on_bid } />
}
</>
}
@@ -66,16 +109,15 @@ pub fn table(props: &TableProps) -> Html {
info!("Card clicked: {}", card);
})
};
- let on_bid = {
- Callback::from(move |bid| {
- info!("Bid clicked: {:?}", bid);
- })
- };
+
let center = match &props.table {
- GameStatePlayerView::Bidding(BiddingStatePlayerView { bidding, .. }) => html! {
+ GameStatePlayerView::Bidding(BiddingStatePlayerView {
+ bidding,
+ ..
+ }) => html! {
<>
<BiddingTable bidding={bidding.clone()} />
- <BiddingBox current_bid={bidding.highest_bid().clone()} { on_bid } />
+ <BiddingBox current_bid={bidding.highest_bid().clone()} on_bid={ props.on_bid.clone() } />
{ format!("It is {:?} to bid", bidding.current_bidder()) }
</>
},
@@ -100,4 +142,5 @@ pub fn table(props: &TableProps) -> Html {
#[derive(PartialEq, Properties, Clone)]
pub struct TableProps {
pub table: GameStatePlayerView,
+ pub on_bid: Callback<Bid>,
}