Skip to content

Commit

Permalink
ADD Leaderboard component
Browse files Browse the repository at this point in the history
  • Loading branch information
tzhf committed Apr 25, 2024
1 parent 9545e64 commit 9af3772
Show file tree
Hide file tree
Showing 19 changed files with 938 additions and 525 deletions.
30 changes: 21 additions & 9 deletions src/main/GameHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,22 @@ export default class GameHandler {
this.#requestAuthentication()
})

ipcMain.handle('get-global-stats', async (_event, sinceTime: StatisticsInterval) => {
const date = await parseUserDate(sinceTime)
return this.#db.getGlobalStats(date.timeStamp)
})

ipcMain.handle('clear-global-stats', async (_event, sinceTime: StatisticsInterval) => {
const date = await parseUserDate(sinceTime)
try {
await this.#db.deleteGlobalStats(date.timeStamp)
return true
} catch (e) {
console.log(e)
return false
}
})

ipcMain.handle('get-banned-users', () => {
return this.#db.getBannedUsers()
})
Expand All @@ -242,11 +258,6 @@ export default class GameHandler {
ipcMain.on('delete-banned-user', (_event, username: string) => {
this.#db.deleteBannedUser(username)
})

ipcMain.on('clear-stats', async () => {
await this.#db.clear()
await this.#backend?.sendMessage('All stats cleared 🗑️', { system: true })
})
}

