General cleanup after the first game
+ Randomized codename order obscuring players more + Added better error handling code + Support hour + minute time increments + Refactored layout to be more clear + clippy + fmtmsg_refactor
parent
5dd1b5daa7
commit
0796f0be21
|
@ -14,6 +14,15 @@ version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -977,6 +986,23 @@ dependencies = [
|
||||||
"rand_core 0.5.1",
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
|
@ -1720,11 +1746,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "woxlf"
|
name = "woxlf"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serenity",
|
"serenity",
|
||||||
"structopt",
|
"structopt",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "woxlf"
|
name = "woxlf"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -8,10 +8,11 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
config = "0.12.0"
|
config = "0.12.0"
|
||||||
structopt = "0.3.26"
|
structopt = "0.3.26"
|
||||||
chrono = "0.4.19"
|
chrono = {version="0.4.19", features=["serde"]}
|
||||||
serde = "1.0.136"
|
serde = "1.0.136"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
toml = "0.5.8"
|
toml = "0.5.8"
|
||||||
|
regex = "1.5.5"
|
||||||
|
|
||||||
[dependencies.serenity]
|
[dependencies.serenity]
|
||||||
version = "0.10.10"
|
version = "0.10.10"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use config::{Config, File};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
#[structopt(name = "WOXlf", about = "WOXlf discord bot")]
|
||||||
|
pub struct Args {
|
||||||
|
pub cfg_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct BotConfig {
|
||||||
|
pub token: String,
|
||||||
|
pub app_id: u64,
|
||||||
|
pub host_channel: u64,
|
||||||
|
pub vote_channel: u64,
|
||||||
|
pub category: u64,
|
||||||
|
pub game_state_dir: PathBuf,
|
||||||
|
pub occupation: Vec<String>,
|
||||||
|
pub adjective: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotConfig {
|
||||||
|
pub fn new(config_path: &Path) -> Result<Self, config::ConfigError> {
|
||||||
|
let cfg = Config::builder()
|
||||||
|
.add_source(File::from(config_path))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
cfg.try_deserialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_game_state_path(&self) -> PathBuf {
|
||||||
|
self.game_state_dir.join("wOxlf_data.toml")
|
||||||
|
}
|
||||||
|
}
|
183
src/data.rs
183
src/data.rs
|
@ -1,183 +0,0 @@
|
||||||
use config::{Config, File};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serenity::prelude::TypeMapKey;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use structopt::StructOpt;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
|
||||||
#[structopt(name = "WOXlf", about = "WOXlf discord bot")]
|
|
||||||
pub struct Args {
|
|
||||||
pub cfg_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct BotConfig {
|
|
||||||
pub token: String,
|
|
||||||
pub app_id: u64,
|
|
||||||
pub host_channel: u64,
|
|
||||||
pub vote_channel: u64,
|
|
||||||
pub category: u64,
|
|
||||||
pub game_state_dir: PathBuf,
|
|
||||||
pub occupation: Vec<String>,
|
|
||||||
pub adjective: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BotConfig {
|
|
||||||
pub fn new(config_path: &Path) -> Result<Self, config::ConfigError> {
|
|
||||||
let cfg = Config::builder()
|
|
||||||
.add_source(File::from(config_path))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
cfg.try_deserialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_game_state_path(&self) -> PathBuf {
|
|
||||||
self.game_state_dir.join("wOxlf_data.toml")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
|
|
||||||
pub enum Phase {
|
|
||||||
Day,
|
|
||||||
Night,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Phase {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Night
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Phase {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let phase_name = match self {
|
|
||||||
Self::Day => "Day",
|
|
||||||
Self::Night => "Night",
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(f, "{}", phase_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)]
|
|
||||||
pub struct PlayerData {
|
|
||||||
pub channel: u64,
|
|
||||||
pub discord_id: u64,
|
|
||||||
pub codename: String,
|
|
||||||
pub vote_target: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
|
||||||
pub struct GameState {
|
|
||||||
pub phase_number: u64,
|
|
||||||
pub current_phase: Phase,
|
|
||||||
pub phase_end_time: u64,
|
|
||||||
pub player_data: Vec<PlayerData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameState {
|
|
||||||
pub fn codename_exists(&self, codename: &str) -> bool {
|
|
||||||
self.player_data
|
|
||||||
.iter()
|
|
||||||
.any(|data| data.codename.to_lowercase() == codename)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.player_data.clear();
|
|
||||||
self.current_phase = Phase::Night;
|
|
||||||
self.phase_end_time = 0;
|
|
||||||
self.phase_number = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_from_channel(&self, channel_id: u64) -> Option<&PlayerData> {
|
|
||||||
self.player_data.iter().find(|p| p.channel == channel_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_from_discord_id(&self, discord_id: u64) -> Option<&PlayerData> {
|
|
||||||
self.player_data.iter().find(|p| p.discord_id == discord_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_from_channel_mut(&mut self, channel_id: u64) -> Option<&mut PlayerData> {
|
|
||||||
self.player_data
|
|
||||||
.iter_mut()
|
|
||||||
.find(|p| p.channel == channel_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_by_codename(&self, codename: &str) -> Option<PlayerData> {
|
|
||||||
self.player_data
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.codename.to_lowercase() == codename.to_lowercase())
|
|
||||||
.cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_phase(&mut self) {
|
|
||||||
if self.current_phase == Phase::Night {
|
|
||||||
self.current_phase = Phase::Day
|
|
||||||
} else {
|
|
||||||
self.phase_number += 1;
|
|
||||||
self.current_phase = Phase::Night
|
|
||||||
}
|
|
||||||
|
|
||||||
for mut player in &mut self.player_data {
|
|
||||||
player.vote_target = None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_phase_end_time(&self) -> String {
|
|
||||||
format!("<t:{}:f>", self.phase_end_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_phase_countdown(&self) -> String {
|
|
||||||
format!("<t:{}:R>", self.phase_end_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_time_to_phase(&mut self, hours: u64) {
|
|
||||||
self.phase_end_time += hours * 60 * 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_vote_tallies(&self) -> HashMap<String, u32> {
|
|
||||||
let mut vote_set: HashMap<String, u32> = HashMap::new();
|
|
||||||
|
|
||||||
for player in &self.player_data {
|
|
||||||
if let Some(vote_target) = player.vote_target {
|
|
||||||
let target = self.get_player_from_discord_id(vote_target);
|
|
||||||
if let Some(target) = target {
|
|
||||||
*vote_set.entry(target.codename.clone()).or_insert(0) += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vote_set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct GlobalData {
|
|
||||||
pub cfg: BotConfig,
|
|
||||||
pub game_state: GameState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlobalData {
|
|
||||||
pub fn new(cfg: BotConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
cfg,
|
|
||||||
game_state: GameState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TypeMapKey for GlobalData {
|
|
||||||
type Value = Arc<Mutex<GlobalData>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub enum MessageSource {
|
|
||||||
Player(u64),
|
|
||||||
Host,
|
|
||||||
Automated,
|
|
||||||
}
|
|
|
@ -1,21 +1,25 @@
|
||||||
use serenity::framework::standard::macros::help;
|
use std::collections::HashSet;
|
||||||
use serenity::framework::standard::macros::{command, group};
|
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
|
use rand::thread_rng;
|
||||||
|
use serenity::framework::standard::macros::{command, group, help, hook};
|
||||||
use serenity::framework::standard::{
|
use serenity::framework::standard::{
|
||||||
help_commands, Args, CommandGroup, CommandResult, HelpOptions,
|
help_commands, Args, CommandGroup, CommandResult, HelpOptions,
|
||||||
};
|
};
|
||||||
use serenity::framework::StandardFramework;
|
use serenity::framework::StandardFramework;
|
||||||
|
use serenity::model::guild::Member;
|
||||||
use serenity::model::id::ChannelId;
|
use serenity::model::id::ChannelId;
|
||||||
use serenity::model::prelude::{Message, UserId};
|
use serenity::model::prelude::{Message, UserId};
|
||||||
use serenity::prelude::Context;
|
use serenity::prelude::Context;
|
||||||
use serenity::utils::MessageBuilder;
|
use serenity::utils::MessageBuilder;
|
||||||
|
|
||||||
use crate::data::{GlobalData, MessageSource, Phase};
|
use crate::discord::helper::{
|
||||||
use crate::helper;
|
add_user_to_game, build_system_message, parse_duration_arg, send_msg_to_player_channels,
|
||||||
use crate::helper::{
|
|
||||||
build_system_message, clear_game_state, get_phase_end_timestamp, print_game_status,
|
|
||||||
save_game_state, send_msg_to_player_channels,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use crate::error::{Result, WoxlfError};
|
||||||
|
use crate::game::global_data::GlobalData;
|
||||||
|
use crate::game::MessageSource;
|
||||||
|
use crate::game::Phase;
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[commands(start, say, end, broadcast, next_phase, terminate, add_time)]
|
#[commands(start, say, end, broadcast, next_phase, terminate, add_time)]
|
||||||
|
@ -30,38 +34,54 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let mut data = ctx.data.write().await;
|
let mut data = ctx.data.write().await;
|
||||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
let mut global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
clear_game_state(&mut global_data).unwrap();
|
let duration = parse_duration_arg(&mut args).await?;
|
||||||
|
|
||||||
let duration = match args.single::<u64>() {
|
global_data.start_game(Phase::Night, duration.into());
|
||||||
Ok(d) => d,
|
|
||||||
|
let players: Result<Vec<&Member>> = args
|
||||||
|
.iter::<String>()
|
||||||
|
.flatten()
|
||||||
|
.map(|discord_id| {
|
||||||
|
let discord_id = match discord_id.parse::<u64>() {
|
||||||
|
Ok(discord_id) => discord_id,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
msg.reply(&ctx.http, "Error parsing phase duration!")
|
return Err(WoxlfError::DiscordIdParseError(discord_id));
|
||||||
.await
|
}
|
||||||
.unwrap();
|
};
|
||||||
|
|
||||||
|
if let Some(discord_user) = guild.members.get(&UserId::from(discord_id)) {
|
||||||
|
Ok(discord_user)
|
||||||
|
} else {
|
||||||
|
Err(WoxlfError::DiscordIdParseError(discord_id.to_string()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut players = match players {
|
||||||
|
Ok(players) => players,
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = match e {
|
||||||
|
WoxlfError::DiscordIdParseError(e) => {
|
||||||
|
format!("Error parsing '{}' as a discord user id.", e)
|
||||||
|
}
|
||||||
|
_ => "Internal bot error".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
msg.reply(&ctx.http, err_msg).await?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
global_data.game_state.phase_end_time = get_phase_end_timestamp(duration);
|
players.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
for player in args.iter::<u64>().flatten() {
|
for player in players {
|
||||||
if let Some(discord_user) = guild.members.get(&UserId::from(player)) {
|
add_user_to_game(ctx, &guild, &mut global_data, player).await?;
|
||||||
helper::add_user_to_game(ctx, &guild, &mut global_data, discord_user).await?;
|
|
||||||
} else {
|
|
||||||
msg.reply(
|
|
||||||
&ctx.http,
|
|
||||||
format!("User {} is invalid or not in this server!", player),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save_game_state(&global_data).unwrap();
|
global_data.save_game_state().unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -76,7 +96,7 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
||||||
|
|
||||||
let mut global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
for player_data in &global_data.game_state.player_data {
|
for player_data in &global_data.game_state_mut()?.player_data {
|
||||||
let channel = guild
|
let channel = guild
|
||||||
.channels
|
.channels
|
||||||
.get(&ChannelId::from(player_data.channel))
|
.get(&ChannelId::from(player_data.channel))
|
||||||
|
@ -85,7 +105,7 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
||||||
channel.delete(&ctx.http).await?;
|
channel.delete(&ctx.http).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_game_state(&mut global_data).unwrap();
|
global_data.clear_game_state()?;
|
||||||
|
|
||||||
msg.reply(&ctx.http, "Game ended!").await.unwrap();
|
msg.reply(&ctx.http, "Game ended!").await.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -95,24 +115,24 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
#[allowed_roles("wolfx host")]
|
#[allowed_roles("wolfx host")]
|
||||||
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let mut data = ctx.data.write().await;
|
||||||
let global_data = data.get::<GlobalData>().unwrap();
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
let global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
let msg = format!("**wOxlf **> {}", args.rest());
|
let msg = format!("**wOxlf **> {}", args.rest());
|
||||||
|
|
||||||
send_msg_to_player_channels(
|
send_msg_to_player_channels(
|
||||||
ctx,
|
ctx,
|
||||||
&guild,
|
&guild,
|
||||||
&global_data,
|
&mut global_data,
|
||||||
MessageSource::Host,
|
MessageSource::Host,
|
||||||
&msg,
|
&msg,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -121,24 +141,24 @@ async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
#[allowed_roles("wolfx host")]
|
#[allowed_roles("wolfx host")]
|
||||||
async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let mut data = ctx.data.write().await;
|
||||||
let global_data = data.get::<GlobalData>().unwrap();
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
let global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
let msg = build_system_message(args.rest());
|
let msg = build_system_message(args.rest());
|
||||||
|
|
||||||
send_msg_to_player_channels(
|
send_msg_to_player_channels(
|
||||||
ctx,
|
ctx,
|
||||||
&guild,
|
&guild,
|
||||||
&global_data,
|
&mut global_data,
|
||||||
MessageSource::Host,
|
MessageSource::Host,
|
||||||
&msg,
|
&msg,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -153,24 +173,14 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
|
|
||||||
let mut global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
let duration = match args.single::<u64>() {
|
let duration = parse_duration_arg(&mut args).await?;
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => {
|
|
||||||
msg.reply(&ctx.http, "Error parsing phase duration!")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
global_data.game_state.next_phase();
|
global_data.game_state_mut()?.next_phase(duration.into());
|
||||||
|
|
||||||
global_data.game_state.phase_end_time = get_phase_end_timestamp(duration);
|
|
||||||
|
|
||||||
let broadcast = MessageBuilder::new()
|
let broadcast = MessageBuilder::new()
|
||||||
.push_line(args.rest())
|
.push_line(args.rest())
|
||||||
.push_line("")
|
.push_line("")
|
||||||
.push(print_game_status(&global_data.game_state))
|
.push(global_data.print_game_status())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let broadcast = build_system_message(&broadcast);
|
let broadcast = build_system_message(&broadcast);
|
||||||
|
@ -178,15 +188,15 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
send_msg_to_player_channels(
|
send_msg_to_player_channels(
|
||||||
ctx,
|
ctx,
|
||||||
&guild,
|
&guild,
|
||||||
&global_data,
|
&mut global_data,
|
||||||
MessageSource::Host,
|
MessageSource::Host,
|
||||||
&broadcast,
|
&broadcast,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
if global_data.game_state.current_phase == Phase::Day {
|
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||||
let vote_channel = guild
|
let vote_channel = guild
|
||||||
.channels
|
.channels
|
||||||
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||||
|
@ -195,24 +205,22 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
m.content(format!(
|
m.content(format!(
|
||||||
"**DAY {} VOTES:**",
|
"**DAY {} VOTES:**",
|
||||||
global_data.game_state.phase_number
|
global_data.game_state_mut().unwrap().phase_number
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.reply(
|
msg.reply(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
format!(
|
format!(
|
||||||
"Phase has been cycled to {}.",
|
"Phase has been cycled to {}.",
|
||||||
&global_data.game_state.current_phase
|
&global_data.game_state_mut()?.current_phase
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
save_game_state(&global_data).unwrap();
|
global_data.save_game_state()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -229,13 +237,13 @@ async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
|
||||||
let target = args.rest().to_lowercase();
|
let target = args.rest().to_lowercase();
|
||||||
let index = global_data
|
let index = global_data
|
||||||
.game_state
|
.game_state_mut()?
|
||||||
.player_data
|
.player_data
|
||||||
.iter()
|
.iter()
|
||||||
.position(|p| p.codename.to_lowercase() == target);
|
.position(|p| p.codename.to_lowercase() == target);
|
||||||
|
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
let player = global_data.game_state.player_data.remove(index);
|
let player = global_data.game_state_mut()?.player_data.remove(index);
|
||||||
|
|
||||||
let player_channel = guild
|
let player_channel = guild
|
||||||
.channels
|
.channels
|
||||||
|
@ -256,7 +264,7 @@ async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
save_game_state(&global_data).unwrap();
|
global_data.save_game_state().unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,22 +278,16 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
|
|
||||||
let mut global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
let duration = match args.single::<u64>() {
|
let duration = parse_duration_arg(&mut args).await?;
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => {
|
|
||||||
msg.reply(&ctx.http, "Error parsing phase duration!")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
global_data.game_state.add_time_to_phase(duration);
|
global_data
|
||||||
|
.game_state_mut()?
|
||||||
|
.add_time_to_phase(duration.into());
|
||||||
|
|
||||||
let broadcast = MessageBuilder::new()
|
let broadcast = MessageBuilder::new()
|
||||||
.push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!")
|
.push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!")
|
||||||
.push_line("")
|
.push_line("")
|
||||||
.push(print_game_status(&global_data.game_state))
|
.push(global_data.print_game_status())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let broadcast = build_system_message(&broadcast);
|
let broadcast = build_system_message(&broadcast);
|
||||||
|
@ -293,19 +295,19 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
send_msg_to_player_channels(
|
send_msg_to_player_channels(
|
||||||
ctx,
|
ctx,
|
||||||
&guild,
|
&guild,
|
||||||
&global_data,
|
&mut global_data,
|
||||||
MessageSource::Host,
|
MessageSource::Host,
|
||||||
&broadcast,
|
&broadcast,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
msg.reply(&ctx.http, "Phase has been updated")
|
msg.reply(&ctx.http, "Phase has been updated")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
save_game_state(&global_data).unwrap();
|
global_data.save_game_state().unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +325,7 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
|
||||||
let mut global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
if global_data.game_state.current_phase != Phase::Day {
|
if global_data.game_state_mut()?.current_phase != Phase::Day {
|
||||||
msg.reply(
|
msg.reply(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
"You can only select subject for termination during the day!",
|
"You can only select subject for termination during the day!",
|
||||||
|
@ -334,11 +336,13 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
if global_data
|
if global_data
|
||||||
.game_state
|
.game_state_mut()?
|
||||||
.get_player_from_channel(msg.channel_id.0)
|
.get_player_from_channel(msg.channel_id.0)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
let target_player = global_data.game_state.get_player_by_codename(args.rest());
|
let target_player = global_data
|
||||||
|
.game_state_mut()?
|
||||||
|
.get_player_by_codename(args.rest());
|
||||||
|
|
||||||
if let Some(target_player) = target_player {
|
if let Some(target_player) = target_player {
|
||||||
let vote_channel = guild
|
let vote_channel = guild
|
||||||
|
@ -347,10 +351,11 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let player_data = global_data
|
let player_data = global_data
|
||||||
.game_state
|
.game_state_mut()?
|
||||||
.get_player_from_channel_mut(msg.channel_id.0)
|
.get_player_from_channel_mut(msg.channel_id.0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
player_data.vote_target = Some(target_player.discord_id);
|
|
||||||
|
player_data.cast_vote(target_player.discord_id);
|
||||||
|
|
||||||
vote_channel
|
vote_channel
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
|
@ -373,7 +378,7 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
save_game_state(&global_data).unwrap();
|
global_data.save_game_state().unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,25 +386,25 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
#[description = "Get the game status. $status"]
|
#[description = "Get the game status. $status"]
|
||||||
async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let mut data = ctx.data.write().await;
|
||||||
let global_data = data.get::<GlobalData>().unwrap();
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
|
|
||||||
let global_data = global_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
let mut msg_builder = MessageBuilder::new();
|
let mut msg_builder = MessageBuilder::new();
|
||||||
|
|
||||||
msg_builder.push(print_game_status(&global_data.game_state));
|
msg_builder.push(global_data.print_game_status());
|
||||||
|
|
||||||
if global_data.game_state.current_phase == Phase::Day {
|
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||||
msg_builder.push_line("");
|
msg_builder.push_line("");
|
||||||
|
|
||||||
let vote_tallies = global_data.game_state.get_vote_tallies();
|
let vote_tallies = global_data.game_state_mut()?.get_vote_tallies();
|
||||||
|
|
||||||
if vote_tallies.is_empty() {
|
if vote_tallies.is_empty() {
|
||||||
msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST");
|
msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST");
|
||||||
} else {
|
} else {
|
||||||
msg_builder.push_line("TERMINATION VOTE TALLIES:");
|
msg_builder.push_line("TERMINATION VOTE TALLIES:");
|
||||||
for (player, tally) in global_data.game_state.get_vote_tallies() {
|
for (player, tally) in global_data.game_state_mut()?.get_vote_tallies() {
|
||||||
msg_builder.push_line(format!("{}: {}", player, tally));
|
msg_builder.push_line(format!("{}: {}", player, tally));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,7 +428,7 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||||
|
|
||||||
msg_builder.push_line("Test Subjects:");
|
msg_builder.push_line("Test Subjects:");
|
||||||
|
|
||||||
for player in &global_data.game_state.player_data {
|
for player in &global_data.game_state()?.player_data {
|
||||||
msg_builder.push("* ").push(&player.codename);
|
msg_builder.push("* ").push(&player.codename);
|
||||||
|
|
||||||
if msg.channel_id.0 == global_data.cfg.host_channel {
|
if msg.channel_id.0 == global_data.cfg.host_channel {
|
||||||
|
@ -445,7 +450,7 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||||
#[command_not_found_text = "Could not find: `{}`."]
|
#[command_not_found_text = "Could not find: `{}`."]
|
||||||
#[max_levenshtein_distance(3)]
|
#[max_levenshtein_distance(3)]
|
||||||
#[indention_prefix = "+"]
|
#[indention_prefix = "+"]
|
||||||
#[lacking_role = "Nothing"]
|
#[lacking_role = "Strike"]
|
||||||
#[wrong_channel = "Strike"]
|
#[wrong_channel = "Strike"]
|
||||||
async fn help(
|
async fn help(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
|
@ -459,10 +464,32 @@ async fn help(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
|
async fn handle_errors(
|
||||||
|
ctx: &Context,
|
||||||
|
msg: &Message,
|
||||||
|
command_name: &str,
|
||||||
|
command_result: CommandResult,
|
||||||
|
) {
|
||||||
|
match command_result {
|
||||||
|
Ok(()) => println!("Successfully processed command '{}'", command_name),
|
||||||
|
Err(err) => {
|
||||||
|
let reply_msg = format!(
|
||||||
|
"Command '{}' returned an error. {}",
|
||||||
|
command_name,
|
||||||
|
err.to_string()
|
||||||
|
);
|
||||||
|
println!("{}", reply_msg);
|
||||||
|
msg.reply(&ctx.http, reply_msg).await.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn command_framework() -> StandardFramework {
|
pub fn command_framework() -> StandardFramework {
|
||||||
StandardFramework::new()
|
StandardFramework::new()
|
||||||
.configure(|c| c.prefix('$'))
|
.configure(|c| c.prefix('$'))
|
||||||
.group(&HOST_GROUP)
|
.group(&HOST_GROUP)
|
||||||
.group(&PLAYER_GROUP)
|
.group(&PLAYER_GROUP)
|
||||||
.help(&HELP)
|
.help(&HELP)
|
||||||
|
.after(handle_errors)
|
||||||
}
|
}
|
|
@ -1,31 +1,29 @@
|
||||||
use std::fs::File;
|
use serenity::client::Context;
|
||||||
use std::io::prelude::*;
|
use serenity::framework::standard::Args;
|
||||||
|
use serenity::http::AttachmentType;
|
||||||
use rand::Rng;
|
|
||||||
use serenity::framework::standard::CommandResult;
|
|
||||||
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
|
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
|
||||||
use serenity::model::guild::Member;
|
use serenity::model::guild::{Guild, Member};
|
||||||
use serenity::model::id::UserId;
|
use serenity::model::id::{ChannelId, UserId};
|
||||||
use serenity::model::prelude::ChannelId;
|
|
||||||
use serenity::model::prelude::Guild;
|
|
||||||
use serenity::model::Permissions;
|
use serenity::model::Permissions;
|
||||||
use serenity::prelude::Context;
|
|
||||||
use serenity::utils::MessageBuilder;
|
use serenity::utils::MessageBuilder;
|
||||||
|
|
||||||
use crate::data::{BotConfig, GameState, GlobalData, MessageSource, PlayerData};
|
use crate::error::WoxlfError;
|
||||||
use serenity::http::AttachmentType;
|
use crate::game::game_state::PhaseDuration;
|
||||||
use std::time::UNIX_EPOCH;
|
use crate::game::global_data::GlobalData;
|
||||||
|
use crate::game::player_data::PlayerData;
|
||||||
|
use crate::game::MessageSource;
|
||||||
|
use crate::{error, game};
|
||||||
|
|
||||||
pub async fn send_msg_to_player_channels(
|
pub async fn send_msg_to_player_channels(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
guild: &Guild,
|
guild: &Guild,
|
||||||
global_data: &GlobalData,
|
global_data: &mut GlobalData,
|
||||||
msg_source: MessageSource,
|
msg_source: MessageSource,
|
||||||
msg: &str,
|
msg: &str,
|
||||||
attachment: Option<Vec<AttachmentType<'_>>>,
|
attachment: Option<Vec<AttachmentType<'_>>>,
|
||||||
pin: bool,
|
pin: bool,
|
||||||
) {
|
) -> error::Result<()> {
|
||||||
for player_data in &global_data.game_state.player_data {
|
for player_data in &global_data.game_state_mut()?.player_data {
|
||||||
if let MessageSource::Player(channel_id) = msg_source {
|
if let MessageSource::Player(channel_id) = msg_source {
|
||||||
if channel_id == player_data.channel {
|
if channel_id == player_data.channel {
|
||||||
continue;
|
continue;
|
||||||
|
@ -47,12 +45,11 @@ pub async fn send_msg_to_player_channels(
|
||||||
|
|
||||||
m
|
m
|
||||||
})
|
})
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if pin {
|
if pin {
|
||||||
// pin system messages
|
// pin system messages
|
||||||
msg.pin(&ctx.http).await.unwrap();
|
msg.pin(&ctx.http).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +61,7 @@ pub async fn send_msg_to_player_channels(
|
||||||
let source = match msg_source {
|
let source = match msg_source {
|
||||||
MessageSource::Player(channel_id) => {
|
MessageSource::Player(channel_id) => {
|
||||||
let discord_id = global_data
|
let discord_id = global_data
|
||||||
.game_state
|
.game_state_mut()?
|
||||||
.get_player_from_channel(channel_id)
|
.get_player_from_channel(channel_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.discord_id;
|
.discord_id;
|
||||||
|
@ -89,60 +86,21 @@ pub async fn send_msg_to_player_channels(
|
||||||
|
|
||||||
m
|
m
|
||||||
})
|
})
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_game_state(global_data: &GlobalData) -> std::io::Result<()> {
|
|
||||||
let s = toml::to_string_pretty(&global_data.game_state).unwrap();
|
|
||||||
|
|
||||||
let mut file = File::create(global_data.cfg.get_game_state_path())?;
|
|
||||||
|
|
||||||
file.write_all(s.as_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_game_state(global_data: &mut GlobalData) -> std::io::Result<()> {
|
|
||||||
let mut file = File::open(global_data.cfg.get_game_state_path())?;
|
|
||||||
|
|
||||||
let mut data = String::new();
|
|
||||||
|
|
||||||
file.read_to_string(&mut data)?;
|
|
||||||
|
|
||||||
global_data.game_state = toml::from_str(&data).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_game_state(global_data: &mut GlobalData) -> std::io::Result<()> {
|
|
||||||
global_data.game_state.clear();
|
|
||||||
|
|
||||||
let state_path = global_data.cfg.get_game_state_path();
|
|
||||||
if state_path.exists() {
|
|
||||||
std::fs::remove_file(global_data.cfg.get_game_state_path())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_codename(config: &BotConfig) -> String {
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
let occupation = &config.occupation[rng.gen_range(0..config.occupation.len())];
|
|
||||||
let adj = &config.adjective[rng.gen_range(0..config.adjective.len())];
|
|
||||||
|
|
||||||
format!("{} {}", adj, occupation)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_user_to_game(
|
pub async fn add_user_to_game(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
guild: &Guild,
|
guild: &Guild,
|
||||||
global_data: &mut GlobalData,
|
global_data: &mut GlobalData,
|
||||||
discord_user: &Member,
|
discord_user: &Member,
|
||||||
) -> CommandResult {
|
) -> error::Result<()> {
|
||||||
let mut codename = generate_codename(&global_data.cfg);
|
let mut codename = game::generate_codename(&global_data.cfg);
|
||||||
|
|
||||||
while global_data.game_state.codename_exists(&codename) {
|
while global_data.game_state_mut()?.codename_exists(&codename) {
|
||||||
codename = generate_codename(&global_data.cfg);
|
codename = game::generate_codename(&global_data.cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = guild
|
let channel = guild
|
||||||
|
@ -175,7 +133,7 @@ pub async fn add_user_to_game(
|
||||||
.push_line("")
|
.push_line("")
|
||||||
.push("SUBJECT CODENAME: ")
|
.push("SUBJECT CODENAME: ")
|
||||||
.push_line(&codename)
|
.push_line(&codename)
|
||||||
.push(print_game_status(&global_data.game_state))
|
.push(global_data.print_game_status())
|
||||||
)
|
)
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
|
@ -188,7 +146,7 @@ pub async fn add_user_to_game(
|
||||||
codename,
|
codename,
|
||||||
};
|
};
|
||||||
|
|
||||||
global_data.game_state.player_data.push(player_data);
|
global_data.game_state_mut()?.player_data.push(player_data);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -203,21 +161,9 @@ pub fn build_system_message(msg: &str) -> String {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_game_status(game_state: &GameState) -> String {
|
pub async fn parse_duration_arg(args: &mut Args) -> error::Result<PhaseDuration> {
|
||||||
MessageBuilder::new()
|
match args.single::<PhaseDuration>() {
|
||||||
.push_line(format!(
|
Ok(d) => Ok(d),
|
||||||
"CURRENT EXPERIMENT PHASE: {} {}",
|
Err(_) => Err(WoxlfError::DurationParseError),
|
||||||
game_state.current_phase, game_state.phase_number
|
}
|
||||||
))
|
|
||||||
.push_line(format!(
|
|
||||||
"PHASE END TIME: {}",
|
|
||||||
game_state.get_phase_end_time()
|
|
||||||
))
|
|
||||||
.push_line(format!("PHASE ENDING {}", game_state.get_phase_countdown()))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_phase_end_timestamp(hours: u64) -> u64 {
|
|
||||||
let end_time = std::time::SystemTime::now() + std::time::Duration::from_secs(hours * 60 * 60);
|
|
||||||
end_time.duration_since(UNIX_EPOCH).unwrap().as_secs()
|
|
||||||
}
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod commands;
|
||||||
|
pub mod helper;
|
|
@ -0,0 +1,57 @@
|
||||||
|
use serenity::prelude::SerenityError;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, WoxlfError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WoxlfError {
|
||||||
|
IOError(std::io::Error),
|
||||||
|
GameStateParseError(toml::de::Error),
|
||||||
|
GameStateSerializeError(toml::ser::Error),
|
||||||
|
DurationParseError,
|
||||||
|
SerenityError(serenity::Error),
|
||||||
|
DiscordIdParseError(String),
|
||||||
|
GameNotInProgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WoxlfError {}
|
||||||
|
|
||||||
|
impl Display for WoxlfError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let msg = match self {
|
||||||
|
WoxlfError::IOError(e) => format!("IO Error: {}", e),
|
||||||
|
WoxlfError::GameStateParseError(e) => format!("Game State Parse Error: {}", e),
|
||||||
|
WoxlfError::GameStateSerializeError(e) => format!("Game State Serialize Error: {}", e),
|
||||||
|
WoxlfError::DurationParseError => "Unable to parse duration".to_string(),
|
||||||
|
WoxlfError::SerenityError(e) => format!("Serenity error: {}", e),
|
||||||
|
WoxlfError::DiscordIdParseError(e) => format!("Unable to parse player id {}", e),
|
||||||
|
WoxlfError::GameNotInProgress => "A game is not currently in progress".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "Woxlf Error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SerenityError> for WoxlfError {
|
||||||
|
fn from(err: SerenityError) -> Self {
|
||||||
|
Self::SerenityError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for WoxlfError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::IOError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for WoxlfError {
|
||||||
|
fn from(err: toml::de::Error) -> Self {
|
||||||
|
Self::GameStateParseError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::ser::Error> for WoxlfError {
|
||||||
|
fn from(err: toml::ser::Error) -> Self {
|
||||||
|
Self::GameStateSerializeError(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::WoxlfError;
|
||||||
|
use crate::game::player_data::PlayerData;
|
||||||
|
use crate::game::Phase;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct GameState {
|
||||||
|
pub phase_number: u64,
|
||||||
|
pub current_phase: Phase,
|
||||||
|
pub starting_phase: Phase,
|
||||||
|
pub phase_end_time: DateTime<Utc>,
|
||||||
|
pub player_data: Vec<PlayerData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameState {
|
||||||
|
pub fn new(starting_phase: Phase, starting_phase_duration: Duration) -> Self {
|
||||||
|
let phase_end_time = Utc::now() + starting_phase_duration;
|
||||||
|
Self {
|
||||||
|
phase_number: 1,
|
||||||
|
current_phase: starting_phase,
|
||||||
|
phase_end_time,
|
||||||
|
player_data: vec![],
|
||||||
|
starting_phase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codename_exists(&self, codename: &str) -> bool {
|
||||||
|
let codename = codename.to_lowercase();
|
||||||
|
|
||||||
|
self.player_data.iter().any(|data| {
|
||||||
|
let player_codename = data.codename.to_lowercase();
|
||||||
|
let adj_occupation: Vec<&str> = player_codename.split(' ').collect();
|
||||||
|
|
||||||
|
codename.contains(adj_occupation[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_from_channel(&self, channel_id: u64) -> Option<&PlayerData> {
|
||||||
|
self.player_data.iter().find(|p| p.channel == channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_from_discord_id(&self, discord_id: u64) -> Option<&PlayerData> {
|
||||||
|
self.player_data.iter().find(|p| p.discord_id == discord_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_from_channel_mut(&mut self, channel_id: u64) -> Option<&mut PlayerData> {
|
||||||
|
self.player_data
|
||||||
|
.iter_mut()
|
||||||
|
.find(|p| p.channel == channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_by_codename(&self, codename: &str) -> Option<PlayerData> {
|
||||||
|
self.player_data
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.codename.to_lowercase() == codename.to_lowercase())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_phase(&mut self, duration: Duration) {
|
||||||
|
if self.current_phase == Phase::Night {
|
||||||
|
self.current_phase = Phase::Day
|
||||||
|
} else {
|
||||||
|
self.current_phase = Phase::Night
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current_phase == self.starting_phase {
|
||||||
|
self.phase_number += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for player in &mut self.player_data {
|
||||||
|
player.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_phase_end_time(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_phase_end_time(&mut self, duration: Duration) {
|
||||||
|
self.phase_end_time = Utc::now() + duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_phase_end_time(&self) -> String {
|
||||||
|
format!("<t:{}:f>", self.phase_end_time.timestamp())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_phase_countdown(&self) -> String {
|
||||||
|
format!("<t:{}:R>", self.phase_end_time.timestamp())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_time_to_phase(&mut self, duration: Duration) {
|
||||||
|
self.phase_end_time = self.phase_end_time + duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vote_tallies(&self) -> HashMap<String, u32> {
|
||||||
|
let mut vote_set: HashMap<String, u32> = HashMap::new();
|
||||||
|
|
||||||
|
for player in &self.player_data {
|
||||||
|
if let Some(vote_target) = player.vote_target {
|
||||||
|
let target = self.get_player_from_discord_id(vote_target);
|
||||||
|
if let Some(target) = target {
|
||||||
|
*vote_set.entry(target.codename.clone()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vote_set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PhaseDuration(Duration);
|
||||||
|
|
||||||
|
impl From<PhaseDuration> for Duration {
|
||||||
|
fn from(dur: PhaseDuration) -> Self {
|
||||||
|
dur.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PhaseDuration {
|
||||||
|
type Err = WoxlfError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
let re = Regex::new(r"(((?P<hour>[0-9]*)h)?((?P<min>[0-9]*)m)?)").unwrap();
|
||||||
|
|
||||||
|
let mut duration = Duration::hours(0);
|
||||||
|
|
||||||
|
if re.is_match(s) {
|
||||||
|
let cap = re.captures_iter(s).next().unwrap();
|
||||||
|
|
||||||
|
if let Some(hour_str) = cap.name("hour") {
|
||||||
|
let hours: i64 = hour_str
|
||||||
|
.as_str()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| WoxlfError::DurationParseError)?;
|
||||||
|
|
||||||
|
duration = duration + Duration::hours(hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(minute_str) = cap.name("min") {
|
||||||
|
let minutes: i64 = minute_str
|
||||||
|
.as_str()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| WoxlfError::DurationParseError)?;
|
||||||
|
|
||||||
|
duration = duration + Duration::minutes(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PhaseDuration(duration))
|
||||||
|
} else {
|
||||||
|
Err(WoxlfError::DurationParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::prelude::{Mutex, TypeMapKey};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use crate::config::BotConfig;
|
||||||
|
use crate::error::{Result, WoxlfError};
|
||||||
|
use crate::game::game_state::GameState;
|
||||||
|
use crate::game::Phase;
|
||||||
|
use chrono::Duration;
|
||||||
|
use serenity::utils::MessageBuilder;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct GlobalData {
|
||||||
|
pub cfg: BotConfig,
|
||||||
|
pub game_state: Option<GameState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalData {
|
||||||
|
pub fn new(cfg: BotConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
cfg,
|
||||||
|
game_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_game(&mut self, starting_phase: Phase, starting_phase_duration: Duration) {
|
||||||
|
self.game_state = Some(GameState::new(starting_phase, starting_phase_duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_game_state(&mut self) -> Result<()> {
|
||||||
|
if let Some(game_state) = &mut self.game_state {
|
||||||
|
let s = toml::to_string_pretty(game_state)?;
|
||||||
|
|
||||||
|
let mut file = File::create(self.cfg.get_game_state_path())?;
|
||||||
|
|
||||||
|
file.write_all(s.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(WoxlfError::GameNotInProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_state_exists(&self) -> bool {
|
||||||
|
self.cfg.get_game_state_path().exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_game_state(&mut self) -> Result<()> {
|
||||||
|
let mut file = File::open(self.cfg.get_game_state_path())?;
|
||||||
|
|
||||||
|
let mut data = String::new();
|
||||||
|
|
||||||
|
file.read_to_string(&mut data)?;
|
||||||
|
|
||||||
|
self.game_state = Some(toml::from_str(&data)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_state_mut(&mut self) -> Result<&mut GameState> {
|
||||||
|
match &mut self.game_state {
|
||||||
|
None => Err(WoxlfError::GameNotInProgress),
|
||||||
|
Some(game_state) => Ok(game_state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_state(&self) -> Result<&GameState> {
|
||||||
|
match &self.game_state {
|
||||||
|
None => Err(WoxlfError::GameNotInProgress),
|
||||||
|
Some(game_state) => Ok(game_state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_game_state(&mut self) -> Result<()> {
|
||||||
|
self.game_state = None;
|
||||||
|
|
||||||
|
if self.game_state_exists() {
|
||||||
|
std::fs::remove_file(self.cfg.get_game_state_path())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_game_status(&self) -> String {
|
||||||
|
if let Some(game_state) = &self.game_state {
|
||||||
|
MessageBuilder::new()
|
||||||
|
.push_line(format!(
|
||||||
|
"CURRENT EXPERIMENT PHASE: {} {}",
|
||||||
|
game_state.current_phase, game_state.phase_number
|
||||||
|
))
|
||||||
|
.push_line(format!(
|
||||||
|
"PHASE END TIME: {}",
|
||||||
|
game_state.get_phase_end_time()
|
||||||
|
))
|
||||||
|
.push_line(format!("PHASE ENDING {}", game_state.get_phase_countdown()))
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
"Game not in progress.".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeMapKey for GlobalData {
|
||||||
|
type Value = Arc<Mutex<GlobalData>>;
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::BotConfig;
|
||||||
|
|
||||||
|
pub mod game_state;
|
||||||
|
pub mod global_data;
|
||||||
|
pub mod player_data;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Copy)]
|
||||||
|
pub enum Phase {
|
||||||
|
Day,
|
||||||
|
Night,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Phase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Night
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Phase {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let phase_name = match self {
|
||||||
|
Self::Day => "Day",
|
||||||
|
Self::Night => "Night",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{}", phase_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_codename(config: &BotConfig) -> String {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let occupation = &config.occupation[rng.gen_range(0..config.occupation.len())];
|
||||||
|
let adj = &config.adjective[rng.gen_range(0..config.adjective.len())];
|
||||||
|
|
||||||
|
format!("{} {}", adj, occupation)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub enum MessageSource {
|
||||||
|
Player(u64),
|
||||||
|
Host,
|
||||||
|
Automated,
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)]
|
||||||
|
pub struct PlayerData {
|
||||||
|
pub channel: u64,
|
||||||
|
pub discord_id: u64,
|
||||||
|
pub codename: String,
|
||||||
|
pub vote_target: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerData {
|
||||||
|
pub fn cast_vote(&mut self, vote_target_id: u64) {
|
||||||
|
self.vote_target = Some(vote_target_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.vote_target = None;
|
||||||
|
}
|
||||||
|
}
|
46
src/main.rs
46
src/main.rs
|
@ -1,18 +1,24 @@
|
||||||
mod commands;
|
use std::sync::Arc;
|
||||||
mod data;
|
|
||||||
mod helper;
|
|
||||||
|
|
||||||
use crate::commands::command_framework;
|
|
||||||
use crate::data::{Args, BotConfig, GlobalData, MessageSource};
|
|
||||||
use crate::helper::{get_game_state, send_msg_to_player_channels};
|
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::client::bridge::gateway::GatewayIntents;
|
use serenity::client::bridge::gateway::GatewayIntents;
|
||||||
use serenity::http::AttachmentType;
|
use serenity::http::AttachmentType;
|
||||||
use serenity::model::prelude::{Message, Ready};
|
use serenity::model::prelude::{Message, Ready};
|
||||||
use serenity::prelude::*;
|
use serenity::prelude::*;
|
||||||
use std::sync::Arc;
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
use discord::commands::command_framework;
|
||||||
|
use discord::helper::send_msg_to_player_channels;
|
||||||
|
use game::global_data::GlobalData;
|
||||||
|
use game::MessageSource;
|
||||||
|
|
||||||
|
use crate::config::{Args, BotConfig};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod discord;
|
||||||
|
mod error;
|
||||||
|
mod game;
|
||||||
|
|
||||||
struct Handler {}
|
struct Handler {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -28,12 +34,18 @@ impl EventHandler for Handler {
|
||||||
|
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
|
||||||
let game_data = data.get::<GlobalData>().unwrap();
|
let global_data = data.get::<GlobalData>().unwrap();
|
||||||
|
|
||||||
let game_data = game_data.lock().await;
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
if let Some(player_data) = game_data
|
if global_data.game_state.is_none() {
|
||||||
.game_state
|
// no game in progress
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(player_data) = global_data
|
||||||
|
.game_state()
|
||||||
|
.unwrap()
|
||||||
.get_player_from_channel(msg.channel_id.0)
|
.get_player_from_channel(msg.channel_id.0)
|
||||||
{
|
{
|
||||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
@ -48,13 +60,14 @@ impl EventHandler for Handler {
|
||||||
send_msg_to_player_channels(
|
send_msg_to_player_channels(
|
||||||
&ctx,
|
&ctx,
|
||||||
&guild,
|
&guild,
|
||||||
&game_data,
|
&mut global_data,
|
||||||
MessageSource::Player(msg.channel_id.0),
|
MessageSource::Player(msg.channel_id.0),
|
||||||
&user_msg,
|
&user_msg,
|
||||||
Some(attachments),
|
Some(attachments),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.expect("Unable to send message to players");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,8 +84,11 @@ async fn main() {
|
||||||
|
|
||||||
let mut global_data = GlobalData::new(bot_cfg.clone());
|
let mut global_data = GlobalData::new(bot_cfg.clone());
|
||||||
|
|
||||||
if get_game_state(&mut global_data).is_ok() {
|
if global_data.game_state_exists() {
|
||||||
println!("Resuming game...")
|
println!("Resuming game...");
|
||||||
|
global_data
|
||||||
|
.load_game_state()
|
||||||
|
.expect("Unable to open saved game state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = Client::builder(&bot_cfg.token)
|
let mut client = Client::builder(&bot_cfg.token)
|
||||||
|
|
Loading…
Reference in New Issue