Added remaining bare minimum functionality
+ Added phase handling and player termination + Fixed an issue with some commands not saving game data + Added game_status command + Updated readme with commands and new features + clippy + fmtmsg_refactor
parent
e72d603149
commit
10fa6f6906
33
README.md
33
README.md
|
@ -5,10 +5,31 @@ Discord bot for managing an anonymous [Werewolf Game](https://en.wikipedia.org/w
|
||||||
A host gets a list of players to play, and then begins the game with the `!start` command.
|
A host gets a list of players to play, and then begins the game with the `!start` command.
|
||||||
|
|
||||||
Each player is assigned a channel where they will view the game through. The player can read and send messages
|
Each player is assigned a channel where they will view the game through. The player can read and send messages
|
||||||
in this channel normally. When a message is sent, it is forwarded to all other channels. The message's author
|
in this channel normally. When a message is sent, it is forwarded to all other player channels. The message's author
|
||||||
is obscured by a codename.
|
is obscured by a codename. During day phases, players can cast a vote for who they wish to "terminate" that day.
|
||||||
|
|
||||||
The bot also handles communications from the host, daily votes, and notifying players of deaths.
|
The game proceeds as a normal Werewolf game.
|
||||||
|
|
||||||
|
## Channels
|
||||||
|
* Host Channel: Used by the host to interact with the game, can also be used by spectators to see the game state.
|
||||||
|
* Real player names are displayed along with code names in this channel
|
||||||
|
* Player Channel: A channel for a single player, allows them to chat with the other players and run commands.
|
||||||
|
* Vote Channel: Contains all the votes made in a game.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
## Host
|
||||||
|
* `!start <list of player ids>` - starts the game
|
||||||
|
* `!end` - Ends the current game
|
||||||
|
* `!say <msg>` - Allows the host to speak into the game chat
|
||||||
|
* `!broadcast <msg>` - Broadcasts a system message, this message is then pinned in each player channel
|
||||||
|
* `!next_phase <duration> <msg>` - Send the next phase message. Also cycles the phase
|
||||||
|
* `!terminate <player>` - Kills a player and removes them from the game
|
||||||
|
* `!add_time <duration>` - Adds more time to the current game
|
||||||
|
|
||||||
|
## Players
|
||||||
|
* `!vote <player>` - Casts a vote for a player to be terminated. Only can be used during the day phase
|
||||||
|
* `!status` - Get the current game status. Includes time left in the phase and current vote tallies
|
||||||
|
|
||||||
## Example Config
|
## Example Config
|
||||||
```toml
|
```toml
|
||||||
|
@ -18,8 +39,12 @@ token = ""
|
||||||
app_id = 0
|
app_id = 0
|
||||||
# Channel to accept host commands from
|
# Channel to accept host commands from
|
||||||
host_channel = 1
|
host_channel = 1
|
||||||
|
# Channel to put vote status messages in
|
||||||
|
vote_channel = 2
|
||||||
# Category to crate the player cannels in
|
# Category to crate the player cannels in
|
||||||
category = 2
|
category = 3
|
||||||
|
# Directory to save game data into, game data will be saved as "wOxlf.toml"
|
||||||
|
game_state_dir = "."
|
||||||
|
|
||||||
# Random code names are generated in the format of <Adjective> <Occupation>
|
# Random code names are generated in the format of <Adjective> <Occupation>
|
||||||
|
|
||||||
|
|
370
src/commands.rs
370
src/commands.rs
|
@ -1,87 +1,22 @@
|
||||||
use crate::data::{BotConfig, GlobalData, MessageSource, PlayerData};
|
|
||||||
use crate::helper::{clear_game_state, save_game_state, send_msg_to_player_channels};
|
|
||||||
use rand::Rng;
|
|
||||||
use serenity::framework::standard::macros::{command, group};
|
use serenity::framework::standard::macros::{command, group};
|
||||||
use serenity::framework::standard::{Args, CommandResult};
|
use serenity::framework::standard::{Args, CommandResult};
|
||||||
use serenity::framework::StandardFramework;
|
use serenity::framework::StandardFramework;
|
||||||
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
|
|
||||||
use serenity::model::guild::{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::model::Permissions;
|
|
||||||
use serenity::prelude::Context;
|
use serenity::prelude::Context;
|
||||||
use serenity::utils::MessageBuilder;
|
use serenity::utils::MessageBuilder;
|
||||||
|
|
||||||
|
use crate::data::{GlobalData, MessageSource, Phase};
|
||||||
|
use crate::helper;
|
||||||
|
use crate::helper::{
|
||||||
|
build_system_message, clear_game_state, get_phase_end_timestamp, print_game_status,
|
||||||
|
save_game_state, send_msg_to_player_channels,
|
||||||
|
};
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[commands(start, say, end, broadcast)]
|
#[commands(start, say, end, broadcast, next_phase, terminate, add_time)]
|
||||||
struct Host;
|
struct Host;
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_user_to_game(
|
|
||||||
ctx: &Context,
|
|
||||||
guild: &Guild,
|
|
||||||
global_data: &mut GlobalData,
|
|
||||||
discord_user: &Member,
|
|
||||||
) -> CommandResult {
|
|
||||||
let mut codename = generate_codename(&global_data.cfg);
|
|
||||||
|
|
||||||
while global_data.game_state.codename_exists(&codename) {
|
|
||||||
codename = generate_codename(&global_data.cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
let channel = guild
|
|
||||||
.create_channel(&ctx.http, |c| {
|
|
||||||
c.category(&ChannelId::from(global_data.cfg.category))
|
|
||||||
.name(format!("{}'s Channel", discord_user.display_name()))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let allow = Permissions::SEND_MESSAGES
|
|
||||||
| Permissions::READ_MESSAGE_HISTORY
|
|
||||||
| Permissions::READ_MESSAGE_HISTORY;
|
|
||||||
|
|
||||||
let overwrite = PermissionOverwrite {
|
|
||||||
allow,
|
|
||||||
deny: Default::default(),
|
|
||||||
kind: PermissionOverwriteType::Member(discord_user.user.id),
|
|
||||||
};
|
|
||||||
|
|
||||||
channel.create_permission(&ctx.http, &overwrite).await?;
|
|
||||||
|
|
||||||
let msg = channel.send_message(&ctx.http, |m| {
|
|
||||||
m.content(MessageBuilder::new()
|
|
||||||
.push("Welcome ")
|
|
||||||
.mention(discord_user)
|
|
||||||
.push_line(" to your WOxlf Terminal. You may use this terminal to communicate to other subjects.")
|
|
||||||
.push_line("You will also use this terminal for choosing one of your fellow subjects for termination.")
|
|
||||||
.push_line("Happy testing :)")
|
|
||||||
.push_line("")
|
|
||||||
.push("SUBJECT CODENAME: ")
|
|
||||||
.push_line(&codename)
|
|
||||||
)
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
channel.pin(&ctx.http, msg.id).await?;
|
|
||||||
|
|
||||||
let player_data = PlayerData {
|
|
||||||
channel: channel.id.0,
|
|
||||||
discord_id: discord_user.user.id.0,
|
|
||||||
codename,
|
|
||||||
};
|
|
||||||
|
|
||||||
global_data.game_state.player_data.push(player_data);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
#[allowed_roles("wolfx host")]
|
#[allowed_roles("wolfx host")]
|
||||||
|
@ -96,9 +31,21 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
|
||||||
clear_game_state(&mut global_data).unwrap();
|
clear_game_state(&mut global_data).unwrap();
|
||||||
|
|
||||||
|
let duration = match args.single::<u64>() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
msg.reply(&ctx.http, "Error parsing phase duration!")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
global_data.game_state.phase_end_time = get_phase_end_timestamp(duration);
|
||||||
|
|
||||||
for player in args.iter::<u64>().flatten() {
|
for player in args.iter::<u64>().flatten() {
|
||||||
if let Some(discord_user) = guild.members.get(&UserId::from(player)) {
|
if let Some(discord_user) = guild.members.get(&UserId::from(player)) {
|
||||||
add_user_to_game(ctx, &guild, &mut global_data, discord_user).await?;
|
helper::add_user_to_game(ctx, &guild, &mut global_data, discord_user).await?;
|
||||||
} else {
|
} else {
|
||||||
msg.reply(
|
msg.reply(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
|
@ -136,6 +83,7 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
||||||
|
|
||||||
clear_game_state(&mut global_data).unwrap();
|
clear_game_state(&mut global_data).unwrap();
|
||||||
|
|
||||||
|
msg.reply(&ctx.http, "Game ended!").await.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,21 +114,281 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
|
||||||
let global_data = global_data.lock().await;
|
let global_data = global_data.lock().await;
|
||||||
|
|
||||||
let msg = MessageBuilder::new()
|
let msg = build_system_message(args.rest());
|
||||||
.push_bold_line("\\*\\*IMPORTANT wOxlf SYSTEM MESSAGE\\*\\*")
|
|
||||||
.push_line("")
|
|
||||||
.push_line(args.rest())
|
|
||||||
.push_line("")
|
|
||||||
.push_bold_line("\\*\\*END OF SYSTEM MESSAGE\\*\\*")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
send_msg_to_player_channels(ctx, &guild, &global_data, MessageSource::Host, &msg, true).await;
|
send_msg_to_player_channels(ctx, &guild, &global_data, MessageSource::Host, &msg, true).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[allowed_roles("wolfx host")]
|
||||||
|
async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let mut data = ctx.data.write().await;
|
||||||
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
|
let duration = match args.single::<u64>() {
|
||||||
|
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.phase_end_time = get_phase_end_timestamp(duration);
|
||||||
|
|
||||||
|
let broadcast = MessageBuilder::new()
|
||||||
|
.push_line(args.rest())
|
||||||
|
.push_line("")
|
||||||
|
.push(print_game_status(&global_data.game_state))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let broadcast = build_system_message(&broadcast);
|
||||||
|
|
||||||
|
send_msg_to_player_channels(
|
||||||
|
ctx,
|
||||||
|
&guild,
|
||||||
|
&global_data,
|
||||||
|
MessageSource::Host,
|
||||||
|
&broadcast,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if global_data.game_state.current_phase == Phase::Day {
|
||||||
|
let vote_channel = guild
|
||||||
|
.channels
|
||||||
|
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||||
|
.unwrap();
|
||||||
|
vote_channel
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
m.content(format!(
|
||||||
|
"**DAY {} VOTES:**",
|
||||||
|
global_data.game_state.phase_number
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.reply(
|
||||||
|
&ctx.http,
|
||||||
|
format!(
|
||||||
|
"Phase has been cycled to {}.",
|
||||||
|
&global_data.game_state.current_phase
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
save_game_state(&global_data).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[allowed_roles("wolfx host")]
|
||||||
|
async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
let mut data = ctx.data.write().await;
|
||||||
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
|
let target = args.rest().to_lowercase();
|
||||||
|
let index = global_data
|
||||||
|
.game_state
|
||||||
|
.player_data
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.codename.to_lowercase() == target);
|
||||||
|
|
||||||
|
if let Some(index) = index {
|
||||||
|
let player = global_data.game_state.player_data.remove(index);
|
||||||
|
|
||||||
|
let player_channel = guild
|
||||||
|
.channels
|
||||||
|
.get(&ChannelId::from(player.channel))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
player_channel.delete(&ctx.http).await.unwrap();
|
||||||
|
|
||||||
|
msg.reply(
|
||||||
|
&ctx.http,
|
||||||
|
format!("{} has been terminated.", player.codename),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
msg.reply(&ctx.http, "No subject found with that codename.")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
save_game_state(&global_data).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[allowed_roles("wolfx host")]
|
||||||
|
async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let mut data = ctx.data.write().await;
|
||||||
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
|
global_data.game_state.next_phase();
|
||||||
|
|
||||||
|
let duration = match args.single::<u64>() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let broadcast = MessageBuilder::new()
|
||||||
|
.push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!")
|
||||||
|
.push_line("")
|
||||||
|
.push(print_game_status(&global_data.game_state))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let broadcast = build_system_message(&broadcast);
|
||||||
|
|
||||||
|
send_msg_to_player_channels(
|
||||||
|
ctx,
|
||||||
|
&guild,
|
||||||
|
&global_data,
|
||||||
|
MessageSource::Host,
|
||||||
|
&broadcast,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
msg.reply(&ctx.http, "Phase has been updated")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
save_game_state(&global_data).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[group]
|
||||||
|
#[commands(vote, status)]
|
||||||
|
struct Player;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
|
let mut data = ctx.data.write().await;
|
||||||
|
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let mut global_data = global_data.lock().await;
|
||||||
|
|
||||||
|
if global_data.game_state.current_phase != Phase::Day {
|
||||||
|
msg.reply(
|
||||||
|
&ctx.http,
|
||||||
|
"You can only select subject for termination during the day!",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if global_data
|
||||||
|
.game_state
|
||||||
|
.get_player_from_channel(msg.channel_id.0)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
let target_player = global_data.game_state.get_player_by_codename(args.rest());
|
||||||
|
|
||||||
|
if let Some(target_player) = target_player {
|
||||||
|
let vote_channel = guild
|
||||||
|
.channels
|
||||||
|
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let player_data = global_data
|
||||||
|
.game_state
|
||||||
|
.get_player_from_channel_mut(msg.channel_id.0)
|
||||||
|
.unwrap();
|
||||||
|
player_data.vote_target = Some(target_player.channel);
|
||||||
|
|
||||||
|
vote_channel
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
m.content(format!(
|
||||||
|
"{} has selected {} for termination",
|
||||||
|
&player_data.codename, target_player.codename
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
msg.reply(&ctx.http, "Subject not found!").await.unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msg.reply(
|
||||||
|
&ctx.http,
|
||||||
|
"This command needs to be run in a game channel, goober",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
save_game_state(&global_data).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let global_data = data.get::<GlobalData>().unwrap();
|
||||||
|
|
||||||
|
let global_data = global_data.lock().await;
|
||||||
|
|
||||||
|
let mut msg_builder = MessageBuilder::new();
|
||||||
|
|
||||||
|
msg_builder.push(print_game_status(&global_data.game_state));
|
||||||
|
|
||||||
|
if global_data.game_state.current_phase == Phase::Day {
|
||||||
|
msg_builder.push_line("");
|
||||||
|
|
||||||
|
let vote_tallies = global_data.game_state.get_vote_tallies();
|
||||||
|
|
||||||
|
if vote_tallies.is_empty() {
|
||||||
|
msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST");
|
||||||
|
} else {
|
||||||
|
msg_builder.push_line("TERMINATION VOTE TALLIES:");
|
||||||
|
for (player, tally) in global_data.game_state.get_vote_tallies() {
|
||||||
|
msg_builder.push_line(format!("{}: {}", player, tally));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.reply(&ctx.http, msg_builder.build()).await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
74
src/data.rs
74
src/data.rs
|
@ -1,6 +1,8 @@
|
||||||
use config::{Config, File};
|
use config::{Config, File};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serenity::prelude::TypeMapKey;
|
use serenity::prelude::TypeMapKey;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -18,6 +20,7 @@ pub struct BotConfig {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub app_id: u64,
|
pub app_id: u64,
|
||||||
pub host_channel: u64,
|
pub host_channel: u64,
|
||||||
|
pub vote_channel: u64,
|
||||||
pub category: u64,
|
pub category: u64,
|
||||||
pub game_state_dir: PathBuf,
|
pub game_state_dir: PathBuf,
|
||||||
pub occupation: Vec<String>,
|
pub occupation: Vec<String>,
|
||||||
|
@ -38,7 +41,7 @@ impl BotConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
|
||||||
pub enum Phase {
|
pub enum Phase {
|
||||||
Day,
|
Day,
|
||||||
Night,
|
Night,
|
||||||
|
@ -50,16 +53,30 @@ impl Default for Phase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)]
|
||||||
pub struct PlayerData {
|
pub struct PlayerData {
|
||||||
pub channel: u64,
|
pub channel: u64,
|
||||||
pub discord_id: u64,
|
pub discord_id: u64,
|
||||||
pub codename: String,
|
pub codename: String,
|
||||||
|
pub vote_target: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct GameState {
|
pub struct GameState {
|
||||||
|
pub phase_number: u64,
|
||||||
pub current_phase: Phase,
|
pub current_phase: Phase,
|
||||||
|
pub phase_end_time: u64,
|
||||||
pub player_data: Vec<PlayerData>,
|
pub player_data: Vec<PlayerData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,11 +90,66 @@ impl GameState {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.player_data.clear();
|
self.player_data.clear();
|
||||||
self.current_phase = Phase::Night;
|
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> {
|
pub fn get_player_from_channel(&self, channel_id: u64) -> Option<&PlayerData> {
|
||||||
self.player_data.iter().find(|p| p.channel == channel_id)
|
self.player_data.iter().find(|p| p.channel == channel_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_channel(vote_target);
|
||||||
|
if let Some(target) = target {
|
||||||
|
*vote_set.entry(target.codename.clone()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vote_set
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
|
112
src/helper.rs
112
src/helper.rs
|
@ -1,10 +1,19 @@
|
||||||
use crate::data::{GlobalData, MessageSource};
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
|
||||||
|
use serenity::model::guild::Member;
|
||||||
use serenity::model::id::UserId;
|
use serenity::model::id::UserId;
|
||||||
use serenity::model::prelude::ChannelId;
|
use serenity::model::prelude::ChannelId;
|
||||||
use serenity::model::prelude::Guild;
|
use serenity::model::prelude::Guild;
|
||||||
|
use serenity::model::Permissions;
|
||||||
use serenity::prelude::Context;
|
use serenity::prelude::Context;
|
||||||
use std::fs::File;
|
use serenity::utils::MessageBuilder;
|
||||||
use std::io::prelude::*;
|
|
||||||
|
use crate::data::{BotConfig, GameState, GlobalData, MessageSource, PlayerData};
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
pub async fn send_msg_to_player_channels(
|
pub async fn send_msg_to_player_channels(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
@ -97,3 +106,100 @@ pub fn clear_game_state(global_data: &mut GlobalData) -> std::io::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(
|
||||||
|
ctx: &Context,
|
||||||
|
guild: &Guild,
|
||||||
|
global_data: &mut GlobalData,
|
||||||
|
discord_user: &Member,
|
||||||
|
) -> CommandResult {
|
||||||
|
let mut codename = generate_codename(&global_data.cfg);
|
||||||
|
|
||||||
|
while global_data.game_state.codename_exists(&codename) {
|
||||||
|
codename = generate_codename(&global_data.cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = guild
|
||||||
|
.create_channel(&ctx.http, |c| {
|
||||||
|
c.category(&ChannelId::from(global_data.cfg.category))
|
||||||
|
.name(format!("{}'s Channel", discord_user.display_name()))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let allow = Permissions::SEND_MESSAGES
|
||||||
|
| Permissions::READ_MESSAGE_HISTORY
|
||||||
|
| Permissions::READ_MESSAGE_HISTORY;
|
||||||
|
|
||||||
|
let overwrite = PermissionOverwrite {
|
||||||
|
allow,
|
||||||
|
deny: Default::default(),
|
||||||
|
kind: PermissionOverwriteType::Member(discord_user.user.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.create_permission(&ctx.http, &overwrite).await?;
|
||||||
|
|
||||||
|
let msg = channel.send_message(&ctx.http, |m| {
|
||||||
|
m.content(MessageBuilder::new()
|
||||||
|
.push("Welcome ")
|
||||||
|
.mention(discord_user)
|
||||||
|
.push_line(" to your WOxlf Terminal. You may use this terminal to communicate to other subjects.")
|
||||||
|
.push_line("You will also use this terminal for choosing one of your fellow subjects for termination.")
|
||||||
|
.push_line("Happy testing :)")
|
||||||
|
.push_line("")
|
||||||
|
.push("SUBJECT CODENAME: ")
|
||||||
|
.push_line(&codename)
|
||||||
|
.push(print_game_status(&global_data.game_state))
|
||||||
|
)
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
channel.pin(&ctx.http, msg.id).await?;
|
||||||
|
|
||||||
|
let player_data = PlayerData {
|
||||||
|
channel: channel.id.0,
|
||||||
|
discord_id: discord_user.user.id.0,
|
||||||
|
vote_target: None,
|
||||||
|
codename,
|
||||||
|
};
|
||||||
|
|
||||||
|
global_data.game_state.player_data.push(player_data);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_system_message(msg: &str) -> String {
|
||||||
|
MessageBuilder::new()
|
||||||
|
.push_bold_line("\\*\\*IMPORTANT wOxlf SYSTEM MESSAGE\\*\\*")
|
||||||
|
.push_line("")
|
||||||
|
.push_line(msg)
|
||||||
|
.push_line("")
|
||||||
|
.push_bold_line("\\*\\*END OF SYSTEM MESSAGE\\*\\*")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_game_status(game_state: &GameState) -> String {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue