Skip to content

Commit dd1d866

Browse files
authored
feat: support annotating chombos with MERS weight (#295)
1 parent 442d77d commit dd1d866

File tree

4 files changed

+207
-15
lines changed

4 files changed

+207
-15
lines changed

chombot-common/src/data_watcher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use log::error;
77
use poise::serenity_prelude::Context;
88
use tokio::time::sleep;
99

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

1212
pub trait WatchableData: Sized {
1313
type Diff;

chombot-kcc/src/chombot.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use riichi_hand::parser::HandParseError;
77
use riichi_hand::raster_renderer::HandRenderError;
88
use tokio::try_join;
99

10-
use crate::kcc3::data_types::{Chombo, Player, PlayerId};
10+
use crate::kcc3::data_types::{Chombo, ChomboWeight, Player, PlayerId};
1111
use crate::kcc3::{Kcc3Client, Kcc3ClientError};
1212

1313
#[derive(Debug)]
@@ -80,6 +80,7 @@ impl Chombot {
8080
predicate: P,
8181
create_new: F,
8282
comment: &str,
83+
weight: ChomboWeight,
8384
) -> ChombotResult<Chombo>
8485
where
8586
P: (Fn(&Player) -> bool) + Send + Sync,
@@ -95,28 +96,28 @@ impl Chombot {
9596
client.add_player(&create_new()).await?
9697
};
9798

98-
let chombo = Chombo::new(Utc::now(), &player.id, comment);
99+
let chombo = Chombo::new(Utc::now(), &player.id, comment, weight);
99100
Ok(client.add_chombo(&chombo).await?)
100101
}
101102

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

108109
let mut player_map: HashMap<PlayerId, Player> =
109110
players.into_iter().map(|x| (x.id.clone(), x)).collect();
110-
let mut chombo_counts: HashMap<PlayerId, usize> = HashMap::new();
111+
let mut player_scores: HashMap<PlayerId, f64> = HashMap::new();
111112
for chombo in chombos {
112-
let entry = chombo_counts.entry(chombo.player).or_insert(0);
113-
*entry += 1;
113+
let entry = player_scores.entry(chombo.player).or_insert(0.0);
114+
*entry += chombo.weight.as_f64();
114115
}
115-
let mut result: Vec<(Player, usize)> = chombo_counts
116+
let mut result: Vec<(Player, f64)> = player_scores
116117
.into_iter()
117118
.map(|(player_id, num)| (player_map.remove(&player_id).unwrap(), num))
118119
.collect();
119-
result.sort_by(|(_, num_1), (_, num_2)| num_2.cmp(num_1));
120+
result.sort_by(|(_, num_1), (_, num_2)| f64::total_cmp(num_2, num_1));
120121

121122
Ok(result)
122123
}

chombot-kcc/src/kcc3/data_types.rs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::fmt::{Debug, Display, Formatter};
22

33
use chrono::{DateTime, Utc};
4-
use serde::{Deserialize, Serialize};
4+
use poise::ChoiceParameter;
5+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
56

67
#[derive(Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
78
pub struct PlayerId(pub String);
@@ -47,27 +48,115 @@ impl Player {
4748
}
4849
}
4950

51+
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, ChoiceParameter)]
52+
pub enum ChomboWeight {
53+
#[default]
54+
#[name = "1"]
55+
W1,
56+
#[name = "1.5"]
57+
W1_5,
58+
#[name = "2"]
59+
W2,
60+
#[name = "2.5"]
61+
W2_5,
62+
#[name = "3"]
63+
W3,
64+
#[name = "3.5"]
65+
W3_5,
66+
#[name = "4"]
67+
W4,
68+
#[name = "4.5"]
69+
W4_5,
70+
#[name = "5"]
71+
W5,
72+
#[name = "5.5"]
73+
W5_5,
74+
#[name = "6"]
75+
W6,
76+
}
77+
78+
impl ChomboWeight {
79+
pub const fn as_f64(self) -> f64 {
80+
match self {
81+
Self::W1 => 1.0,
82+
Self::W1_5 => 1.5,
83+
Self::W2 => 2.0,
84+
Self::W2_5 => 2.5,
85+
Self::W3 => 3.0,
86+
Self::W3_5 => 3.5,
87+
Self::W4 => 4.0,
88+
Self::W4_5 => 4.5,
89+
Self::W5 => 5.0,
90+
Self::W5_5 => 5.5,
91+
Self::W6 => 6.0,
92+
}
93+
}
94+
95+
fn from_f64(v: f64) -> Self {
96+
match v {
97+
1.5 => Self::W1_5,
98+
2.0 => Self::W2,
99+
2.5 => Self::W2_5,
100+
3.0 => Self::W3,
101+
3.5 => Self::W3_5,
102+
4.0 => Self::W4,
103+
4.5 => Self::W4_5,
104+
5.0 => Self::W5,
105+
5.5 => Self::W5_5,
106+
6.0 => Self::W6,
107+
_ => Self::W1,
108+
}
109+
}
110+
}
111+
112+
impl Display for ChomboWeight {
113+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
114+
write!(f, "{}", self.as_f64())
115+
}
116+
}
117+
118+
impl Serialize for ChomboWeight {
119+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
120+
self.as_f64().serialize(serializer)
121+
}
122+
}
123+
124+
impl<'de> Deserialize<'de> for ChomboWeight {
125+
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
126+
let v = f64::deserialize(deserializer)?;
127+
Ok(Self::from_f64(v))
128+
}
129+
}
130+
50131
#[derive(Clone, Serialize, Deserialize, Debug)]
51132
pub struct Chombo {
52133
pub timestamp: DateTime<Utc>,
53134
pub player: PlayerId,
54135
#[serde(default)]
55136
pub comment: String,
137+
#[serde(default)]
138+
pub weight: ChomboWeight,
56139
}
57140

