Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chombot-common/src/data_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use log::error;
use poise::serenity_prelude::Context;
use tokio::time::sleep;

const DATA_UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 10);
const DATA_UPDATE_INTERVAL: Duration = Duration::from_mins(10);

pub trait WatchableData: Sized {
type Diff;
Expand Down
17 changes: 9 additions & 8 deletions chombot-kcc/src/chombot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use riichi_hand::parser::HandParseError;
use riichi_hand::raster_renderer::HandRenderError;
use tokio::try_join;

use crate::kcc3::data_types::{Chombo, Player, PlayerId};
use crate::kcc3::data_types::{Chombo, ChomboWeight, Player, PlayerId};
use crate::kcc3::{Kcc3Client, Kcc3ClientError};

#[derive(Debug)]
Expand Down Expand Up @@ -80,6 +80,7 @@ impl Chombot {
predicate: P,
create_new: F,
comment: &str,
weight: ChomboWeight,
) -> ChombotResult<Chombo>
where
P: (Fn(&Player) -> bool) + Send + Sync,
Expand All @@ -95,28 +96,28 @@ impl Chombot {
client.add_player(&create_new()).await?
};

let chombo = Chombo::new(Utc::now(), &player.id, comment);
let chombo = Chombo::new(Utc::now(), &player.id, comment, weight);
Ok(client.add_chombo(&chombo).await?)
}

pub async fn create_chombo_ranking(&self) -> ChombotResult<Vec<(Player, usize)>> {
pub async fn create_chombo_ranking(&self) -> ChombotResult<Vec<(Player, f64)>> {
let client = self.get_client()?;
let players_fut = client.get_players();
let chombos_fut = client.get_chombos();
let (players, chombos) = try_join!(players_fut, chombos_fut)?;

let mut player_map: HashMap<PlayerId, Player> =
players.into_iter().map(|x| (x.id.clone(), x)).collect();
let mut chombo_counts: HashMap<PlayerId, usize> = HashMap::new();
let mut player_scores: HashMap<PlayerId, f64> = HashMap::new();
for chombo in chombos {
let entry = chombo_counts.entry(chombo.player).or_insert(0);
*entry += 1;
let entry = player_scores.entry(chombo.player).or_insert(0.0);
*entry += chombo.weight.as_f64();
}
let mut result: Vec<(Player, usize)> = chombo_counts
let mut result: Vec<(Player, f64)> = player_scores
.into_iter()
.map(|(player_id, num)| (player_map.remove(&player_id).unwrap(), num))
.collect();
result.sort_by(|(_, num_1), (_, num_2)| num_2.cmp(num_1));
result.sort_by(|(_, num_1), (_, num_2)| f64::total_cmp(num_2, num_1));

Ok(result)
}
Expand Down
131 changes: 128 additions & 3 deletions chombot-kcc/src/kcc3/data_types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::fmt::{Debug, Display, Formatter};

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use poise::ChoiceParameter;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

#[derive(Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
pub struct PlayerId(pub String);
Expand Down Expand Up @@ -47,27 +48,115 @@ impl Player {
}
}

#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, ChoiceParameter)]
pub enum ChomboWeight {
#[default]
#[name = "1"]
W1,
#[name = "1.5"]
W1_5,
#[name = "2"]
W2,
#[name = "2.5"]
W2_5,
#[name = "3"]
W3,
#[name = "3.5"]
W3_5,
#[name = "4"]
W4,
#[name = "4.5"]
W4_5,
#[name = "5"]
W5,
#[name = "5.5"]
W5_5,
#[name = "6"]
W6,
}

