diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e8afe15..4b94c07 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -2,4 +2,4 @@ members = [ "basic" -] +, "tic-tac-toe"] diff --git a/examples/tic-tac-toe/Cargo.toml b/examples/tic-tac-toe/Cargo.toml new file mode 100644 index 0000000..fca8100 --- /dev/null +++ b/examples/tic-tac-toe/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2021" + +[dependencies] +termcolor = "0.3" +waku = { path = "../../waku-bindings", package = "waku-bindings" } +serde_json = "1.0" +ark-std = "0.4" +ctrlc = "3.2.4" diff --git a/examples/tic-tac-toe/src/main.rs b/examples/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..61e5e65 --- /dev/null +++ b/examples/tic-tac-toe/src/main.rs @@ -0,0 +1,224 @@ +extern crate termcolor; + +use std::io::Write; +use std::str::from_utf8; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +use waku::{ + waku_new, Event, WakuNodeConfig, + LibwakuResponse, Multiaddr, Running, WakuNodeHandle, +}; + +fn greeting() { + println!( + "\nRust TicTacToe\n\ + --------------\n\ + A simple game written in the rust programming language.\n\ + Credits to: https://github.com/flofriday/tictactoe" + ) +} + +fn print_player(player: &char) { + let mut stdout = StandardStream::stdout(ColorChoice::Always); + + if player == &'X' { + stdout + .set_color(ColorSpec::new().set_fg(Some(Color::Blue))) + .unwrap(); + } else if player == &'O' { + stdout + .set_color(ColorSpec::new().set_fg(Some(Color::Green))) + .unwrap(); + } + + write!(&mut stdout, "{}", player).unwrap(); + stdout.reset().unwrap(); +} + +fn draw(board: &[char]) { + println!("\n"); + + for i in (0..3).rev() { + let offset = i * 3; + + print!("-------------\n| "); + print_player(&board[offset]); + print!(" | "); + print_player(&board[offset + 1]); + print!(" | "); + print_player(&board[offset + 2]); + println!(" |"); + } + + println!("-------------"); +} + +fn ask_user(board: &mut [char], player: char) { + loop { + print!("Player '"); + print_player(&player); + println!("', enter a number: "); + + let mut input = String::new(); + if std::io::stdin().read_line(&mut input).is_err() { + println!("Couldn't read line! Try again."); + continue; + } + + if let Ok(number) = input.trim().parse::() { + if number < 1 || number > 9 { + println!("The field number must be between 1 and 9."); + continue; + } + + let number = number - 1; + + if board[number] == 'X' || board[number] == 'O' { + print!("This field is already taken by '"); + print_player(&board[number]); + println!("'."); + continue; + } + + board[number] = player; + + break; + } else { + println!("Only numbers are allowed."); + continue; + } + } +} + +fn has_won(board: &[char]) -> bool { + for tmp in 0..3 { + if board[tmp] == board[tmp + 3] && board[tmp] == board[tmp + 6] { + return true; + } + + let tmp = tmp * 3; + + if board[tmp] == board[tmp + 1] && board[tmp] == board[tmp + 2] { + return true; + } + } + + if (board[0] == board[4] && board[0] == board[8]) + || (board[2] == board[4] && board[2] == board[6]) + { + return true; + } + + false +} + +#[inline(always)] +fn is_over(board: &[char]) -> bool { + board.iter().all(|&v| v == 'X' || v == 'O') +} + +// Return true if the game should end. +// false otherwise +fn game_logic(player: &mut char, board: &mut [char], + topic: &str, waku: &WakuNodeHandle) -> bool { + // Ask for user input + ask_user(board, *player); + + let board_string: String = board.iter().collect(); + let board_str_slice: &str = &board_string; + + let _ = waku.relay_publish_txt(topic, &board_str_slice, "tic-tac-toe-example", None); + + // Check if a player won + if has_won(&board) { + draw(&board); + print!("Player '"); + print_player(&player); + println!("' won! \\(^.^)/"); + return true; + } + + // Check if all fields are used + if is_over(&board) { + draw(&board); + println!("All fields are used. No one won. (._.)"); + return true; + } + + // Switch player + *player = if *player == 'X' { 'O' } else { 'X' }; + return false; +} + +fn main() { + let mut board = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; + let mut player = 'X'; + let topic = "/waku/2/rs/16/64"; + + // Create a Waku instance + let waku = waku_new(Some(WakuNodeConfig { + port: Some(60010), + cluster_id: Some(16), + ..Default::default() + })) + .expect("should instantiate"); + + // ctrlc::set_handler(move ||{ + // println!("Ctrl+C detected. Exiting gracefully..."); + // // waku.stop(); + // }).expect("Error setting Ctrl+C handler"); + + let waku = waku.start().expect("waku should start"); + + // Establish a closure that handles the incoming messages + waku.set_event_callback(&|response| { + if let LibwakuResponse::Success(v) = response { + let event: Event = + serde_json::from_str(v.unwrap().as_str()).expect("Parsing event to succeed"); + + match event { + Event::WakuMessage(evt) => { + println!("WakuMessage event received: {:?}", evt.waku_message); + let message = evt.waku_message; + let payload = message.payload.to_vec(); + match from_utf8(&payload) { + Ok(msg) => { + println!("::::::::::::::::::::::::::::::::::::::::::::::::::::"); + println!("Message Received: {}", msg); + println!("::::::::::::::::::::::::::::::::::::::::::::::::::::"); + + // game_logic(&mut player, &mut board, topic, &waku); + } + Err(e) => { + eprintln!("Failed to decode payload as UTF-8: {}", e); + // Handle the error as needed, or just log and skip + } + } + } + Event::Unrecognized(err) => panic!("Unrecognized waku event: {:?}", err), + _ => panic!("event case not expected"), + }; + } + }); + + waku.relay_subscribe(&topic).expect("waku should subscribe"); + + let target_node_multi_addr = + "/dns4/store-01.do-ams3.status.staging.status.im/tcp/30303/p2p/16Uiu2HAm3xVDaz6SRJ6kErwC21zBJEZjavVXg7VSkoWzaV1aMA3F" + .parse::().expect("parse multiaddress"); + + waku.connect(&target_node_multi_addr, None) + .expect("waku should connect to other node"); + + // Welcome the player + greeting(); + + loop { + // Draw the field + draw(&board); + + if game_logic(&mut player, &mut board, topic, &waku) { + break; + } + } +} \ No newline at end of file diff --git a/examples/tic-tac-toe/src/waku.rs b/examples/tic-tac-toe/src/waku.rs new file mode 100644 index 0000000..601896a --- /dev/null +++ b/examples/tic-tac-toe/src/waku.rs @@ -0,0 +1,5 @@ +use waku::{ + waku_destroy, waku_new, Encoding, Event, WakuContentTopic, WakuMessage, WakuNodeConfig, LibwakuResponse, +}; + +pub mut waku: WakuNodeHandle; diff --git a/waku-bindings/src/lib.rs b/waku-bindings/src/lib.rs index e88bd52..ee98013 100644 --- a/waku-bindings/src/lib.rs +++ b/waku-bindings/src/lib.rs @@ -2,7 +2,7 @@ //! //! Implementation on top of [`waku-bindings`](https://rfc.vac.dev/spec/36/) mod general; -mod node; +pub mod node; pub mod utils; // Re-export the LibwakuResponse type to make it accessible outside this module diff --git a/waku-bindings/src/node/mod.rs b/waku-bindings/src/node/mod.rs index 4549b2c..cd3b266 100644 --- a/waku-bindings/src/node/mod.rs +++ b/waku-bindings/src/node/mod.rs @@ -23,6 +23,10 @@ pub use config::WakuNodeConfig; pub use events::{Event, WakuMessageEvent}; pub use relay::waku_create_content_topic; +use crate::WakuContentTopic; +use crate::Encoding; +use std::time::SystemTime; + /// Marker trait to disallow undesired waku node states in the handle pub trait WakuNodeState {} @@ -65,7 +69,7 @@ impl WakuNodeHandle { } } -impl WakuNodeHandle { +impl WakuNodeHandle { /// Stops a Waku node /// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop) pub fn stop(self) -> Result> { @@ -95,20 +99,45 @@ impl WakuNodeHandle { peers::waku_connect(&self.ctx, address, timeout) } + pub fn relay_publish_txt( + &self, + pubsub_topic: &str, + msg_txt: &str, + content_topic_name: &'static str, + timeout: Option, + ) -> Result { + let content_topic = WakuContentTopic::new("waku", "2", content_topic_name, Encoding::Proto); + let message = WakuMessage::new( + msg_txt, + content_topic, + 0, + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis() + .try_into() + .unwrap(), + Vec::new(), + false, + ); + + relay::waku_relay_publish_message(&self.ctx, &message, pubsub_topic, timeout) + } + /// Publish a message using Waku Relay. /// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms) /// The pubsub_topic parameter is optional and if not specified it will be derived from the contentTopic. pub fn relay_publish_message( &self, message: &WakuMessage, - pubsub_topic: &String, + pubsub_topic: &str, timeout: Option, ) -> Result { relay::waku_relay_publish_message(&self.ctx, message, pubsub_topic, timeout) } /// Subscribe to WakuRelay to receive messages matching a content filter. - pub fn relay_subscribe(&self, pubsub_topic: &String) -> Result<()> { + pub fn relay_subscribe(&self, pubsub_topic: &str) -> Result<()> { relay::waku_relay_subscribe(&self.ctx, pubsub_topic) } diff --git a/waku-bindings/src/node/relay.rs b/waku-bindings/src/node/relay.rs index e42caf4..d45d33e 100644 --- a/waku-bindings/src/node/relay.rs +++ b/waku-bindings/src/node/relay.rs @@ -60,7 +60,7 @@ pub fn waku_create_content_topic( pub fn waku_relay_publish_message( ctx: &WakuNodeContext, message: &WakuMessage, - pubsub_topic: &String, + pubsub_topic: &str, timeout: Option, ) -> Result { let pubsub_topic = pubsub_topic.to_string(); @@ -105,7 +105,7 @@ pub fn waku_relay_publish_message( handle_response(code, result) } -pub fn waku_relay_subscribe(ctx: &WakuNodeContext, pubsub_topic: &String) -> Result<()> { +pub fn waku_relay_subscribe(ctx: &WakuNodeContext, pubsub_topic: &str) -> Result<()> { let pubsub_topic = pubsub_topic.to_string(); let pubsub_topic_ptr = CString::new(pubsub_topic) .expect("CString should build properly from pubsub topic") diff --git a/waku-bindings/tests/node.rs b/waku-bindings/tests/node.rs index 08c2f40..29af82c 100644 --- a/waku-bindings/tests/node.rs +++ b/waku-bindings/tests/node.rs @@ -57,7 +57,7 @@ async fn test_echo_messages( node2.set_event_callback(&closure); // Set the event callback with the closure - let topic = TEST_PUBSUBTOPIC.to_string(); + let topic = TEST_PUBSUBTOPIC; node1.relay_subscribe(&topic).unwrap(); node2.relay_subscribe(&topic).unwrap();