about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/database.rs54
-rw-r--r--src/main.rs22
-rw-r--r--src/util.rs42
3 files changed, 107 insertions, 11 deletions
diff --git a/src/database.rs b/src/database.rs
index 0fb6987..d5ccf92 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -72,18 +72,19 @@ impl Database {
 	pub fn get_leaderboard(&self, guild_id: u64) -> Result<Vec<BoardRow>, Error> {
 		// Don't deadlock!
 		let lb = self.leaderboard_id(guild_id)?;
+		let query = lb.sql_leaderboard_query();
 		let conn = self.conn.lock().unwrap();
 
-		let mut query = conn
-			.prepare(&lb.sql("SELECT * FROM leaderboard_LBID ORDER BY points DESC"))
-			.unwrap();
+		// The query that get's run sorts ties by preferring who got to that score first
+		let mut query = conn.prepare(&lb.sql(query)).unwrap();
+
 		let vec = query
 			.query_map((), |row| {
 				Ok(BoardRow {
-					user_id: row.get(0)?,
-					user_handle: row.get(1)?,
-					user_nickname: row.get(2)?,
-					points: row.get(3)?,
+					user_id: row.get(1)?,
+					user_handle: row.get(2)?,
+					user_nickname: row.get(3)?,
+					points: row.get(4)?,
 				})
 			})
 			.optional()
@@ -232,6 +233,15 @@ const CREATE_TABLE_LEADERBOARD_HISTORY: &'static str = "\
 		points INTEGER NOT NULL DEFAULT 0
 	);";
 
+const CREATE_TRIGGER_LDBCHANGETIME_UPDATE: &'static str = "\
+	CREATE TRIGGER ldbchangetime_update_LBID
+		AFTER UPDATE ON leaderboard_LBID
+		FOR EACH ROW
+		WHEN NEW.score_updated < OLD.score_updated OR OLD.score_updated IS NULL
+		BEGIN
+			UPDATE leaderboard_LBID SET score_updated=CURRENT_TIMESTAMP WHERE user_id=OLD.user_id;
+		END;";
+
 const CREATE_TRIGGER_LDBHIST_UPDATE: &'static str = "\
 	CREATE TRIGGER ldbdhist_update_LBID BEFORE UPDATE ON leaderboard_LBID
 		BEGIN
@@ -244,6 +254,19 @@ const CREATE_TRIGGER_LDBHIST_INSERT: &'static str = "\
 			INSERT INTO leaderboard_history_LBID(user_id, points) VALUES(new.user_id, new.points);
 		END;";
 
+const LEADERBOARD_QUERY: &'static str = "\
+	SELECT
+		history.timestamp,
+		leaderboard_LBID.*
+	FROM leaderboard_LBID
+	JOIN (
+	SELECT user_id, max(timestamp) as timestamp
+	FROM leaderboard_history_LBID
+	GROUP BY user_id
+	) history
+	ON history.user_id = leaderboard_LBID.user_id
+	ORDER BY leaderboard_LBID.points DESC, history.timestamp ASC;";
+
 struct LeaderboardTable {
 	id: usize,
 }
@@ -272,6 +295,10 @@ impl LeaderboardTable {
 	pub fn sql_create_insert_trigger_history_table(&self) -> String {
 		CREATE_TRIGGER_LDBHIST_INSERT.replace("LBID", &self.id.to_string())
 	}
+
+	pub fn sql_leaderboard_query(&self) -> String {
+		LEADERBOARD_QUERY.replace("LBID", &self.id.to_string())
+	}
 }
 
 #[derive(Clone, Debug)]
@@ -282,6 +309,19 @@ pub struct BoardRow {
 	pub points: i64,
 }
 
+#[derive(Clone, Debug)]
+pub struct Placement {
+	pub row: BoardRow,
+
+	/// Absolute placement sorting by points and then
+	/// TODO: sort by date points attained
+	pub placement: usize,
+	/// Placement allowing ties, and placing ties in the same place
+	pub placement_tie: usize,
+	/// Whether or not this placement was first of a tie. True if untied.
+	pub placement_first_of_tie: bool,
+}
+
 #[derive(Debug)]
 pub enum Error {
 	TableNotExist,
diff --git a/src/main.rs b/src/main.rs
index 0b34f30..c46ce7d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,11 +30,12 @@ use twilight_util::builder::{
 mod brain;
 mod database;
 mod import;
+mod util;
 
 const PROD_APP_ID: u64 = 1363055126264283136;
 #[allow(dead_code)]
 const DEV_APP_ID: u64 = 1363494986699771984;
-const APP_ID: u64 = DEV_APP_ID;
+const APP_ID: u64 = PROD_APP_ID;
 
 macro_rules! bail {
 	($msg:expr) => {
@@ -279,10 +280,23 @@ fn get_leaderboard(brain: Arc<Brain>, guild: Id<GuildMarker>) -> InteractionResp
 		}
 		Err(DbError::UserNotExist) => unreachable!(),
 		Ok(data) => {
-			let str = data
+			let placed = util::tiebreak_shared_positions(data);
+
+			let str = placed
 				.into_iter()
-				.enumerate()
-				.map(|(idx, br)| format!("{idx}. <@{}>: {}", br.user_id, br.points))
+				.map(|placement| {
+					if placement.placement_first_of_tie {
+						format!(
+							"`{:>2}` <@{}>: {}",
+							placement.placement_tie, placement.row.user_id, placement.row.points
+						)
+					} else {
+						format!(
+							"`  ` <@{}>: {}",
+							placement.row.user_id, placement.row.points
+						)
+					}
+				})
 				.collect::<Vec<String>>()
 				.join("\n");
 
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..a05eeb9
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,42 @@
+//! Uh-oh, a `util.rs` file. That can't be good.
+
+use crate::database::{BoardRow, Placement};
+
+pub fn tiebreak_shared_positions(board: Vec<BoardRow>) -> Vec<Placement> {
+	let first = match board.first() {
+		Some(r) => r.clone(),
+		None => return vec![],
+	};
+
+	let mut last_score = first.points;
+	let mut last_placed = 1;
+	let mut placed = vec![Placement {
+		row: first,
+		placement: 1,
+		placement_tie: 1,
+		placement_first_of_tie: true,
+	}];
+
+	for (idx, row) in board.into_iter().enumerate().skip(1) {
+		if row.points == last_score {
+			placed.push(Placement {
+				row,
+				placement: idx + 1,
+				placement_tie: last_placed,
+				placement_first_of_tie: false,
+			});
+		} else {
+			last_score = row.points;
+			last_placed = idx + 1;
+
+			placed.push(Placement {
+				row,
+				placement: idx + 1,
+				placement_tie: last_placed,
+				placement_first_of_tie: true,
+			});
+		}
+	}
+
+	placed
+}