impl ChomboWeight {
pub const fn as_f64(self) -> f64 {
match self {
Self::W1 => 1.0,
Self::W1_5 => 1.5,
Self::W2 => 2.0,
Self::W2_5 => 2.5,
Self::W3 => 3.0,
Self::W3_5 => 3.5,
Self::W4 => 4.0,
Self::W4_5 => 4.5,
Self::W5 => 5.0,
Self::W5_5 => 5.5,
Self::W6 => 6.0,
}
}

fn from_f64(v: f64) -> Self {
match v {
1.5 => Self::W1_5,
2.0 => Self::W2,
2.5 => Self::W2_5,
3.0 => Self::W3,
3.5 => Self::W3_5,
4.0 => Self::W4,
4.5 => Self::W4_5,
5.0 => Self::W5,
5.5 => Self::W5_5,
6.0 => Self::W6,
_ => Self::W1,
}
}
}

impl Display for ChomboWeight {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_f64())
}
}

impl Serialize for ChomboWeight {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.as_f64().serialize(serializer)
}
}

impl<'de> Deserialize<'de> for ChomboWeight {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = f64::deserialize(deserializer)?;
Ok(Self::from_f64(v))
}
}

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Chombo {
pub timestamp: DateTime<Utc>,
pub player: PlayerId,
#[serde(default)]
pub comment: String,
#[serde(default)]
pub weight: ChomboWeight,
}

impl Chombo {
pub fn new(timestamp: DateTime<Utc>, player: &PlayerId, comment: &str) -> Self {
pub fn new(
timestamp: DateTime<Utc>,
player: &PlayerId,
comment: &str,
weight: ChomboWeight,
) -> Self {
Self {
timestamp,
player: player.to_owned(),
comment: comment.to_owned(),
weight,
}
}
}

#[cfg(test)]
mod tests {
use crate::kcc3::data_types::{DiscordId, Player, PlayerId};
use crate::kcc3::data_types::{ChomboWeight, DiscordId, Player, PlayerId};

#[test]
fn short_name_should_return_nickname() {
Expand All @@ -92,4 +181,40 @@ mod tests {
};
assert_eq!(player.short_name(), "A B");
}

#[test]
fn as_f64() {
assert_eq!(ChomboWeight::W1.as_f64(), 1.0);
assert_eq!(ChomboWeight::W1_5.as_f64(), 1.5);
assert_eq!(ChomboWeight::W2.as_f64(), 2.0);
assert_eq!(ChomboWeight::W2_5.as_f64(), 2.5);
assert_eq!(ChomboWeight::W3.as_f64(), 3.0);
assert_eq!(ChomboWeight::W3_5.as_f64(), 3.5);
assert_eq!(ChomboWeight::W4.as_f64(), 4.0);
assert_eq!(ChomboWeight::W4_5.as_f64(), 4.5);
assert_eq!(ChomboWeight::W5.as_f64(), 5.0);
assert_eq!(ChomboWeight::W5_5.as_f64(), 5.5);
assert_eq!(ChomboWeight::W6.as_f64(), 6.0);
}

#[test]
fn from_f64() {
assert_eq!(ChomboWeight::from_f64(1.0), ChomboWeight::W1);
assert_eq!(ChomboWeight::from_f64(1.5), ChomboWeight::W1_5);
assert_eq!(ChomboWeight::from_f64(2.0), ChomboWeight::W2);
assert_eq!(ChomboWeight::from_f64(2.5), ChomboWeight::W2_5);
assert_eq!(ChomboWeight::from_f64(3.0), ChomboWeight::W3);
assert_eq!(ChomboWeight::from_f64(3.5), ChomboWeight::W3_5);
assert_eq!(ChomboWeight::from_f64(4.0), ChomboWeight::W4);
assert_eq!(ChomboWeight::from_f64(4.5), ChomboWeight::W4_5);
assert_eq!(ChomboWeight::from_f64(5.0), ChomboWeight::W5);
assert_eq!(ChomboWeight::from_f64(5.5), ChomboWeight::W5_5);
assert_eq!(ChomboWeight::from_f64(6.0), ChomboWeight::W6);
}

#[test]
fn from_f64_unknown_defaults_to_w1() {
assert_eq!(ChomboWeight::from_f64(0.0), ChomboWeight::W1);
assert_eq!(ChomboWeight::from_f64(7.0), ChomboWeight::W1);
}
}
72 changes: 69 additions & 3 deletions chombot-kcc/src/slash_commands/chombo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use poise::CreateReply;
use slug::slugify;

use crate::chombot::Chombot;
use crate::kcc3::data_types::{Chombo, DiscordId, Player, PlayerId};
use crate::kcc3::data_types::{Chombo, ChomboWeight, DiscordId, Player, PlayerId};
use crate::PoiseContext;

#[poise::command(slash_command, subcommands("ranking", "list", "add"))]
Expand Down Expand Up @@ -46,6 +46,7 @@ async fn add(
ctx: PoiseContext<'_>,
#[description = "User that made a chombo"] user: User,
#[description = "Chombo description"] description: String,
#[description = "MERS tournament weight (default: 1)"] weight: Option<ChomboWeight>,
) -> Result<()> {
let chombot = &ctx.data().kcc_chombot;
chombot
Expand All @@ -59,6 +60,7 @@ async fn add(
)
},
&description,
weight.unwrap_or_default(),
)
.await?;