getTwitchConnectionState(): TwitchConnectionState {
Expand Down Expand Up @@ -558,7 +569,9 @@ export default class GameHandler {
if (dateInfo.timeStamp === 0) {
await this.#backend?.sendMessage(`${userstate['display-name']} you've never guessed yet.`)
} else {
await this.#backend?.sendMessage(`${userstate['display-name']} no guesses for this time period.`)
await this.#backend?.sendMessage(
`${userstate['display-name']} no guesses for this time period.`
)
}
} else {
let msg = `${getEmoji(userInfo.flag)} ${userInfo.username}: `
Expand Down Expand Up @@ -589,7 +602,7 @@ export default class GameHandler {
await this.#backend?.sendMessage(`${userstate['display-name']}: ${dateInfo.description}.`)
return
}
const { streak, victories, perfects } = this.#db.getGlobalStats(dateInfo.timeStamp)
const { streak, victories, perfects } = this.#db.getBestStats(dateInfo.timeStamp)
if (!streak && !victories && !perfects) {
await this.#backend?.sendMessage('No stats available.')
} else {
Expand All @@ -603,8 +616,7 @@ export default class GameHandler {
if (perfects) {
msg += `5ks: ${perfects.perfects} (${perfects.username}). `
}
if (!dateInfo.description)
{
if (!dateInfo.description) {
await this.#backend?.sendMessage(`Channels best: ${msg}`)
} else {
await this.#backend?.sendMessage(`Best ${dateInfo.description}: ${msg}`)
Expand Down
247 changes: 167 additions & 80 deletions src/main/utils/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,20 +402,6 @@ class db {
})
}

// setGuessStreak(guessId: string, streak: number, lastStreak: number | null = null) {
// const updateGuess = this.#db.prepare(`
// UPDATE guesses
// SET streak = :streak, last_streak = :lastStreak
// WHERE id = :id
// `)

// updateGuess.run({
// id: guessId,
// streak,
// lastStreak
// })
// }

getUserStreak(userId: string): { id: string; count: number; lastLocation: LatLng } | undefined {
const stmt = this.#db.prepare(`
SELECT streaks.id, streaks.count, rounds.location
Expand Down Expand Up @@ -765,7 +751,7 @@ class db {
WHERE users.id = :id
`)

const record = stmt.get({ id: userId, since: sinceTimestamp}) as
const record = stmt.get({ id: userId, since: sinceTimestamp }) as
| {
username: string
flag: string
Expand Down Expand Up @@ -794,40 +780,58 @@ class db {
: undefined
}

getGlobalStats(sinceTime: number = 0) {
resetUserStats(userId: string) {
this.#db
.prepare(`UPDATE users SET current_streak_id = NULL, reset_at = :resetAt WHERE id = :id`)
.run({
id: userId,
resetAt: timestamp()
})
}

statsQueries() {
const streakQuery = this.#db.prepare(`
SELECT users.id, users.username, MAX(streaks.count) AS streak
FROM users, streaks
WHERE NOT users.id = 'BROADCASTER'
AND streaks.user_id = users.id
AND streaks.created_at > users.reset_at
AND streaks.created_at > :since
GROUP BY users.id
ORDER BY streak DESC
`)
SELECT users.id, users.username, users.avatar, users.color, users.flag, MAX(streaks.count) AS streak
FROM users, streaks
WHERE streaks.user_id = users.id
AND streaks.updated_at > users.reset_at
AND streaks.updated_at > :since
GROUP BY users.id
ORDER BY streak DESC
LIMIT 100
`)

const victoriesQuery = this.#db.prepare(`
SELECT users.id, users.username, COUNT(*) AS victories
FROM game_winners, users
WHERE NOT users.id = 'BROADCASTER'
AND users.id = game_winners.user_id
AND game_winners.created_at > users.reset_at
AND game_winners.created_at > :since
GROUP BY users.id
ORDER BY victories DESC
`)
SELECT users.id, users.username, users.avatar, users.color, users.flag, COUNT(*) AS victories
FROM game_winners, users
WHERE users.id = game_winners.user_id
AND game_winners.created_at > users.reset_at
AND game_winners.created_at > :since
GROUP BY users.id
ORDER BY victories DESC
LIMIT 100
`)

const perfectQuery = this.#db.prepare(`
SELECT users.id, users.username, COUNT(guesses.id) AS perfects
FROM users
LEFT JOIN guesses ON guesses.user_id = users.id
AND guesses.created_at > users.reset_at
AND guesses.created_at > :since
WHERE NOT users.id = 'BROADCASTER'
AND guesses.score = 5000
GROUP BY users.id
ORDER BY perfects DESC
`)
SELECT users.id, users.username, users.avatar, users.color, users.flag, COUNT(guesses.id) AS perfects
FROM users
LEFT JOIN guesses ON guesses.user_id = users.id
AND guesses.created_at > users.reset_at
AND guesses.created_at > :since
WHERE guesses.score = 5000
GROUP BY users.id
ORDER BY perfects DESC
LIMIT 100
`)

return { streakQuery, victoriesQuery, perfectQuery }
}

/**
* Get best stats for !best command
*/
getBestStats(sinceTime: number = 0) {
const { streakQuery, victoriesQuery, perfectQuery } = this.statsQueries()

const bestStreak = streakQuery.get({ since: sinceTime }) as
| { id: string; username: string; streak: number }
Expand All @@ -846,13 +850,116 @@ class db {
}
}

resetUserStats(userId: string) {
/**
* Get all sorted stats in given interval for Leaderboard
*/
getGlobalStats(sinceTime: number = 0): Statistics {
const { streakQuery, victoriesQuery, perfectQuery } = this.statsQueries()

const streaksRecord = streakQuery.all({ since: sinceTime }) as {
id: string
username: string
avatar: string | null
color: string
flag: string | null
streak: number
}[]

const streaks = streaksRecord.map((record) => ({
player: {
userId: record.id,
username: record.username,
avatar: record.avatar,
color: record.color,
flag: record.flag
},
count: record.streak
}))

const victoriesRecord = victoriesQuery.all({ since: sinceTime }) as {
id: string
username: string
avatar: string | null
color: string
flag: string | null
victories: number
}[]

const victories = victoriesRecord.map((record) => ({
player: {
userId: record.id,
username: record.username,
avatar: record.avatar,
color: record.color,
flag: record.flag
},
count: record.victories
}))

const perfectsRecord = perfectQuery.all({ since: sinceTime }) as {
id: string
username: string
avatar: string | null
color: string
flag: string | null
perfects: number
}[]

const perfects = perfectsRecord.map((record) => ({
player: {
userId: record.id,
username: record.username,
avatar: record.avatar,
color: record.color,
flag: record.flag
},
count: record.perfects
}))

return {
streaks,
victories,
perfects
}
}

/**
* Delete all records in given interval
*/
async deleteGlobalStats(sinceTime: number = 0) {
if (!this.#db.memory) {
try {
await this.#db.backup(`${this.#db.name}.bak`)
} catch {}
}

// For now i'm commenting the transaction, with it there's a forein_key constraint error when deleting ROUNDS (even with foreign_keys = OFF),
// no idea where it comes from, maybe because of the game_winners VIEW ?
// Best would be DELETE IN CASCADE but i failed to add that migration
// const deleteEverything = this.#db.transaction(() => {
// Disable foreign key checking while we delete everything
this.#db.prepare('PRAGMA foreign_keys = OFF;').run()
this.#db
.prepare(`UPDATE users SET current_streak_id = NULL, reset_at = :resetAt WHERE id = :id`)
.run({
id: userId,
resetAt: timestamp()
})
.prepare(
'UPDATE users SET current_streak_id = NULL WHERE current_streak_id IN (SELECT id FROM streaks WHERE updated_at > :since);'
)
.run({ since: sinceTime })
this.#db.prepare('DELETE FROM streaks WHERE updated_at > :since;').run({ since: sinceTime })
this.#db.prepare('DELETE FROM guesses WHERE created_at > :since;').run({ since: sinceTime })
// For ROUNDS and GAMES we keep the most recent record in case stats are cleared during a game
this.#db
.prepare(
'DELETE from rounds WHERE game_id IN (SELECT id FROM games WHERE created_at > :since AND created_at NOT IN (SELECT max (created_at) from games))'
)
.run({ since: sinceTime })
this.#db
.prepare(
'DELETE FROM games WHERE created_at > :since AND created_at NOT IN (SELECT max (created_at) from games);'
)
.run({ since: sinceTime })
this.#db.prepare('PRAGMA foreign_keys = ON;').run()
// })
// deleteEverything()
}

getLastlocs() {
Expand Down Expand Up @@ -882,36 +989,6 @@ class db {
}))
}

