zero-trust-rps 0.0.4

Online Multiplayer Rock Paper Scissors
Documentation
use std::time::Duration;

use quinn::{Connection, VarInt};
use rand::{thread_rng, Rng};
use tokio::{
    sync::mpsc::{error::SendError, unbounded_channel},
    task::JoinError,
};

use crate::common::message::{RoomId, RpsData, UserState};

use super::{
    connection::{
        conn::{run_client, RunClientError},
        init::initialize_connection,
    },
    simple_move::SimpleUserMove,
    state::{ClientStateView, RpsState},
};

#[allow(unused)]
const RPS_CHOICES: [RpsData; 3] = unsafe {
    [
        RpsData::new_unchecked([
            240, 159, 170, 168, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        ]),
        RpsData::new_unchecked([
            240, 159, 147, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        ]),
        RpsData::new_unchecked([
            226, 156, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        ]),
    ]
};

#[derive(thiserror::Error, Debug)]
#[allow(clippy::enum_variant_names)]
pub enum RunBotError {
    #[error("Could not send over channel: {}", .0)]
    ChannelSendError(#[from] SendError<SimpleUserMove>),
    #[error("Failed to initialize Connection: {}", .0)]
    #[allow(unused)]
    InitError(String),
    #[error("Error while running: {}", .0)]
    RunClientError(#[from] RunClientError),
    #[error("Could not join: {}", .0)]
    TokioError(#[from] JoinError),
}

#[allow(dead_code)]
pub async fn run_bot(
    id: usize,
    connection: Connection,
    room_id: RoomId,
    domain: String,
    port: u16,
) -> Result<(), RunBotError> {
    log::info!("Run bot {id} for room {room_id}, connect to {domain}:{port} ({connection:?})");
    match _run_bot(connection, room_id, domain.as_str(), port).await {
        Ok(()) => {
            log::info!("Bot {id} finished");
            Ok(())
        }
        Err(err) => {
            log::error!("Bot {id} exited with error: {err:?}");
            Err(err)
        }
    }
}

async fn _run_bot(
    connection: Connection,
    room_id: RoomId,
    domain: &str,
    port: u16,
) -> Result<(), RunBotError> {
    let (connection, writer, reader) = initialize_connection(connection, domain, port)
        .await
        .map_err(|err| RunBotError::InitError(format!("{err}")))?;

    let (umove_send, umove_recv) = unbounded_channel::<SimpleUserMove>();
    let (state_send, mut state_recv) = unbounded_channel::<ClientStateView>();

    let jh = tokio::spawn(run_client(None, writer, reader, (), state_send, umove_recv));

    let mut last_state = RpsState::BeforeRoom;
    let mut requested_state_change = false;

    while let Some(state) = state_recv.recv().await {
        if state.state != last_state {
            requested_state_change = false;
            last_state = state.state;
        } else {
            let duration = Duration::from_millis(thread_rng().gen_range(500..900));
            tokio::time::sleep(duration).await;
        }
        if requested_state_change {
            continue;
        }
        use super::state::RpsState::*;
        match state.state {
            BeforeRoom => {
                requested_state_change = true;
                umove_send.send(SimpleUserMove::JoinRoom(room_id))?
            }
            InRoom => {
                if let Some(room) = state.room.as_ref() {
                    if let Some(round) = room.round.as_ref() {
                        if room
                            .users
                            .iter()
                            .filter(|u| round.users.contains(&u.id))
                            .all(|u| matches!(u.state, UserState::InRoom | UserState::Played(_)))
                        {
                            requested_state_change = true;
                            umove_send.send(SimpleUserMove::Play(
                                RPS_CHOICES[rand::thread_rng().gen_range(0..RPS_CHOICES.len())],
                            ))?
                        }
                    } else if room.users.len() > 1 {
                        requested_state_change = true;
                        umove_send.send(SimpleUserMove::Play(
                            RPS_CHOICES[rand::thread_rng().gen_range(0..RPS_CHOICES.len())],
                        ))?
                    }
                }
            }
            Played => {
                if let Some(room) = state.room.as_ref() {
                    if let Some(round) = room.round.as_ref() {
                        if room
                            .users
                            .iter()
                            .filter(|u| round.users.contains(&u.id))
                            .all(|u| {
                                matches!(u.state, UserState::Played(_) | UserState::Confirmed(_))
                            })
                        {
                            requested_state_change = true;
                            umove_send.send(SimpleUserMove::ConfirmPlay)?
                        }
                    }
                }
            }
            Confirmed => {
                if let Some(room) = state.room.as_ref() {
                    if let Some(round) = room.round.as_ref() {
                        if room
                            .users
                            .iter()
                            .filter(|u| round.users.contains(&u.id))
                            .all(|u| matches!(u.state, UserState::InRoom | UserState::Confirmed(_)))
                        {
                            requested_state_change = true;
                            umove_send.send(SimpleUserMove::BackToRoom)?
                        }
                    }
                }
            }
        }
    }

    let _: () = jh.await??;

    connection.close(VarInt::from_u32(0), &[]);

    Ok(())
}