Expand All @@ -82,7 +84,7 @@ async fn get_chombos_embed_entries(
Ok(chombo_ranking
.into_iter()
.take(DISCORD_EMBED_FIELD_LIMIT)
.map(|(player, num)| (player.short_name(), num.to_string(), true)))
.map(|(player, score)| (player.short_name(), score.to_string(), true)))
}

fn create_chombos_embed(entries: impl Iterator<Item = (String, String, bool)>) -> CreateEmbed {
Expand Down Expand Up @@ -112,13 +114,77 @@ async fn create_chombos_list(chombot: &Chombot) -> Result<String> {
Ok(result)
}

#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};

use super::*;

fn test_player() -> Player {
Player::new_from_discord(
PlayerId("test".to_string()),
"TestPlayer".to_string(),
DiscordId("123456".to_string()),
)
}

fn test_chombo(comment: &str, weight: ChomboWeight) -> Chombo {
let timestamp = Utc.with_ymd_and_hms(2025, 3, 15, 14, 30, 0).unwrap();
Chombo::new(timestamp, &PlayerId("test".to_string()), comment, weight)
}

#[test]
fn format_chombo_entry_default_weight_with_comment() {
let result = format_chombo_entry(
&test_player(),
&test_chombo("broke the wall", ChomboWeight::W1),
);
assert_eq!(
result,
"<@!123456> at Saturday, 2025-03-15 14:30: *broke the wall*\n"
);
}

#[test]
fn format_chombo_entry_custom_weight_with_comment() {
let result = format_chombo_entry(
&test_player(),
&test_chombo("broke the wall", ChomboWeight::W2_5),
);
assert_eq!(
result,
"<@!123456> at Saturday, 2025-03-15 14:30 (x2.5): *broke the wall*\n"
);
}

#[test]
fn format_chombo_entry_default_weight_no_comment() {
let result = format_chombo_entry(&test_player(), &test_chombo("", ChomboWeight::W1));
assert_eq!(result, "<@!123456> at Saturday, 2025-03-15 14:30\n");
}

#[test]
fn format_chombo_entry_custom_weight_no_comment() {
let result = format_chombo_entry(&test_player(), &test_chombo("", ChomboWeight::W2));
assert_eq!(result, "<@!123456> at Saturday, 2025-03-15 14:30 (x2)\n");
}
}

fn format_chombo_entry(player: &Player, chombo: &Chombo) -> String {
let comment = if chombo.comment.is_empty() {
String::new()
} else {
format!(": *{}*", chombo.comment)
};
let weight = if chombo.weight == ChomboWeight::default() {
String::new()
} else {
format!(" (x{})", chombo.weight)
};
let timestamp = chombo.timestamp.format("%A, %Y-%m-%d %H:%M");

format!("<@!{}> at {}{}\n", player.discord_id, timestamp, comment)
format!(
"<@!{}> at {}{}{}\n",
player.discord_id, timestamp, weight, comment
)
}
Loading