58141
impl Chombo {
59-
pub fn new(timestamp: DateTime<Utc>, player: &PlayerId, comment: &str) -> Self {
142+
pub fn new(
143+
timestamp: DateTime<Utc>,
144+
player: &PlayerId,
145+
comment: &str,
146+
weight: ChomboWeight,
147+
) -> Self {
60148
Self {
61149
timestamp,
62150
player: player.to_owned(),
63151
comment: comment.to_owned(),
152+
weight,
64153
}
65154
}
66155
}
67156

68157
#[cfg(test)]
69158
mod tests {
70-
use crate::kcc3::data_types::{DiscordId, Player, PlayerId};
159+
use crate::kcc3::data_types::{ChomboWeight, DiscordId, Player, PlayerId};
71160

72161
#[test]
73162
fn short_name_should_return_nickname() {
@@ -92,4 +181,40 @@ mod tests {
92181
};
93182
assert_eq!(player.short_name(), "A B");
94183
}
184+
185+
#[test]
186+
fn as_f64() {
187+
assert_eq!(ChomboWeight::W1.as_f64(), 1.0);
188+
assert_eq!(ChomboWeight::W1_5.as_f64(), 1.5);
189+
assert_eq!(ChomboWeight::W2.as_f64(), 2.0);
190+
assert_eq!(ChomboWeight::W2_5.as_f64(), 2.5);
191+
assert_eq!(ChomboWeight::W3.as_f64(), 3.0);
192+
assert_eq!(ChomboWeight::W3_5.as_f64(), 3.5);
193+
assert_eq!(ChomboWeight::W4.as_f64(), 4.0);
194+
assert_eq!(ChomboWeight::W4_5.as_f64(), 4.5);
195+
assert_eq!(ChomboWeight::W5.as_f64(), 5.0);
196+
assert_eq!(ChomboWeight::W5_5.as_f64(), 5.5);
197+
assert_eq!(ChomboWeight::W6.as_f64(), 6.0);
198+
}
199+
200+
#[test]
201+
fn from_f64() {
202+
assert_eq!(ChomboWeight::from_f64(1.0), ChomboWeight::W1);
203+
assert_eq!(ChomboWeight::from_f64(1.5), ChomboWeight::W1_5);
204+
assert_eq!(ChomboWeight::from_f64(2.0), ChomboWeight::W2);
205+
assert_eq!(ChomboWeight::from_f64(2.5), ChomboWeight::W2_5);
206+
assert_eq!(ChomboWeight::from_f64(3.0), ChomboWeight::W3);
207+
assert_eq!(ChomboWeight::from_f64(3.5), ChomboWeight::W3_5);
208+
assert_eq!(ChomboWeight::from_f64(4.0), ChomboWeight::W4);
209+
assert_eq!(ChomboWeight::from_f64(4.5), ChomboWeight::W4_5);
210+
assert_eq!(ChomboWeight::from_f64(5.0), ChomboWeight::W5);
211+
assert_eq!(ChomboWeight::from_f64(5.5), ChomboWeight::W5_5);
212+
assert_eq!(ChomboWeight::from_f64(6.0), ChomboWeight::W6);
213+
}
214+
215+
#[test]
216+
fn from_f64_unknown_defaults_to_w1() {
217+
assert_eq!(ChomboWeight::from_f64(0.0), ChomboWeight::W1);
218+
assert_eq!(ChomboWeight::from_f64(7.0), ChomboWeight::W1);
219+
}
95220
}

chombot-kcc/src/slash_commands/chombo.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use poise::CreateReply;
55
use slug::slugify;
66

77
use crate::chombot::Chombot;
8-
use crate::kcc3::data_types::{Chombo, DiscordId, Player, PlayerId};
8+
use crate::kcc3::data_types::{Chombo, ChomboWeight, DiscordId, Player, PlayerId};
99
use crate::PoiseContext;
1010

1111
#[poise::command(slash_command, subcommands("ranking", "list", "add"))]
@@ -46,6 +46,7 @@ async fn add(
4646
ctx: PoiseContext<'_>,
4747
#[description = "User that made a chombo"] user: User,
4848
#[description = "Chombo description"] description: String,
49+
#[description = "MERS tournament weight (default: 1)"] weight: Option<ChomboWeight>,
4950
) -> Result<()> {
5051
let chombot = &ctx.data().kcc_chombot;
5152
chombot
@@ -59,6 +60,7 @@ async fn add(
5960
)
6061
},
6162
&description,
63+
weight.unwrap_or_default(),
6264
)
6365
.await?;
6466

