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