From ec9d3f29819830ca75b4ba72c9e16c348e24ff25 Mon Sep 17 00:00:00 2001
From: Eric Paul
Date: Sun, 2 Mar 2025 17:30:15 +1100
Subject: [PATCH 1/2] chore: fix types
---
src/components/game-screen/CodeRunning.tsx | 105 +-----------------
src/components/game-screen/InProgressGame.tsx | 4 +-
.../game-screen/PictureQuestionResults.tsx | 16 +++
.../ProgrammingQuestionResults.tsx | 91 +++++++++++++++
src/components/game-screen/RunCodePanel.tsx | 7 --
src/lib/games/queries.ts | 2 +
src/lib/games/question-types/base.ts | 2 +
.../games/question-types/picture/client.tsx | 14 ++-
.../question-types/programming/client.tsx | 13 ++-
src/lib/games/types.ts | 6 +
10 files changed, 147 insertions(+), 113 deletions(-)
create mode 100644 src/components/game-screen/PictureQuestionResults.tsx
create mode 100644 src/components/game-screen/ProgrammingQuestionResults.tsx
delete mode 100644 src/components/game-screen/RunCodePanel.tsx
diff --git a/src/components/game-screen/CodeRunning.tsx b/src/components/game-screen/CodeRunning.tsx
index ed9209a..8472b06 100644
--- a/src/components/game-screen/CodeRunning.tsx
+++ b/src/components/game-screen/CodeRunning.tsx
@@ -1,99 +1,12 @@
-import type { ReactNode } from "react"
import React from "react"
-import { CheckCircle2, Loader2, MinusCircleIcon, XCircle } from "lucide-react"
-import type { PlayerGameSession } from "~/lib/games/types"
-import { type ProgrammingQuestionWithTestCases } from "~/lib/games/types"
+import type { ClientQuestionStrategy } from "~/lib/games/question-types/base"
import { api } from "~/lib/trpc/react"
import { Skeleton } from "../ui/skeleton"
import { useGameManager } from "./GameManagerProvider"
-function QuestionTestCaseResults(props: {
- question: ProgrammingQuestionWithTestCases
- testState: PlayerGameSession["testState"]
-}) {
- const testResultByTestCaseId = Object.fromEntries(
- props.testState?.programmingResults.map((result) => [
- `${result.programming_question_test_case_id}`,
- result,
- ]) ?? [],
- )
-
- const correctTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
- const result = testResultByTestCaseId[tc.id]
- return result?.status === "success" && result.is_correct
- })
-
- const incorrectTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
- const result = testResultByTestCaseId[tc.id]
- return result && (result.status === "error" || !result.is_correct)
- })
-
- const unsubmittedTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
- return !testResultByTestCaseId[tc.id]
- })
-
- const sortedTestCases = [...unsubmittedTestCases, ...incorrectTestCases, ...correctTestCases]
-
- return (
-
- {sortedTestCases.map((testCase, i) => {
- const result = props.testState?.programmingResults.find(
- (result) => result.programming_question_test_case_id === testCase.id,
- )
-
- let testEmoji: ReactNode
- if (props.testState?.status === "running") {
- testEmoji = (
-
-
-
- )
- } else if (result) {
- testEmoji = (
-
- {result.status === "success" && result.is_correct ? (
-
- ) : (
-
- )}
-
- )
- } else {
- testEmoji = (
-
-
-
- )
- }
- return (
-
-
- {testEmoji}
-
- solution({testCase.args.map((a) => JSON.stringify(a)).join(", ")}) =={" "}
- {JSON.stringify(testCase.expectedOutput)}
-
-
- {result?.status === "success" && !result.is_correct && (
-
- Output: {JSON.stringify(result.result)}
-
- )}
- {result?.status === "error" && (
-
- {result.reason}
-
- )}
-
- )
- })}
-
- )
-}
-
-export default function CodeRunning() {
- const { gameInfo, gameSessionInfo, submitCodeMutation } = useGameManager()
+export default function CodeRunning(props: { questionStrategy: ClientQuestionStrategy }) {
+ const { gameSessionInfo, submitCodeMutation } = useGameManager()
const submisisonMetricsQuery = api.games.getSubmissionMetrics.useQuery({
game_id: gameSessionInfo.game_id,
})
@@ -107,10 +20,7 @@ export default function CodeRunning() {
-
- Test
-
Cases
-
+
Results
{gameSessionInfo.submission_state_id ? (
<>
@@ -146,12 +56,7 @@ export default function CodeRunning() {
)}
- {gameInfo.question?.programmingQuestion ? (
-
- ) : null}
+ {props.questionStrategy.results(gameSessionInfo.testState)}
)
diff --git a/src/components/game-screen/InProgressGame.tsx b/src/components/game-screen/InProgressGame.tsx
index a127208..0024cc2 100644
--- a/src/components/game-screen/InProgressGame.tsx
+++ b/src/components/game-screen/InProgressGame.tsx
@@ -12,10 +12,10 @@ import { createClientQuestionStrategy } from "~/lib/games/question-types/client_
import { type NotWaitingForPlayersGameState } from "~/lib/games/types"
import { createDefaultLayout, createDefaultMobileLayout } from "~/lib/surfaces/panels/layouts"
import PanelSkeleton from "~/lib/surfaces/panels/PanelSkeleton"
+import CodeRunning from "./CodeRunning"
import CodeRunningFooter from "./CodeRunningFooter"
import { CodeViewImpl } from "./CodeView"
import MultiSelectPanel from "./MultiSelectPanel"
-import RunCodePanel from "./RunCodePanel"
export const MOBILE_VIEWPORT = "(max-width: 640px)"
@@ -37,7 +37,7 @@ function useViews(props: {
const CodeRunningViewImpl = {
key: "run-code",
className: "p-4",
- component: ,
+ component: ,
footer: ,
footerClassName: "px-4 pb-4 pt-2",
}
diff --git a/src/components/game-screen/PictureQuestionResults.tsx b/src/components/game-screen/PictureQuestionResults.tsx
new file mode 100644
index 0000000000..0991ae6
--- /dev/null
+++ b/src/components/game-screen/PictureQuestionResults.tsx
@@ -0,0 +1,16 @@
+import type { PictureQuestion, QuestionTestState } from "~/lib/games/types"
+
+export function PictureQuestionResults(props: {
+ question: PictureQuestion
+ testState: QuestionTestState | null
+}) {
+ return (
+
+ Picture question results coming soon!
+
+ {props.question.pictureQuestion.id}
+
+ Match percentage: {props.testState?.pictureResult.match_percentage}
+
+ )
+}
diff --git a/src/components/game-screen/ProgrammingQuestionResults.tsx b/src/components/game-screen/ProgrammingQuestionResults.tsx
new file mode 100644
index 0000000000..ead914c
--- /dev/null
+++ b/src/components/game-screen/ProgrammingQuestionResults.tsx
@@ -0,0 +1,91 @@
+import type { ReactNode } from "react"
+import React from "react"
+import { CheckCircle2, Loader2, MinusCircleIcon, XCircle } from "lucide-react"
+
+import type { PlayerGameSession } from "~/lib/games/types"
+import { type ProgrammingQuestionWithTestCases } from "~/lib/games/types"
+
+export function ProgrammingQuestionResults(props: {
+ question: ProgrammingQuestionWithTestCases
+ testState: PlayerGameSession["testState"] | undefined
+}) {
+ const testResultByTestCaseId = Object.fromEntries(
+ props.testState?.programmingResults.map((result) => [
+ `${result.programming_question_test_case_id}`,
+ result,
+ ]) ?? [],
+ )
+
+ const correctTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
+ const result = testResultByTestCaseId[tc.id]
+ return result?.status === "success" && result.is_correct
+ })
+
+ const incorrectTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
+ const result = testResultByTestCaseId[tc.id]
+ return result && (result.status === "error" || !result.is_correct)
+ })
+
+ const unsubmittedTestCases = props.question.programmingQuestion.testCases.filter((tc) => {
+ return !testResultByTestCaseId[tc.id]
+ })
+
+ const sortedTestCases = [...unsubmittedTestCases, ...incorrectTestCases, ...correctTestCases]
+
+ console.log("sortedTestCases", sortedTestCases)
+ return (
+
+ {sortedTestCases.map((testCase, i) => {
+ const result = props.testState?.programmingResults.find(
+ (result) => result.programming_question_test_case_id === testCase.id,
+ )
+
+ let testEmoji: ReactNode
+ if (props.testState?.status === "running") {
+ testEmoji = (
+
+
+
+ )
+ } else if (result) {
+ testEmoji = (
+
+ {result.status === "success" && result.is_correct ? (
+
+ ) : (
+
+ )}
+
+ )
+ } else {
+ testEmoji = (
+
+
+
+ )
+ }
+ return (
+
+
+ {testEmoji}
+
+ solution({testCase.args.map((a) => JSON.stringify(a)).join(", ")}) =={" "}
+ {JSON.stringify(testCase.expectedOutput)}
+
+
+ {result?.status === "success" && !result.is_correct && (
+
+ Output: {JSON.stringify(result.result)}
+
+ )}
+ {result?.status === "error" && (
+
+ {result.reason}
+
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/src/components/game-screen/RunCodePanel.tsx b/src/components/game-screen/RunCodePanel.tsx
deleted file mode 100644
index 0095f52..0000000000
--- a/src/components/game-screen/RunCodePanel.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from "react"
-
-import CodeRunning from "./CodeRunning"
-
-export default function RunCodePanel() {
- return
-}
diff --git a/src/lib/games/queries.ts b/src/lib/games/queries.ts
index fbfc8d9..7d1de69 100644
--- a/src/lib/games/queries.ts
+++ b/src/lib/games/queries.ts
@@ -278,6 +278,7 @@ export async function getSessionInfoForPlayer(tx: DBOrTransaction, userId: strin
with: {
submissionState: {
with: {
+ pictureResult: true,
programmingResults: {
columns: {
is_correct: true,
@@ -287,6 +288,7 @@ export async function getSessionInfoForPlayer(tx: DBOrTransaction, userId: strin
},
testState: {
with: {
+ pictureResult: true,
programmingResults: true,
},
},
diff --git a/src/lib/games/question-types/base.ts b/src/lib/games/question-types/base.ts
index 6fd4fed..a5a54b2 100644
--- a/src/lib/games/question-types/base.ts
+++ b/src/lib/games/question-types/base.ts
@@ -1,6 +1,7 @@
import { invariant } from "@epic-web/invariant"
import type { GameMode, QuestionDifficultyLevels, QuestionType } from "../constants"
+import type { QuestionTestState } from "../types"
import { entries } from "~/lib/utils/object"
import { randomElement } from "~/lib/utils/random"
import { GAME_MODE_DETAILS } from "../constants"
@@ -14,6 +15,7 @@ export interface ClientQuestionStrategy extends BaseQuestionStrategy {
readonly title: string
readonly description: string
readonly preview: JSX.Element
+ results: (testState: QuestionTestState | null) => JSX.Element
}
export abstract class QuestionTypeConfig {
diff --git a/src/lib/games/question-types/picture/client.tsx b/src/lib/games/question-types/picture/client.tsx
index b1987a4..1f8dfc9 100644
--- a/src/lib/games/question-types/picture/client.tsx
+++ b/src/lib/games/question-types/picture/client.tsx
@@ -1,10 +1,16 @@
import { invariant } from "@epic-web/invariant"
-import { type FullQuestion } from "../../types"
+import type { FullQuestion } from "../../types"
+import type { ClientQuestionStrategy } from "../base"
+import { PictureQuestionResults } from "~/components/game-screen/PictureQuestionResults"
+import { type QuestionTestState } from "../../types"
import { BaseQuestionStrategy } from "../base"
import { PictureQuestionConfig } from "./config"
-export class PictureQuestionStrategy extends BaseQuestionStrategy {
+export class PictureQuestionStrategy
+ extends BaseQuestionStrategy
+ implements ClientQuestionStrategy
+{
private readonly question: FullQuestion & {
pictureQuestion: NonNullable
}
@@ -26,4 +32,8 @@ export class PictureQuestionStrategy extends BaseQuestionStrategy {
get preview() {
return Picture question
}
+
+ results(testState: QuestionTestState | null) {
+ return
+ }
}
diff --git a/src/lib/games/question-types/programming/client.tsx b/src/lib/games/question-types/programming/client.tsx
index 99bd18b..b685070 100644
--- a/src/lib/games/question-types/programming/client.tsx
+++ b/src/lib/games/question-types/programming/client.tsx
@@ -1,11 +1,16 @@
import { invariant } from "@epic-web/invariant"
import { TestTubeDiagonal } from "lucide-react"
-import { type FullQuestion } from "../../types"
+import type { FullQuestion, QuestionTestState } from "../../types"
+import type { ClientQuestionStrategy } from "../base"
+import { ProgrammingQuestionResults } from "~/components/game-screen/ProgrammingQuestionResults"
import { BaseQuestionStrategy } from "../base"
import { ProgrammingQuestionConfig } from "./config"
-export class ProgrammingQuestionStrategy extends BaseQuestionStrategy {
+export class ProgrammingQuestionStrategy
+ extends BaseQuestionStrategy
+ implements ClientQuestionStrategy
+{
private readonly question: FullQuestion & {
programmingQuestion: NonNullable
}
@@ -52,4 +57,8 @@ export class ProgrammingQuestionStrategy extends BaseQuestionStrategy {
)
}
+
+ results(testState: QuestionTestState | null) {
+ return
+ }
}
diff --git a/src/lib/games/types.ts b/src/lib/games/types.ts
index 8641d0d..280c11c 100644
--- a/src/lib/games/types.ts
+++ b/src/lib/games/types.ts
@@ -41,6 +41,12 @@ export type ProgrammingQuestionWithTestCases = FullGameState["question"] & {
testCases: Doc<"programmingQuestionTestCases">[]
}
}
+export type PictureQuestion = FullGameState["question"] & {
+ pictureQuestion: Doc<"pictureQuestions">
+}
+export type QuestionTestState = NonNullable<
+ NonNullable>>["testState"]
+>
export type PlayerGameSession = NonNullable>>
export type FinalPlayerResult = Pick, "position" | "score">
From 6ec68ca92ec5c0136e61443f052f4ab944a6a529 Mon Sep 17 00:00:00 2001
From: Eric Paul
Date: Sun, 16 Mar 2025 15:06:23 +1100
Subject: [PATCH 2/2] chore: add submission metrics to api
---
.../ProgrammingSubmissionStatus.tsx | 44 +++++++++++++++++++
src/lib/games/question-types/base.ts | 3 +-
.../games/question-types/picture/client.tsx | 10 ++++-
.../question-types/programming/client.tsx | 10 ++++-
src/lib/games/types.ts | 14 ++++++
5 files changed, 78 insertions(+), 3 deletions(-)
create mode 100644 src/components/game-screen/ProgrammingSubmissionStatus.tsx
diff --git a/src/components/game-screen/ProgrammingSubmissionStatus.tsx b/src/components/game-screen/ProgrammingSubmissionStatus.tsx
new file mode 100644
index 0000000000..3fd7ce4
--- /dev/null
+++ b/src/components/game-screen/ProgrammingSubmissionStatus.tsx
@@ -0,0 +1,44 @@
+import type { ProgrammingSubmissionState } from "~/lib/games/types"
+import { Skeleton } from "../ui/skeleton"
+
+export function ProgrammingSubmissionStatus(props: {
+ submissionState: ProgrammingSubmissionState
+}) {
+ const hasMetricsToShow = true
+ return (
+
+ {props.submissionState.submission_state_id ? (
+ <>
+ Last submssion
+
+
+ {hasMetricsToShow ? (
+ `${props.submissionState.metrics.numPassingSubmissionsTestCases}/${props.submissionState.metrics.numTestCases} `
+ ) : (
+
+ )}
+ testcases passed
+
+ {hasMetricsToShow ? (
+
+ ) : (
+
+ )}
+ >
+ ) : (
+ <>
+ No submissions
+
+ recorded yet
+ >
+ )}
+
+ )
+}
diff --git a/src/lib/games/question-types/base.ts b/src/lib/games/question-types/base.ts
index a5a54b2..08e1fe4 100644
--- a/src/lib/games/question-types/base.ts
+++ b/src/lib/games/question-types/base.ts
@@ -1,7 +1,7 @@
import { invariant } from "@epic-web/invariant"
import type { GameMode, QuestionDifficultyLevels, QuestionType } from "../constants"
-import type { QuestionTestState } from "../types"
+import type { QuestionTestState, SubmissionState } from "../types"
import { entries } from "~/lib/utils/object"
import { randomElement } from "~/lib/utils/random"
import { GAME_MODE_DETAILS } from "../constants"
@@ -16,6 +16,7 @@ export interface ClientQuestionStrategy extends BaseQuestionStrategy {
readonly description: string
readonly preview: JSX.Element
results: (testState: QuestionTestState | null) => JSX.Element
+ submissionMetrics: (submissionState: SubmissionState) => JSX.Element
}
export abstract class QuestionTypeConfig {
diff --git a/src/lib/games/question-types/picture/client.tsx b/src/lib/games/question-types/picture/client.tsx
index 1f8dfc9..1c6c6cc 100644
--- a/src/lib/games/question-types/picture/client.tsx
+++ b/src/lib/games/question-types/picture/client.tsx
@@ -1,6 +1,6 @@
import { invariant } from "@epic-web/invariant"
-import type { FullQuestion } from "../../types"
+import type { FullQuestion, SubmissionState } from "../../types"
import type { ClientQuestionStrategy } from "../base"
import { PictureQuestionResults } from "~/components/game-screen/PictureQuestionResults"
import { type QuestionTestState } from "../../types"
@@ -36,4 +36,12 @@ export class PictureQuestionStrategy
results(testState: QuestionTestState | null) {
return
}
+
+ submissionMetrics(submissionState: SubmissionState) {
+ if (submissionState.type === "picture") {
+ throw new Error("Picture submission metrics not implemented")
+ return <>>
+ }
+ throw new Error("Picture submission state not found")
+ }
}
diff --git a/src/lib/games/question-types/programming/client.tsx b/src/lib/games/question-types/programming/client.tsx
index b685070..1d5eb3c 100644
--- a/src/lib/games/question-types/programming/client.tsx
+++ b/src/lib/games/question-types/programming/client.tsx
@@ -1,9 +1,10 @@
import { invariant } from "@epic-web/invariant"
import { TestTubeDiagonal } from "lucide-react"
-import type { FullQuestion, QuestionTestState } from "../../types"
+import type { FullQuestion, QuestionTestState, SubmissionState } from "../../types"
import type { ClientQuestionStrategy } from "../base"
import { ProgrammingQuestionResults } from "~/components/game-screen/ProgrammingQuestionResults"
+import { ProgrammingSubmissionStatus } from "~/components/game-screen/ProgrammingSubmissionStatus"
import { BaseQuestionStrategy } from "../base"
import { ProgrammingQuestionConfig } from "./config"
@@ -61,4 +62,11 @@ export class ProgrammingQuestionStrategy
results(testState: QuestionTestState | null) {
return
}
+
+ submissionMetrics(submissionState: SubmissionState) {
+ if (submissionState.type === "programming") {
+ return
+ }
+ throw new Error("Programming submission state not found")
+ }
}
diff --git a/src/lib/games/types.ts b/src/lib/games/types.ts
index 280c11c..c023845 100644
--- a/src/lib/games/types.ts
+++ b/src/lib/games/types.ts
@@ -61,3 +61,17 @@ export type ClientGameState = {
gameState: InGameState
gameSession: PlayerGameSession
}
+
+export type ProgrammingSubmissionState = {
+ type: "programming"
+ submission_state_id: string
+ metrics: { numPassingSubmissionsTestCases: number; numTestCases: number }
+}
+
+type PictureSubmissionState = {
+ type: "picture"
+ submission_state_id: string
+ metrics: { match_percentage: number }
+}
+
+export type SubmissionState = ProgrammingSubmissionState | PictureSubmissionState