[workshop] Add Rust workshop files

Test: lol

Change-Id: I7ab6693f8a823311c55396bdb72281f697c6cd60
diff --git a/bin/tennis/BUILD.gn b/bin/tennis/BUILD.gn
new file mode 100644
index 0000000..63e3c16
--- /dev/null
+++ b/bin/tennis/BUILD.gn
@@ -0,0 +1,117 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_binary.gni")
+import("//build/package.gni")
+
+package("tennis_service") {
+  deps = [
+    ":tennis_service_bin",
+  ]
+
+  binary = "rust_crates/tennis_service"
+
+  meta = [
+    {
+      path = rebase_path("meta/tennis_service.cmx")
+      dest = "tennis_service.cmx"
+    },
+  ]
+}
+
+rustc_binary("tennis_service_bin") {
+  name = "tennis_service"
+  with_unit_tests = true
+  edition = "2018"
+
+  deps = [
+    "//garnet/public/fidl/fuchsia.game.tennis:fuchsia.game.tennis-rustc",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/crates/fuchsia-app",
+    "//garnet/public/rust/crates/fuchsia-async",
+    "//garnet/public/rust/crates/fuchsia-syslog",
+    "//garnet/public/rust/crates/fuchsia-zircon",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:parking_lot",
+  ]
+}
+
+package("tennis_viewer") {
+  deps = [
+    ":tennis_viewer_bin",
+  ]
+
+  binary = "rust_crates/tennis_viewer"
+
+  meta = [
+    {
+      path = rebase_path("meta/tennis_viewer.cmx")
+      dest = "tennis_viewer.cmx"
+    },
+  ]
+}
+
+rustc_binary("tennis_viewer_bin") {
+  name = "tennis_viewer"
+  edition = "2018"
+
+  source_root = "viewer/main.rs"
+
+  deps = [
+    "//garnet/public/fidl/fuchsia.game.tennis:fuchsia.game.tennis-rustc",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/crates/fuchsia-app",
+    "//garnet/public/rust/crates/fuchsia-async",
+    "//garnet/public/rust/crates/fuchsia-syslog",
+    "//garnet/public/rust/crates/fuchsia-zircon",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:parking_lot",
+  ]
+}
+
+package("tennis_example_ai") {
+  deps = [
+    ":tennis_example_ai_bin",
+  ]
+
+  binary = "rust_crates/tennis_example_ai"
+
+  meta = [
+    {
+      path = rebase_path("meta/tennis_example_ai.cmx")
+      dest = "tennis_example_ai.cmx"
+    },
+  ]
+}
+
+rustc_binary("tennis_example_ai_bin") {
+  name = "tennis_example_ai"
+  edition = "2018"
+
+  source_root = "example_ai/main.rs"
+
+  deps = [
+    "//garnet/public/fidl/fuchsia.game.tennis:fuchsia.game.tennis-rustc",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/crates/fuchsia-app",
+    "//garnet/public/rust/crates/fuchsia-async",
+    "//garnet/public/rust/crates/fuchsia-syslog",
+    "//garnet/public/rust/crates/fuchsia-zircon",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:parking_lot",
+  ]
+}
+
+package("tennis_sysmgr_config") {
+    deprecated_system_image = true
+    resources = [
+      {
+        dest = "sysmgr/tennis.config"
+        path = rebase_path("tennis_sysmgr.config")
+      }
+    ]
+}
diff --git a/bin/tennis/example_ai/main.rs b/bin/tennis/example_ai/main.rs
new file mode 100644
index 0000000..cacd0ba
--- /dev/null
+++ b/bin/tennis/example_ai/main.rs
@@ -0,0 +1,74 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![deny(warnings)]
+
+#![feature(try_from,async_await,await_macro)]
+
+use failure::{Error, ResultExt};
+use fuchsia_async as fasync;
+use fidl::endpoints::create_endpoints;
+use fuchsia_app::client::connect_to_service;
+use fidl_fuchsia_game_tennis::{TennisServiceMarker, PaddleRequest};
+use fuchsia_zircon::DurationNum;
+use parking_lot::Mutex;
+use std::sync::Arc;
+use futures::TryStreamExt;
+
+fn main() -> Result<(), Error> {
+    let mut executor = fasync::Executor::new().context("Error creating executor")?;
+    let tennis_service = connect_to_service::<TennisServiceMarker>()?;
+
+    let (client_end, paddle_controller) = create_endpoints()?;
+
+    let (mut prs, paddle_control_handle) = paddle_controller.into_stream_and_control_handle()?;
+
+    tennis_service.register_paddle("example_ai", client_end)?;
+
+    let i_am_player_2 = Arc::new(Mutex::new(false));
+    let i_am_player_2_clone = i_am_player_2.clone();
+
+    println!("registering with game service");
+    fasync::spawn(
+        async move {
+            while let Some(PaddleRequest::NewGame{is_player_2, ..}) = await!(prs.try_next()).unwrap() { // TODO: remove unwrap
+                if is_player_2 {
+                    println!("I am player 2");
+                } else {
+                    println!("I am player 1");
+                }
+                *i_am_player_2_clone.lock() = is_player_2
+            }
+        }
+    );
+
+    let resp: Result<(), Error> = executor.run_singlethreaded(async move {
+        loop {
+            let time_step: i64 = 1000 / 5;
+            await!(fuchsia_async::Timer::new(time_step.millis().after_now()));
+
+            let state = await!(tennis_service.get_state())?;
+            if state.game_num == 0 {
+                continue;
+            }
+            let my_y;
+            if *i_am_player_2.lock() {
+                my_y = state.player_2_y;
+            } else {
+                my_y = state.player_1_y;
+            }
+            if state.ball_y > my_y {
+                println!("moving down");
+                paddle_control_handle.send_down()?;
+            } else if state.ball_y < my_y{
+                println!("moving up");
+                paddle_control_handle.send_up()?;
+            } else {
+                println!("staying still");
+                paddle_control_handle.send_stop()?;
+            }
+        }
+    });
+    resp
+}
diff --git a/bin/tennis/meta/tennis_example_ai.cmx b/bin/tennis/meta/tennis_example_ai.cmx
new file mode 100644
index 0000000..67591d3
--- /dev/null
+++ b/bin/tennis/meta/tennis_example_ai.cmx
@@ -0,0 +1,11 @@
+{
+    "program": {
+        "binary": "bin/app"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.logger.LogSink",
+            "fuchsia.game.tennis.TennisService"
+        ]
+    }
+}
diff --git a/bin/tennis/meta/tennis_service.cmx b/bin/tennis/meta/tennis_service.cmx
new file mode 100644
index 0000000..2994d32
--- /dev/null
+++ b/bin/tennis/meta/tennis_service.cmx
@@ -0,0 +1,8 @@
+{
+    "program": {
+        "binary": "bin/app"
+    },
+    "sandbox": {
+        "services": [ "fuchsia.logger.LogSink" ]
+    }
+}
diff --git a/bin/tennis/meta/tennis_viewer.cmx b/bin/tennis/meta/tennis_viewer.cmx
new file mode 100644
index 0000000..67591d3
--- /dev/null
+++ b/bin/tennis/meta/tennis_viewer.cmx
@@ -0,0 +1,11 @@
+{
+    "program": {
+        "binary": "bin/app"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.logger.LogSink",
+            "fuchsia.game.tennis.TennisService"
+        ]
+    }
+}
diff --git a/bin/tennis/src/game.rs b/bin/tennis/src/game.rs
new file mode 100644
index 0000000..7405945
--- /dev/null
+++ b/bin/tennis/src/game.rs
@@ -0,0 +1,192 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use fidl_fuchsia_game_tennis as fidl_tennis;
+use fidl_fuchsia_game_tennis::GameState;
+use fuchsia_syslog::{fx_log, fx_log_info};
+use futures::prelude::*;
+use parking_lot::Mutex;
+use std::sync::{Arc, Weak};
+
+const BOARD_HEIGHT: f64 = 10.0;
+const BOARD_WIDTH: f64 = 20.0;
+const PADDLE_SPEED: f64 = 0.4; // distance paddle travels per step
+const PADDLE_SIZE: f64 = 1.0; // vertical height of paddle
+const BALL_SPEEDUP_MULTIPLIER: f64 = 1.05; // speed multiplier applied on every paddle bounce
+const MAX_BOUNCE_ANGLE: f64 = 1.3; // in radians, bounce angle when hitting very top edge of paddle
+
+pub struct Game {
+    state: GameState,
+    player_1: Option<Player>,
+    player_2: Option<Player>,
+    ball_dx: f64,
+    ball_dy: f64,
+}
+
+#[derive(Clone)]
+struct Player {
+    pub name: String,
+    pub state: Arc<Mutex<PlayerState>>,
+}
+
+#[derive(Clone)]
+pub enum PlayerState {
+    Up,
+    Down,
+    Stop,
+}
+
+fn calc_paddle_movement(pos: &mut f64, state: &PlayerState) {
+    let player_delta = match state {
+        PlayerState::Up => PADDLE_SPEED * -1.0,
+        PlayerState::Down => PADDLE_SPEED,
+        PlayerState::Stop => 0.0,
+    };
+    let new_paddle_location = *pos + player_delta;
+    if new_paddle_location >= 0.0 && new_paddle_location < BOARD_HEIGHT {
+        *pos = new_paddle_location;
+    }
+}
+
+impl Game {
+    /// return clone of internal state
+    pub fn state(&self) -> GameState {
+        fidl_tennis::GameState {
+            ball_x: self.state.ball_x,
+            ball_y: self.state.ball_y,
+            player_1_y: self.state.player_1_y,
+            player_2_y: self.state.player_2_y,
+            player_1_score: self.state.player_1_score,
+            player_2_score: self.state.player_2_score,
+            player_1_name: self.state.player_1_name.clone(),
+            player_2_name: self.state.player_2_name.clone(),
+            time: self.state.time,
+            game_num: self.state.game_num,
+        }
+    }
+    pub fn new() -> Game {
+        Game {
+            player_1: None,
+            player_2: None,
+            ball_dx: 0.0,
+            ball_dy: 0.0,
+            state: GameState {
+                ball_x: 0.0,
+                ball_y: 0.0,
+                game_num: 0,
+                player_1_y: 0.0,
+                player_2_y: 0.0,
+                player_1_score: 0,
+                player_2_score: 0,
+                player_1_name: "".to_string(),
+                player_2_name: "".to_string(),
+                time: 0,
+            },
+        }
+    }
+
+    pub fn players_ready(&self) -> bool {
+        return self.player_1.is_some() && self.player_2.is_some();
+    }
+
+    pub fn register_new_paddle(&mut self, player_name: String) -> Arc<Mutex<PlayerState>> {
+        let paddle = Player {
+            name: player_name.clone(),
+            state: Arc::new(Mutex::new(PlayerState::Stop)),
+        };
+        let res = paddle.state.clone();
+        if self.player_1.is_none() {
+            self.player_1 = Some(paddle);
+            self.state.player_1_name = player_name;
+        } else if self.player_2.is_none() {
+            self.player_2 = Some(paddle);
+            self.state.player_2_name = player_name;
+        } else {
+            panic!("too many clients connected");
+        }
+        return res;
+    }
+
+    pub fn step(&mut self) {
+        if self.players_ready() && self.state.game_num == 0 {
+            self.new_game();
+        } else if !self.players_ready() {
+            // game has not started yet
+            return;
+        }
+
+        fx_log_info!("new step");
+
+        self.state.time += 1;
+
+        calc_paddle_movement(
+            &mut self.state.player_1_y,
+            &self.player_1.as_mut().unwrap().state.lock(),
+        );
+        calc_paddle_movement(
+            &mut self.state.player_2_y,
+            &self.player_2.as_mut().unwrap().state.lock(),
+        );
+
+        let mut new_ball_x = self.state.ball_x + self.ball_dx;
+        let mut new_ball_y = self.state.ball_y + self.ball_dy;
+
+        // reflect off the top/bottom of the board
+        if new_ball_y <= 0.0 || new_ball_y > BOARD_HEIGHT {
+            self.ball_dy = -self.ball_dy;
+            new_ball_y = self.state.ball_y;
+            fx_log_info!("bounce off top or bottom");
+        }
+
+        // reflect off the left/right of the board, if a paddle is in the way
+        if new_ball_x <= 0.0 {
+            // we're about to go off of the left side
+            if new_ball_y > self.state.player_1_y + (PADDLE_SIZE / 2.0)
+                || new_ball_y < self.state.player_1_y - (PADDLE_SIZE / 2.0) {
+                    // player 1 missed, so player 2 gets a point and we reset
+                    self.state.player_2_score += 1;
+                    self.new_game();
+                    return;
+            } else {
+                self.ball_dx = -self.ball_dx;
+                new_ball_x = self.state.ball_x;
+                fx_log_info!("bounce off left");
+            }
+        }
+        if new_ball_x > BOARD_WIDTH {
+            // we're about to go off of the right side
+            if new_ball_y > self.state.player_2_y + (PADDLE_SIZE / 2.0)
+                || new_ball_y < self.state.player_2_y - (PADDLE_SIZE / 2.0) {
+                    // player 2 missed, so player 1 gets a point and we reset
+                    self.state.player_1_score += 1;
+                    self.new_game();
+                    return;
+            } else {
+                self.ball_dx = -self.ball_dx;
+                new_ball_x = self.state.ball_x;
+                fx_log_info!("bounce off right");
+            }
+        }
+
+        self.state.ball_x = new_ball_x;
+        self.state.ball_y = new_ball_y;
+    }
+
+    fn new_game(&mut self) {
+        self.player_1.as_mut().map(|player| {
+            *player.state.lock() = PlayerState::Stop;
+        });
+        self.player_2.as_mut().map(|player| {
+            *player.state.lock() = PlayerState::Stop;
+        });
+        self.ball_dx = 0.5; // TODO randomize?
+        self.ball_dy = 0.5; // TODO randomize?
+        self.state.ball_x = BOARD_WIDTH / 2.0;
+        self.state.ball_y = BOARD_HEIGHT / 2.0;
+        self.state.game_num += 1;
+        //self.state.player_1_y = BOARD_HEIGHT / 2.0;
+        //self.state.player_2_y = BOARD_HEIGHT / 2.0;
+        self.state.time = 0;
+    }
+}
diff --git a/bin/tennis/src/main.rs b/bin/tennis/src/main.rs
new file mode 100644
index 0000000..c454bc2
--- /dev/null
+++ b/bin/tennis/src/main.rs
@@ -0,0 +1,33 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![feature(async_await, await_macro)]
+
+mod game;
+mod tennis_service;
+
+use failure::{Error, ResultExt};
+use fidl::endpoints::ServiceMarker;
+use fidl_fuchsia_game_tennis::TennisServiceMarker;
+use fuchsia_app::server::ServicesServer;
+use fuchsia_syslog::{fx_log, fx_log_info, init_with_tags};
+
+fn main() -> Result<(), Error> {
+    init_with_tags(&["tennis_service"])
+        .expect("tennis syslog init should not fail");
+    fx_log_info!("tennis service started");
+    let mut executor = fuchsia_async::Executor::new()
+        .context("Creating fuchsia_async executor for tennis service failed")?;
+    let tennis = tennis_service::TennisService::new();
+    let done = ServicesServer::new()
+        .add_service((TennisServiceMarker::NAME, move |chan| {
+            tennis.bind(chan);
+        })).start()
+        .context("Creating ServicesServer for tennis service failed")?;
+    executor
+        .run_singlethreaded(done)
+        .context("Attempt to start up tennis services on async::Executor failed")?;
+
+    Ok(())
+}
diff --git a/bin/tennis/src/tennis_service.rs b/bin/tennis/src/tennis_service.rs
new file mode 100644
index 0000000..6127bc5
--- /dev/null
+++ b/bin/tennis/src/tennis_service.rs
@@ -0,0 +1,91 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::game::{Game, PlayerState};
+use failure::ResultExt;
+use fidl::endpoints::{ClientEnd, RequestStream, ServerEnd};
+use fidl_fuchsia_game_tennis as fidl_tennis;
+use fuchsia_async as fasync;
+use fuchsia_syslog::{fx_log, fx_log_err, fx_log_info};
+use fuchsia_zircon::DurationNum;
+use futures::prelude::*;
+use parking_lot::Mutex;
+use std::sync::{Arc, Weak};
+
+#[derive(Clone)]
+pub struct TennisService(Arc<Mutex<Game>>);
+
+impl TennisService {
+    pub fn new() -> TennisService {
+        TennisService(Arc::new(Mutex::new(Game::new())))
+    }
+
+    pub fn bind(&self, chan: fuchsia_async::Channel) {
+        let mut self_clone = self.clone();
+        let mut stream = fidl_tennis::TennisServiceRequestStream::from_channel(chan);
+        fuchsia_async::spawn(
+            async move {
+                while let Some(msg) = await!(stream.try_next())
+                    .context("error reading value from tennis service request stream")?
+                {
+                    match msg {
+                        fidl_tennis::TennisServiceRequest::GetState { responder, .. } => {
+                            let TennisService(game_arc) = &self_clone;
+                            responder.send(&mut game_arc.lock().state());
+                        }
+                        fidl_tennis::TennisServiceRequest::RegisterPaddle {
+                            player_name,
+                            paddle,
+                            ..
+                        } => {
+                            fx_log_info!("new paddle registered: {}", player_name);
+                            self_clone.register_paddle(player_name, paddle);
+                        }
+                    }
+                }
+                Ok(())
+            }
+                .unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e)),
+        );
+    }
+
+    pub fn register_paddle(
+        &self, player_name: String,
+        paddle: fidl::endpoints::ClientEnd<fidl_fuchsia_game_tennis::PaddleMarker>,
+    ) {
+        let TennisService(game_arc) = self.clone();
+        let mut game = game_arc.lock();
+        let player_state = game.register_new_paddle(player_name);
+        let mut stream = paddle.into_proxy().unwrap().take_event_stream(); // TODO(lard): remove unwrap
+        fasync::spawn(
+            async move {
+                while let Some(event) = await!(stream.try_next())
+                    .context("error reading value from paddle event stream")?
+                {
+                    let state = match event {
+                        fidl_tennis::PaddleEvent::Up { .. } => PlayerState::Up,
+                        fidl_tennis::PaddleEvent::Down { .. } => PlayerState::Down,
+                        fidl_tennis::PaddleEvent::Stop { .. } => PlayerState::Stop,
+                    };
+                    *player_state.lock() = state;
+                }
+                Ok(())
+            }
+                .unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e)),
+        );
+        if game.players_ready() {
+            fx_log_info!("game is beginning");
+            let game_arc = game_arc.clone();
+            fasync::spawn(
+                async move {
+                    loop {
+                        game_arc.lock().step();
+                        let time_step: i64 = 1000 / 5;
+                        await!(fuchsia_async::Timer::new(time_step.millis().after_now()));
+                    }
+                },
+            );
+        }
+    }
+}
diff --git a/bin/tennis/tennis_sysmgr.config b/bin/tennis/tennis_sysmgr.config
new file mode 100644
index 0000000..1058a90
--- /dev/null
+++ b/bin/tennis/tennis_sysmgr.config
@@ -0,0 +1,5 @@
+{
+  "services": {
+    "fuchsia.game.tennis.TennisService": "tennis_service"
+  }
+}
diff --git a/bin/tennis/viewer/main.rs b/bin/tennis/viewer/main.rs
new file mode 100644
index 0000000..436e7bd
--- /dev/null
+++ b/bin/tennis/viewer/main.rs
@@ -0,0 +1,108 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![deny(warnings)]
+
+#![feature(try_from,async_await,await_macro)]
+
+use failure::{Error, ResultExt};
+use fuchsia_async as fasync;
+use fuchsia_app::client::connect_to_service;
+use fidl_fuchsia_game_tennis::{TennisServiceMarker, GameState};
+use fuchsia_zircon::DurationNum;
+use std::io;
+use std::io::Write;
+
+const DRAW_WIDTH: usize = 80;
+const DRAW_HEIGHT: usize = 25;
+
+const BOARD_WIDTH: f64 = 20.0;
+const BOARD_HEIGHT: f64 = 10.0;
+
+fn main() -> Result<(), Error> {
+    let mut executor = fasync::Executor::new().context("Error creating executor")?;
+    let tennis_service = connect_to_service::<TennisServiceMarker>()?;
+
+    let mut first_print = true;
+
+    println!("connected to tennis service");
+    let resp: Result<(), Error> = executor.run_singlethreaded(async move {
+        loop {
+            let time_step: i64 = 1000 / 5;
+            await!(fuchsia_async::Timer::new(time_step.millis().after_now()));
+
+            let state = await!(tennis_service.get_state())?;
+            if state.game_num == 0 {
+                continue;
+            }
+
+            if first_print {
+                first_print = false;
+            } else {
+                // Print the following to stdout:
+                // - ESC
+                // - [
+                // - The number of lines to move the cursor up, in asci
+                // - A
+                // This is using the ECMA-48 CSI sequences as described here:
+                // http://man7.org/linux/man-pages/man4/console_codes.4.html
+                let mut to_print = Vec::new();
+                to_print.push(0x1B);
+                to_print.push(0x5B);
+                to_print.append(&mut format!("{}", DRAW_HEIGHT).into_bytes().to_vec());
+                to_print.push(0x46);
+                
+                io::stdout().write(&to_print)?;
+            }
+
+            print_game(state);
+        }
+    });
+    resp
+}
+
+fn print_game(state: GameState) {
+    let banner_height = 3;
+    let board_draw_height = DRAW_HEIGHT - banner_height;
+
+    let paddle_1_loc =
+        ((state.player_1_y / BOARD_HEIGHT) * (board_draw_height as f64)) as usize;
+    let paddle_2_loc =
+        ((state.player_2_y / BOARD_HEIGHT) * (board_draw_height as f64)) as usize;
+
+    let ball_x_loc = (state.ball_x / BOARD_WIDTH * ((DRAW_WIDTH - 1) as f64)) as usize;
+    let ball_y_loc = (state.ball_y / BOARD_HEIGHT * ((board_draw_height - 1) as f64)) as usize;
+
+    let mut output = "".to_string();
+    output.push_str(&state.player_1_name);
+    output.push_str(&" ".repeat(DRAW_WIDTH - state.player_1_name.len() - state.player_2_name.len()));
+    output.push_str(&state.player_2_name);
+    output.push_str("\n");
+
+    let p1_score = format!("{}", state.player_1_score);
+    let p2_score = format!("{}", state.player_2_score);
+    output.push_str(&p1_score);
+    output.push_str(&" ".repeat(DRAW_WIDTH - p1_score.len() - p2_score.len()));
+    output.push_str(&p2_score);
+    output.push_str("\n");
+
+    for y in 0..board_draw_height {
+        for x in 0..DRAW_WIDTH {
+            // I have no clue why this "as usize" is necessary
+            if (x, y) == (ball_x_loc as usize, ball_y_loc) {
+                output.push_str("0");
+            } else if (x, y) == (0, paddle_1_loc) {
+                output.push_str(")");
+            } else if (x, y) == (DRAW_WIDTH - 1, paddle_2_loc) {
+                output.push_str("(");
+            } else if x == DRAW_WIDTH / 2 {
+                output.push_str("|");
+            } else {
+                output.push_str(" ");
+            }
+        }
+        output.push_str("\n");
+    }
+    println!("{}", output);
+}
diff --git a/examples/rust101/hello/BUILD.gn b/examples/rust101/hello/BUILD.gn
new file mode 100644
index 0000000..e6a1afe
--- /dev/null
+++ b/examples/rust101/hello/BUILD.gn
@@ -0,0 +1,40 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_binary.gni")
+import("//build/package.gni")
+
+package("hello") {
+  deps = [
+    ":hello_bin",
+  ]
+
+  binary = "rust_crates/hello"
+
+  meta = [
+    {
+      path = rebase_path("meta/hello.cmx")
+      dest = "hello.cmx"
+    },
+  ]
+}
+
+rustc_binary("hello_bin") {
+  name = "hello"
+  with_unit_tests = true
+  edition = "2018"
+
+  deps = [
+    "//garnet/public/fidl/fuchsia.net.oldhttp:fuchsia.net.oldhttp-rustc",
+    "//garnet/public/fidl/fuchsia.game.tennis:fuchsia.game.tennis-rustc",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/crates/fuchsia-app",
+    "//garnet/public/rust/crates/fuchsia-async",
+    "//garnet/public/rust/crates/fuchsia-syslog",
+    "//garnet/public/rust/crates/fuchsia-zircon",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:parking_lot",
+  ]
+}
diff --git a/examples/rust101/hello/meta/hello.cmx b/examples/rust101/hello/meta/hello.cmx
new file mode 100644
index 0000000..2994d32
--- /dev/null
+++ b/examples/rust101/hello/meta/hello.cmx
@@ -0,0 +1,8 @@
+{
+    "program": {
+        "binary": "bin/app"
+    },
+    "sandbox": {
+        "services": [ "fuchsia.logger.LogSink" ]
+    }
+}
diff --git a/examples/rust101/hello/src/main.rs b/examples/rust101/hello/src/main.rs
new file mode 100644
index 0000000..504a714
--- /dev/null
+++ b/examples/rust101/hello/src/main.rs
@@ -0,0 +1,35 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![feature(async_await, await_macro, futures_api)]
+
+use futures::prelude::*;
+use fuchsia_zircon::prelude::*;
+use fuchsia_async as fasync;
+use fuchsia_zircon as zx;
+
+async fn create_future() -> () {
+    println!("This will print second.");
+    let res = await!(wait_1_second());
+    println!("{:?}", res);
+    ()
+}
+
+fn main() {
+    println!("Hello, Fuchsia 2!");
+
+    let mut executor = fuchsia_async::Executor::new()
+        .expect("Creating fuchsia_async executor for tennis service failed");
+
+    let fut = create_future();
+    println!("This will print first!");
+    executor.run_singlethreaded(fut);
+    println!("This will print last!");
+}
+
+async fn wait_1_second() -> () {
+    // TODO use fasync::Timer::new to wait before returning;
+    let time_step: i64 = 1000;
+    await!(fasync::Timer::new(time_step.millis().after_now()));
+}
diff --git a/packages/prod/all b/packages/prod/all
index fa9cabf..3ab25e6 100644
--- a/packages/prod/all
+++ b/packages/prod/all
@@ -91,6 +91,7 @@
         "garnet/packages/prod/wlanphy",
         "garnet/packages/prod/wlanstack",
         "garnet/packages/prod/wlantap",