@@ -82,7 +84,7 @@ async fn get_chombos_embed_entries(
8284
Ok(chombo_ranking
8385
.into_iter()
8486
.take(DISCORD_EMBED_FIELD_LIMIT)
85-
.map(|(player, num)| (player.short_name(), num.to_string(), true)))
87+
.map(|(player, score)| (player.short_name(), score.to_string(), true)))
8688
}
8789

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

117+
#[cfg(test)]
118+
mod tests {
119+
use chrono::{TimeZone, Utc};
120+
121+
use super::*;
122+
123+
fn test_player() -> Player {
124+
Player::new_from_discord(
125+
PlayerId("test".to_string()),
126+
"TestPlayer".to_string(),
127+
DiscordId("123456".to_string()),
128+
)
129+
}
130+
131+
fn test_chombo(comment: &str, weight: ChomboWeight) -> Chombo {
132+
let timestamp = Utc.with_ymd_and_hms(2025, 3, 15, 14, 30, 0).unwrap();
133+
Chombo::new(timestamp, &PlayerId("test".to_string()), comment, weight)
134+
}
135+
136+
#[test]
137+
fn format_chombo_entry_default_weight_with_comment() {
138+
let result = format_chombo_entry(
139+
&test_player(),
140+
&test_chombo("broke the wall", ChomboWeight::W1),
141+
);
142+
assert_eq!(
143+
result,
144+
"<@!123456> at Saturday, 2025-03-15 14:30: *broke the wall*\n"
145+
);
146+
}
147+
148+
#[test]
149+
fn format_chombo_entry_custom_weight_with_comment() {
150+
let result = format_chombo_entry(
151+
&test_player(),
152+
&test_chombo("broke the wall", ChomboWeight::W2_5),
153+
);
154+
assert_eq!(
155+
result,
156+
"<@!123456> at Saturday, 2025-03-15 14:30 (x2.5): *broke the wall*\n"
157+
);
158+
}
159+
160+
#[test]
161+
fn format_chombo_entry_default_weight_no_comment() {
162+
let result = format_chombo_entry(&test_player(), &test_chombo("", ChomboWeight::W1));
163+
assert_eq!(result, "<@!123456> at Saturday, 2025-03-15 14:30\n");
164+
}
165+
166+
#[test]
167+
fn format_chombo_entry_custom_weight_no_comment() {
168+
let result = format_chombo_entry(&test_player(), &test_chombo("", ChomboWeight::W2));
169+
assert_eq!(result, "<@!123456> at Saturday, 2025-03-15 14:30 (x2)\n");
170+
}
171+
}
172+
115173
fn format_chombo_entry(player: &Player, chombo: &Chombo) -> String {
116174
let comment = if chombo.comment.is_empty() {
117175
String::new()
118176
} else {
119177
format!(": *{}*", chombo.comment)
120178
};
179+
let weight = if chombo.weight == ChomboWeight::default() {
180+
String::new()
181+
} else {
182+
format!(" (x{})", chombo.weight)
183+
};
121184
let timestamp = chombo.timestamp.format("%A, %Y-%m-%d %H:%M");
122185

123-
format!("<@!{}> at {}{}\n", player.discord_id, timestamp, comment)
186+
format!(
187+
"<@!{}> at {}{}{}\n",
188+
player.discord_id, timestamp, weight, comment
189+
)
124190
}

0 commit comments

Comments
 (0)