Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: automatically award live session achievements for first, second and third rank #3886

Closed
wants to merge 8 commits into from
259 changes: 205 additions & 54 deletions packages/graphql/src/services/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,42 @@ export async function startSession(
}
}

// TODO: update achievement IDs after seeding
const FIRST_ACHIEVEMENT_ID = 11
const SECOND_ACHIEVEMENT_ID = 12
const THIRD_ACHIEVEMENT_ID = 13

interface EndSessionArgs {
id: string
}

export async function endSession({ id }: EndSessionArgs, ctx: ContextWithUser) {
const firstRankAchievement = await ctx.prisma.achievement.findUnique({
where: { id: FIRST_ACHIEVEMENT_ID },
})
const secondRankAchievement = await ctx.prisma.achievement.findUnique({
where: { id: SECOND_ACHIEVEMENT_ID },
})
const thirdRankAchievement = await ctx.prisma.achievement.findUnique({
where: { id: THIRD_ACHIEVEMENT_ID },
})

const achievements = {
1: FIRST_ACHIEVEMENT_ID,
2: SECOND_ACHIEVEMENT_ID,
3: THIRD_ACHIEVEMENT_ID,
}
const achievementRewardsPoints = {
1: firstRankAchievement?.rewardedPoints ?? 0,
2: secondRankAchievement?.rewardedPoints ?? 0,
3: thirdRankAchievement?.rewardedPoints ?? 0,
}
const achievementRewardsXP = {
1: firstRankAchievement?.rewardedXP ?? 0,
2: secondRankAchievement?.rewardedXP ?? 0,
3: thirdRankAchievement?.rewardedXP ?? 0,
}

const session = await ctx.prisma.liveSession.findFirst({
where: {
id,
Expand Down Expand Up @@ -452,21 +483,32 @@ export async function endSession({ id }: EndSessionArgs, ctx: ContextWithUser) {

let promises: any[] = []

const participants: Record<string, any> = {}
const participants: Record<
string,
{
xp?: number
score?: number
}
> = {}

Object.entries(sessionXP).forEach(([id, xp]) => {
participants[id] = {
xp,
xp: parseInt(xp),
}
})

Object.entries(sessionLB).forEach(([id, score]) => {
participants[id] = {
...(participants[id] ?? {}),
score,
score: parseInt(score),
}
})

// TODO: compute achievements here and add to the participants mapping
// TODO: later on, update the achievements within the same transaction as the score
// TODO: what about multiple people with the same points at rank 1? everyone with max points gets rank 1?
// TODO: if the above is implemented: what about sessions with only questions that have no solution? everyone rank 1 and 2 and 3?

console.log(participants)

// sessionXP should always be around as soon as there are logged-in participants (check first)
Expand Down Expand Up @@ -516,81 +558,190 @@ export async function endSession({ id }: EndSessionArgs, ctx: ContextWithUser) {
where: { id },
data: {
xp: {
increment: Number(xp),
increment: xp,
},
},
})
)
)

// if the session is part of a course, update the course leaderboard with the accumulated points
if (sessionLB && session.courseId) {
if (sessionLB && session.courseId && session.isGamificationEnabled) {
const participantsWithScore = existingParticipants
.filter(
({ score, hasParticipation }) =>
typeof score !== 'undefined' && hasParticipation
)
// @ts-ignore needed here, as score cannot be undefined (filter) but is recognized as optional
.sort((a, b) => b.score - a.score)

console.log(participantsWithScore)

promises = promises.concat(
existingParticipants
.filter(
({ score, hasParticipation }) =>
typeof score !== 'undefined' && hasParticipation
)
.map(({ id, score }) =>
ctx.prisma.leaderboardEntry.upsert({
where: {
type_participantId_courseId: {
type: 'COURSE',
courseId: session.courseId!,
participantId: id,
},
participantsWithScore.map(({ id, score }) =>
ctx.prisma.leaderboardEntry.upsert({
where: {
type_participantId_courseId: {
type: 'COURSE',
courseId: session.courseId!,
participantId: id,
},
include: {
participation: true,
participant: true,
},
include: {
participation: true,
participant: true,
},
create: {
type: 'COURSE',
course: {
connect: {
id: session.courseId!,
},
},
create: {
type: 'COURSE',
course: {
connect: {
id: session.courseId!,
},
participant: {
connect: {
id,
},
participant: {
connect: {
id,
},
participation: {
connectOrCreate: {
where: {
courseId_participantId: {
courseId: session.courseId!,
participantId: id,
},
},
},
participation: {
connectOrCreate: {
where: {
courseId_participantId: {
courseId: session.courseId!,
participantId: id,
create: {
course: {
connect: {
id: session.courseId!,
},
},
create: {
course: {
connect: {
id: session.courseId!,
},
},
participant: {
connect: {
id,
},
participant: {
connect: {
id,
},
},
},
},
score: parseInt(score),
},
update: {
score: {
increment: parseInt(score),
},
score,
},
update: {
score: {
increment: score,
},
})
)
},
})
)
)

// TODO: what about race conditions between participant LB updates and achievement LB updates?
// TODO: need to merge the two approaches into one.. cleanly
// const newAchievements = []

// if (existingParticipantsLB[0]) {
// const firstRank = existingParticipantsLB[0]
// newAchievements.push({
// participantId: firstRank.participantId,
// achievementId: achievements[1],
// points: points[1],
// xp: xp[1],
// })

// if (existingParticipantsLB[1]) {
// const secondRank = existingParticipantsLB[1]
// const rank2 = secondRank.score < firstRank.score ? 2 : 1

// newAchievements.push({
// participantId: secondRank.participantId,
// achievementId: achievements[rank2],
// points: points[rank2],
// xp: xp[rank2],
// })

// if (existingParticipantsLB[2]) {
// const thirdRank = existingParticipantsLB[2]
// const rank3 =
// thirdRank.score < secondRank.score
// ? 3
// : thirdRank.score < firstRank.score
// ? 2
// : 1

// newAchievements.push({
// participantId: thirdRank.participantId,
// achievementId: achievements[rank3],
// points: points[rank3],
// xp: xp[rank3],
// })
// }
// }
// }

// // map over newAchievements and update participants in a prisma transaction
// await ctx.prisma.$transaction(
// newAchievements.map(({ participantId, achievementId, points, xp }) =>
// ctx.prisma.participant.update({
// where: {
// id: participantId,
// },
// data: {
// // increment xp
// xp: {
// increment: xp,
// },
// // increment points on course leaderboard, if session is assigned to course
// leaderboards: {
// update: {
// where: {
// type_participantId_courseId: {
// type: 'COURSE',
// courseId: session.courseId!,
// participantId,
// },
// },
// data: {
// score: {
// increment: points,
// },
// },
// },
// },
// // create achievement or increment achievement count
// achievements: {
// upsert: {
// where: {
// participantId_achievementId: {
// participantId,
// achievementId,
// },
// },
// create: {
// achievement: {
// connect: {
// id: achievementId,
// },
// },
// achievedAt: new Date(),
// },
// update: {
// achievedCount: {
// increment: 1,
// },
// },
// },
// },
// },
// })
// )
// )
}
}

// TODO: remove this once stuff is done and should be committed
return null

// execute XP and points in the same transaction to prevent issues when one fails
// the session update later on should never fail, but we need the return value (keep separate)
await ctx.prisma.$transaction(promises)
Expand Down
56 changes: 56 additions & 0 deletions packages/prisma/src/data/seedTEST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,62 @@ async function seedTest(prisma: Prisma.PrismaClient) {
},
})

const goldMedalAchievement = await prisma.achievement.upsert({
where: { id: 11 },
create: {
id: 11,
name: 'Champion',
description: 'Du hast einen ersten Platz in einer Live-Session erreicht.',
icon: '/achievements/Champ.svg',
type: 'PARTICIPANT',
rewardedPoints: 200,
rewardedXP: 100,
},
update: {
icon: '/achievements/Champ.svg',
rewardedPoints: 200,
rewardedXP: 100,
},
})

const silverMedalAchievement = await prisma.achievement.upsert({
where: { id: 12 },
create: {
id: 12,
name: 'Vize-Champion',
description:
'Du hast einen zweiten Platz in einer Live-Session erreicht.',
icon: '/achievements/VizeChamp.svg',
type: 'PARTICIPANT',
rewardedPoints: 100,
rewardedXP: 100,
},
update: {
icon: '/achievements/VizeChamp.svg',
rewardedPoints: 100,
rewardedXP: 100,
},
})

const bronzeMedalAchievement = await prisma.achievement.upsert({
where: { id: 13 },
create: {
id: 13,
name: 'Vize-Vize-Champion',
description:
'Du hast einen dritten Platz in einer Live-Session erreicht.',
icon: '/achievements/VizevizeChamp.svg',
type: 'PARTICIPANT',
rewardedPoints: 50,
rewardedXP: 100,
},
update: {
icon: '/achievements/VizevizeChamp.svg',
rewardedPoints: 50,
rewardedXP: 100,
},
})

const awardedPilotAchievements = PARTICIPANT_IDS.map(
async (participantId) => {
await prisma.participantAchievementInstance.upsert({
Expand Down
Loading