/**
* Check if the database contains any data.
*/
isEmpty(): boolean {
const result = this.#db.prepare('SELECT COUNT(*) as count FROM users;').get() as
| { count: number }
| undefined
return !result || result.count === 0
}

async clear() {
if (!this.#db.memory) {
try {
await this.#db.backup(`${this.#db.name}.bak`)
} catch {}
}

const deleteEverything = this.#db.transaction(() => {
// Disable foreign key checking while we delete everything
this.#db.prepare('PRAGMA foreign_keys=0;').run()
this.#db.prepare('DELETE FROM guesses;').run()
this.#db.prepare('DELETE FROM streaks;').run()
this.#db.prepare('DELETE FROM rounds;').run()
this.#db.prepare('DELETE FROM games;').run()
this.#db.prepare('DELETE FROM users;').run()
this.#db.prepare('PRAGMA foreign_keys=1;').run()
})
deleteEverything()
}

getBannedUsers() {
const bannedUsers = this.#db.prepare(`SELECT username FROM banned_users`).all() as {
username: string
Expand Down Expand Up @@ -941,6 +1018,16 @@ class db {
.run({ username: username })
}

/**
* Check if the database contains any data.
*/
isEmpty(): boolean {
const result = this.#db.prepare('SELECT COUNT(*) as count FROM users;').get() as
| { count: number }
| undefined
return !result || result.count === 0
}

/**
* Run a custom SQL query, for use in tests only.
*/
Expand Down

0 comments on commit 9af3772

Please sign in to comment.