Ver Fonte

initial commit

Nicolas Winkler há 4 anos atrás
commit
73379f7dc8
5 ficheiros alterados com 308 adições e 0 exclusões
  1. 19 0
      Cargo.toml
  2. 85 0
      src/game.rs
  3. 31 0
      src/main.rs
  4. 124 0
      src/websocket.rs
  5. 49 0
      static/index.html

+ 19 - 0
Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "acorn"
+version = "0.1.0"
+authors = ["Nicolas Winkler <nicolas.winkler@gmx.ch>"]
+edition = "2018"
+
+
+[dependencies]
+actix = "0.10"
+actix-codec = "0.3"
+actix-web = "3"
+actix-web-actors = "3"
+actix-files = "0.3"
+awc = "2"
+env_logger = "0.7"
+futures = "0.3.1"
+bytes = "0.5.3"
+serde = "1.0"
+serde_json = "1.0"

+ 85 - 0
src/game.rs

@@ -0,0 +1,85 @@
+use std::time::{Duration, Instant};
+use serde::{Serialize, Deserialize};
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum GameState {
+    Creating,
+    Guessing(usize),
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreatingEx {
+    pub question: String,
+    pub letters: Vec<char>
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Round {
+    pub exercises: Vec<CreatingEx>
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Player {
+    nick: String,
+    submitted_word: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Game {
+    players: Vec<Player>,
+    round_number: i32,
+
+    #[serde(skip_serializing)]
+    round: Round,
+
+    state: GameState,
+}
+
+
+impl Game {
+    pub fn new() -> Self {
+        Self {
+            players: vec![],
+            round_number: 0,
+            round: Round{ exercises: vec![] },
+            state: GameState::Creating
+        }
+    }
+
+    fn get_player(&mut self, nick: &str) -> Option<&mut Player> {
+        self.players.iter_mut().find(|x| x.nick == nick)
+    }
+
+    pub fn submit_creation(&mut self, player_nick: &str, word: &str) -> bool {
+        match self.state {
+            GameState::Creating => {
+                if let Some(player) = self.get_player(player_nick) {
+                    player.submitted_word = Some(word.to_owned());
+                    true
+                }
+                else {
+                    false
+                }
+            },
+            _ => false
+        }
+    }
+
+    pub fn next_state(&mut self) {
+        match self.state {
+            GameState::Creating => {
+                self.state = GameState::Guessing(0);
+            },
+            GameState::Guessing(index) => {
+                if index + 1 < self.players.len() {
+                    self.state = GameState::Guessing(index + 1);
+                }
+                else {
+                    self.state = GameState::Creating;
+                }
+            }
+        }
+    }
+}
+

+ 31 - 0
src/main.rs

@@ -0,0 +1,31 @@
+use std::collections::BTreeMap;
+use std::sync::Mutex;
+use actix_files as fs;
+use actix_web::{middleware, web, App, HttpServer};
+
+mod websocket;
+mod game;
+//use crate::websocket::ws_initiate;
+
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+    std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
+    env_logger::init();
+
+    let mut games_list = web::Data::new(Mutex::new(BTreeMap::<String, game::Game>::new()));
+
+    HttpServer::new(move || {
+        App::new()
+            .app_data(games_list.clone())
+            // enable logger
+            .wrap(middleware::Logger::default())
+            // websocket route
+            .service(web::resource("/ws/").route(web::get().to(websocket::ws_initiate)))
+            // static files
+            .service(fs::Files::new("/", "static/").index_file("index.html"))
+    })
+    .bind("127.0.0.1:8000")?
+    .run()
+    .await
+}

+ 124 - 0
src/websocket.rs

@@ -0,0 +1,124 @@
+use std::time::{Duration, Instant};
+use std::collections::BTreeMap;
+use std::sync::Mutex;
+
+use serde::{Serialize, Deserialize};
+
+use actix::prelude::*;
+use actix_web::{web, Error, HttpRequest, HttpResponse};
+use actix_web_actors::ws;
+
+use crate::game;
+
+const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
+const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
+
+pub async fn ws_initiate(map: web::Data<Mutex<BTreeMap<String, game::Game>>>, r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
+    println!("{:?}", r);
+    let res = ws::start(WebSock::new(map.clone()), &r, stream);
+    println!("{:?}", res);
+    res
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+enum Message {
+    Join{ game_id: String, nick: String },
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+enum UpdateMessage {
+    GameState (game::Game)
+}
+
+struct WebSock {
+    heartbeat: Instant,
+    nick: Option<String>,
+    game_id: Option<String>,
+    server_data: web::Data<Mutex<BTreeMap<String, game::Game>>>,
+}
+
+impl Actor for WebSock {
+    type Context = ws::WebsocketContext<Self>;
+
+    fn started(&mut self, ctx: &mut Self::Context) {
+        self.initiate_heartbeat(ctx);
+    }
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSock {
+    fn handle(
+        &mut self,
+        msg: Result<ws::Message, ws::ProtocolError>,
+        ctx: &mut Self::Context,
+    ) {
+        // process websocket messages
+        println!("WS: {:?}", msg);
+        match msg {
+            Ok(ws::Message::Ping(msg)) => {
+                self.heartbeat = Instant::now();
+                ctx.pong(&msg);
+            }
+            Ok(ws::Message::Pong(_)) => {
+                self.heartbeat = Instant::now();
+            }
+            Ok(ws::Message::Text(text)) => {
+                self.handle_message(&text, ctx);
+            },
+            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
+            Ok(ws::Message::Close(reason)) => {
+                ctx.close(reason);
+                ctx.stop();
+            }
+            _ => ctx.stop(),
+        }
+    }
+}
+
+impl WebSock {
+    pub fn new(map: web::Data<Mutex<BTreeMap<String, game::Game>>>) -> Self {
+        Self { heartbeat: Instant::now(), nick: None, game_id: None, server_data: map }
+    }
+
+    pub fn initiate_heartbeat(&self, ctx: &mut <Self as Actor>::Context) {
+        ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
+            if Instant::now().duration_since(act.heartbeat) > CLIENT_TIMEOUT {
+                //println!("Websocket Client heartbeat failed, disconnecting!");
+                ctx.stop();
+                return;
+            }
+            ctx.ping(b"");
+        });
+    }
+
+    pub fn handle_message(&mut self, text: &str, ctx: &mut <WebSock as Actor>::Context) {
+        let x = Message::Join{ game_id: "gmae".to_owned(), nick: "name".to_owned() };
+        let string = serde_json::to_string(&x);
+        println!("{}", string.unwrap());
+
+
+        let parsed: Result<Message, _> = serde_json::from_str(text);
+        if let Ok(msg) = parsed {
+            match msg {
+                Message::Join{game_id, nick} => {
+                    println!("just joined: {}, {}", game_id, nick);
+                    self.game_id = Some(game_id.clone());
+                    self.nick = Some(nick);
+                    let mut map = self.server_data.lock().unwrap();
+                    match map.get(&game_id) {
+                        Some(game) => {
+                            let txt = serde_json::to_string(game).unwrap();
+                            println!("{:?}", txt);
+                            ctx.text(txt);
+                        },
+                        None => {
+                            map.insert(game_id, game::Game::new());
+                            println!("game not found");
+                        }
+                    }
+                },
+            }
+        }
+    }
+}

+ 49 - 0
static/index.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<html>
+<head>
+    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+    <script language="javascript" type="text/javascript">
+        $(function() {
+            var connection = null;
+            $('#join').click(function() {
+                if (connection != null) {
+                    connection.close();
+                    connection = null;
+                }
+                var uri = 'ws://' + window.location.host + '/ws/';
+                connection = new WebSocket(uri);
+                
+                var game_id = $('#gameId').val();
+                var nick = $('#nick').val();
+
+                connection.onopen = function() {
+                    var msg = {
+                        join: {
+                            game_id: game_id,
+                            nick: nick,
+                        }
+                    };
+                    connection.send(JSON.stringify(msg));
+                };
+                connection.onmessage = function(e) {
+                    alert('Received: ' + e.data);
+                };
+                connection.onclose = function() {
+                    //alert("closing");
+                    connection = null;
+                };
+            });
+        });
+    </script>
+</head>
+<body>
+<form id="loginform">
+  <label for="gameId">Game ID:</label>
+  <input id="gameId" type="text"  /><br>
+  <label for="nick">Nickname:</label>
+  <input id="nick" type="text" /><br>
+  <input id="join" type="button" value="Join" />
+</form>
+</body>
+</html>