-        "garnet/packages/prod/xi_core"
+        "garnet/packages/prod/xi_core",
+        "garnet/packages/prod/tennis"
     ]
 }
diff --git a/packages/prod/tennis b/packages/prod/tennis
new file mode 100644
index 0000000..2bdfa58
--- /dev/null
+++ b/packages/prod/tennis
@@ -0,0 +1,9 @@
+{
+    "packages": [
+        "//garnet/bin/tennis:tennis_service",
+        "//garnet/bin/tennis:tennis_viewer",
+        "//garnet/bin/tennis:tennis_example_ai",
+        "//garnet/bin/tennis:tennis_sysmgr_config",
+        "//garnet/examples/rust101/hello"
+    ]
+}
diff --git a/public/fidl/fuchsia.game.tennis/BUILD.gn b/public/fidl/fuchsia.game.tennis/BUILD.gn
new file mode 100644
index 0000000..a585e0c
--- /dev/null
+++ b/public/fidl/fuchsia.game.tennis/BUILD.gn
@@ -0,0 +1,13 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+
+fidl("fuchsia.game.tennis") {
+  sdk_category = "partner"
+
+  sources = [
+    "tennis.fidl",
+  ]
+}
diff --git a/public/fidl/fuchsia.game.tennis/tennis.fidl b/public/fidl/fuchsia.game.tennis/tennis.fidl
new file mode 100644
index 0000000..b9e0a27
--- /dev/null
+++ b/public/fidl/fuchsia.game.tennis/tennis.fidl
@@ -0,0 +1,31 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library fuchsia.game.tennis;
+
+[Discoverable]
+interface TennisService {
+    1: GetState() -> (GameState state);
+    2: RegisterPaddle(string player_name, Paddle paddle);
+};
+
+struct GameState {
+    float64 ballX;
+    float64 ballY;
+    float64 player_1_y; // player 1 is on the left side of the screen
+    float64 player_2_y;
+    int64 player_1_score;
+    int64 player_2_score;
+    int64 time; // start of each game is zero, represents in-game time steps elapsed
+    int64 game_num; // increments by one any time a new game starts, 0 if not enough players yet
+    string player_1_name;
+    string player_2_name;
+};
+
+interface Paddle {
+    1: NewGame(bool is_player_2);
+    2: -> Up();
+    3: -> Down();
+    4: -> Stop();
+};