Well, I’ve come quite a long way. Here’s my main.rs
. Note that the "frontend" is currently just a textbox that you can use to send JSON messages to the host.
use futures::channel::mpsc::{unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender};
use futures::channel::oneshot;
use futures::{future::ready, SinkExt, StreamExt};
use rand::prelude::*;
use rand::rngs::SmallRng;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tokio::select;
use warp::ws::{Message as WsMessage, WebSocket};
use warp::Filter;
type PlayerId = usize;
type Players = BTreeMap<PlayerId, Player>;
type Score = u16;
mod msg {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub enum FromGame {
SetId(PlayerId),
PlayerDisconnected(String),
PlayerConnected(String),
Players(Vec<String>),
RoundStarted(usize, String),
PresentChoices(Vec<String>),
PointsAwarded(BTreeMap<usize, ScoreReport>),
GuessedCorrectAnswer,
}
pub enum ToGame {
JoinRequest(oneshot::Sender<JoinResponse>),
RegisterPlayer(String, Sender<FromGame>),
StartGame,
PlayerDisconnected(PlayerId),
SubmitFib(PlayerId, String),
SubmitAnswer(PlayerId, String),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum FromPlayer {
SetName(String),
SubmitFib(String),
SubmitAnswer(String),
StartGame,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ToPlayer {
Prompt(String),
PlayerDisconnected(String),
PlayerConnected(String),
Players(Vec<String>),
RoundStarted(usize, String),
UnrecognizedMessage(String),
GameRunning,
JoinRequestAccepted,
PresentChoices(Vec<String>),
PointsAwarded(BTreeMap<usize, ScoreReport>),
GuessedCorrectAnswer,
}
#[derive(Debug, Clone, Copy)]
pub enum JoinResponse {
Accepted,
GameRunning,
}
}
struct Player {
name: String,
score: Score,
tx: Sender<msg::FromGame>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreReport {
name: String,
fooled: Vec<(usize, String)>,
fib: String,
correct: bool,
points_awarded: Score,
previous_total: Score,
}
impl ScoreReport {
fn new(player: &Player, fib: String) -> Self {
ScoreReport {
name: player.name.clone(),
fooled: Vec::new(),
fib: fib.clone(),
correct: false,
points_awarded: 0,
previous_total: player.score,
}
}
}
#[derive(Clone, Debug, Deserialize)]
struct Prompt {
prompt: String,
answer: String,
}
struct Game {
inbox: Receiver<msg::ToGame>,
running: bool,
current_id: usize,
players: Players,
round: usize,
prompt: Option<Prompt>,
prompts: Vec<Prompt>,
fibs: BTreeMap<String, Vec<PlayerId>>,
choices: Vec<String>,
answers: BTreeMap<usize, String>,
rng: SmallRng,
}
impl Game {
fn new(inbox: Receiver<msg::ToGame>) -> Self {
let prompts =
serde_json::from_str(&std::fs::read_to_string("prompts.json").expect("Reading prompts.json"))
.expect("Parsing prompts");
Game {
inbox,
running: false,
current_id: 1,
players: BTreeMap::new(),
round: 0,
prompt: None,
prompts,
fibs: BTreeMap::new(),
choices: Vec::new(),
answers: BTreeMap::new(),
rng: SmallRng::from_entropy(),
}
}
async fn broadcast(&mut self, m: msg::FromGame) {
for Player { tx, .. } in self.players.values_mut() {
tx.send(m.clone())
.await
.expect("Sending message to player loop");
}
}
async fn new_round(&mut self) {
self.round += 1;
self.fetch_prompt();
let prompt = self
.prompt
.as_ref()
.expect("Prompt should be loaded")
.clone();
self.fibs.clear();
self.choices.clear();
self.answers.clear();
self.broadcast(msg::FromGame::RoundStarted(
self.round,
prompt.prompt.clone(),
))
.await;
}
fn fetch_prompt(&mut self) {
let n = self.rng.next_u32() as usize % self.prompts.len();
self.prompt = Some(self.prompts.remove(n));
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = unbounded();
let tx = warp::any().map(move || tx.clone());
tokio::task::spawn(main_loop(rx));
// GET /fibbagews -> websocket upgrade
let game = warp::path("fibbagews").and(warp::ws()).and(tx).map(
move |ws: warp::ws::Ws, tx: Sender<msg::ToGame>| {
ws.on_upgrade(|socket| player_loop(socket, tx))
},
);
// GET /fibbage -> index html
let index = warp::path("fibbage").map(|| warp::reply::html(INDEX_HTML));
let routes = index.or(game);
warp::serve(routes).run(([127, 0, 0, 1], 3000)).await;
}
async fn main_loop(rx: Receiver<msg::ToGame>) {
let mut game = Game::new(rx);
while let Some(msg) = game.inbox.next().await {
match msg {
msg::ToGame::JoinRequest(tx) => {
if game.running {
tx.send(msg::JoinResponse::GameRunning)
.expect("Sending response to join request");
} else {
tx.send(msg::JoinResponse::Accepted)
.expect("Sending response to join request");
}
}
msg::ToGame::RegisterPlayer(name, mut tx) => {
if game.running {
continue;
}
// Tell this player what their number is and who the other players are
tx.send(msg::FromGame::SetId(game.current_id))
.await
.expect("Sending message to player loop");
let player_list: Vec<String> =
game.players.values().map(|p| p.name.clone()).collect();
tx.send(msg::FromGame::Players(player_list))
.await
.expect("Sending message to player loop");
// Notify other players
game.broadcast(msg::FromGame::PlayerConnected(name.clone()))
.await;
let player = Player {
name: name.clone(),
tx,
score: 0,
};
game.players.insert(game.current_id, player);
game.current_id += 1;
}
msg::ToGame::PlayerDisconnected(id) => {
if let Some(Player { name, .. }) = &game.players.get(&id) {
let name = name.clone();
game.players.remove(&id);
game.broadcast(msg::FromGame::PlayerDisconnected(name.clone()))
.await;
}
}
msg::ToGame::StartGame => {
if !game.running {
game.running = true;
game.new_round().await;
}
}
msg::ToGame::SubmitFib(player, text) => {
if let Some(Player { tx, .. }) = game.players.get_mut(&player) {
if game.fibs.values().any(|v| v.contains(&player)) {
// Player has already submitted a fib
continue;
}
let real_answer = &game
.prompt
.as_ref()
.expect("Prompt should exist when collecting fibs")
.answer;
if text == *real_answer {
tx.send(msg::FromGame::GuessedCorrectAnswer)
.await
.expect("Sending message to player loop");
continue;
}
game.fibs.entry(text).or_insert(Vec::new()).push(player);
// Check if we have received a fib from everybody
// ∀i ∈ Players ∃v ∈ Fibs. i submitted v
if game
.players
.keys()
.all(|i| game.fibs.values().any(|v| v.contains(&i)))
{
game.choices.push(real_answer.to_owned());
for fib in game.fibs.keys() {
game.choices.push(fib.clone());
}
game.choices.shuffle(&mut game.rng);
for (i, Player { tx, .. }) in game.players.iter_mut() {
let (submitted, _) = game
.fibs
.iter()
.find(|(_, v)| v.contains(&i))
.expect("Every player must have submitted a fib");
tx.send(msg::FromGame::PresentChoices(
game.choices
.iter()
.filter(|f| *f != submitted)
.cloned()
.collect(),
))
.await
.expect("Sending message to player loop");
}
}
}
}
msg::ToGame::SubmitAnswer(player, answer) => {
if !game.players.contains_key(&player) || game.answers.contains_key(&player) {
// Either we don't know this player or they've already submitted an answer
continue;
}
game.answers.insert(player, answer);
if game.players.keys().all(|i| game.answers.contains_key(&i)) {
// Award points
let mut scores = BTreeMap::new();
for (player, choice) in game.answers.iter() {
let (player_fib, _) = game
.fibs
.iter()
.find(|(_, v)| v.contains(&player))
.expect("Player must have submitted a fib");
match game.fibs.get(choice) {
None => {
// Player picked the correct answer
let mut entry = scores.entry(*player).or_insert(ScoreReport::new(
&game.players[player],
player_fib.clone(),
));
entry.points_awarded += 200;
entry.correct = true;
}
Some(fibbers) => {
// Player picked a fib → all players who submitted it get points
for p in fibbers.iter() {
debug_assert!(
!fibbers.is_empty(),
"If it's a fib someone must have submitted it"
);
let mut entry = scores.entry(*p).or_insert(ScoreReport::new(
&game.players[p],
choice.clone(),
));
entry.points_awarded += 100;
entry
.fooled
.push((*player, game.players[player].name.clone()))
}
}
}
}
game.broadcast(msg::FromGame::PointsAwarded(scores.clone()))
.await;
game.new_round().await;
}
}
}
}
}
async fn player_loop(ws: WebSocket, mut to_game: Sender<msg::ToGame>) {
let (mut to_ws, mut from_ws) = {
// Split the socket into a sender and receive of messages.
let (to_ws1, from_ws1) = ws.split();
let to_ws2 = to_ws1.with(|msg: msg::ToPlayer| {
let res: Result<WsMessage, warp::Error> = Ok(WsMessage::text(
serde_json::to_string(&msg).expect("Converting message to JSON"),
));
ready(res)
});
(to_ws2, from_ws1)
};
let (greet_tx, greet_rx) = oneshot::channel();
to_game
.send(msg::ToGame::JoinRequest(greet_tx))
.await
.expect("Sending message to main loop");
match greet_rx.await.expect("Receive answer to join request") {
msg::JoinResponse::Accepted => {
to_ws
.send(msg::ToPlayer::JoinRequestAccepted)
.await
.expect("Sending message over WebSocket");
}
msg::JoinResponse::GameRunning => {
to_ws
.send(msg::ToPlayer::GameRunning)
.await
.expect("Sending message over WebSocket");
return;
}
}
let (to_me, mut from_game) = unbounded();
let mut my_id = 0;
loop {
select! {
msg = from_ws.next() => {
let msg = match msg {
Some(msg) => match msg {
Ok(msg) => msg,
Err(_) => {
to_game.send(msg::ToGame::PlayerDisconnected(my_id)).await.expect("Sending message to main loop");
return;
}
}
None => {
to_game.send(msg::ToGame::PlayerDisconnected(my_id)).await.expect("Sending message to main loop");
return;
}
};
if msg.is_close() {
to_game.send(msg::ToGame::PlayerDisconnected(my_id)).await.expect("Sending message to main loop");
return;
}
let msg = msg.to_str().expect("Expecting a string message");
match serde_json::from_str(msg) {
Ok(msg::FromPlayer::SetName(name)) => {
to_game.send(msg::ToGame::RegisterPlayer(name, to_me.clone())).await.expect("Sending message to main loop");
}
Ok(msg::FromPlayer::SubmitFib(text)) => {
to_game.send(msg::ToGame::SubmitFib(my_id, text)).await.expect("Sending message to main loop");
}
Ok(msg::FromPlayer::SubmitAnswer(number)) => {
to_game.send(msg::ToGame::SubmitAnswer(my_id, number)).await.expect("Sending message to main loop");
}
Ok(msg::FromPlayer::StartGame) => {
to_game.send(msg::ToGame::StartGame).await.expect("Sending message to main loop");
}
Err(_) => {
to_ws.send(msg::ToPlayer::UnrecognizedMessage(msg.to_owned())).await.expect("Sending message to main loop");
}
}
}
msg = from_game.next() => {
match msg.expect("Receiving message from main loop") {
msg::FromGame::SetId(id) => my_id = id,
msg::FromGame::PlayerDisconnected(name) => {
to_ws.send(msg::ToPlayer::PlayerDisconnected(name)).await.expect("Sending message over WebSocket");
}
msg::FromGame::PlayerConnected(name) => {
to_ws.send(msg::ToPlayer::PlayerConnected(name)).await.expect("Sending message over WebSocket");
}
msg::FromGame::Players(players) => {
to_ws.send(msg::ToPlayer::Players(players)).await.expect("Sending message over WebSocket");
}
msg::FromGame::RoundStarted(n, prompt) => {
to_ws.send(msg::ToPlayer::RoundStarted(n, prompt)).await.expect("Sending message over WebSocket");
}
msg::FromGame::PresentChoices(choices) => {
to_ws.send(msg::ToPlayer::PresentChoices(choices)).await.expect("Sending message over WebSocket");
}
msg::FromGame::GuessedCorrectAnswer => {
to_ws.send(msg::ToPlayer::GuessedCorrectAnswer).await.expect("Sending message over WebSocket");
}
msg::FromGame::PointsAwarded(report) => {
to_ws.send(msg::ToPlayer::PointsAwarded(report)).await.expect("Sending message over WebSocket");
}
}
}
}
}
}
static INDEX_HTML: &str = r#"
<!DOCTYPE html>
<html>
<head>
<title>Fibbage Test</title>
</head>
<body>
<h1>Fibbage test interface</h1>
<div id="game">
<p><em>Connecting...</em></p>
</div>
<input type="text" id="text" />
<button type="button" id="send">Send</button>
<script type="text/javascript">
var uri = 'wss://' + location.host + '/fibbagews';
var ws = new WebSocket(uri);
function message(data) {
var line = document.createElement('p');
line.innerHTML = data;
game.appendChild(line);
}
ws.onopen = function() {
game.innerHTML = "<p><em>Connected!</em></p>";
}
ws.onmessage = function(msg) {
message('Received: ' + msg.data);
};
send.onclick = function() {
var msg = text.value;
ws.send(msg);
text.value = '';
message('Sent: ' + msg);
};
</script>
</body>
</html>
"#;
It's possible I've way overengineered this, it's something I'm prone to. In any case I learned a ton. I'd be very grateful for any reviews or commentary.