@quizpot/quizcore 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +582 -0
- package/dist/index.d.cts +472 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +472 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +522 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -6
- package/dist/events/client/host/kick-player.d.ts +0 -8
- package/dist/events/client/host/kick-player.d.ts.map +0 -1
- package/dist/events/client/host/kick-player.js +0 -4
- package/dist/events/client/host/next-step.d.ts +0 -5
- package/dist/events/client/host/next-step.d.ts.map +0 -1
- package/dist/events/client/host/next-step.js +0 -3
- package/dist/events/client/host/start-lobby.d.ts +0 -6
- package/dist/events/client/host/start-lobby.d.ts.map +0 -1
- package/dist/events/client/host/start-lobby.js +0 -4
- package/dist/events/client/player/submit-answer.d.ts +0 -9
- package/dist/events/client/player/submit-answer.d.ts.map +0 -1
- package/dist/events/client/player/submit-answer.js +0 -4
- package/dist/events/server/lobby-deleted.d.ts +0 -8
- package/dist/events/server/lobby-deleted.d.ts.map +0 -1
- package/dist/events/server/lobby-deleted.js +0 -4
- package/dist/events/server/lobby-joined.d.ts +0 -14
- package/dist/events/server/lobby-joined.d.ts.map +0 -1
- package/dist/events/server/lobby-joined.js +0 -18
- package/dist/events/server/lobby-status-update.d.ts +0 -32
- package/dist/events/server/lobby-status-update.d.ts.map +0 -1
- package/dist/events/server/lobby-status-update.js +0 -4
- package/dist/events/server/player-answer-result.d.ts +0 -11
- package/dist/events/server/player-answer-result.d.ts.map +0 -1
- package/dist/events/server/player-answer-result.js +0 -4
- package/dist/events/server/player-joined.d.ts +0 -9
- package/dist/events/server/player-joined.d.ts.map +0 -1
- package/dist/events/server/player-joined.js +0 -4
- package/dist/events/server/player-kicked.d.ts +0 -6
- package/dist/events/server/player-kicked.d.ts.map +0 -1
- package/dist/events/server/player-kicked.js +0 -4
- package/dist/events/server/player-left.d.ts +0 -9
- package/dist/events/server/player-left.d.ts.map +0 -1
- package/dist/events/server/player-left.js +0 -4
- package/dist/events/server/player-update.d.ts +0 -9
- package/dist/events/server/player-update.d.ts.map +0 -1
- package/dist/events/server/player-update.js +0 -4
- package/dist/events/server/update-lobby-answers.d.ts +0 -8
- package/dist/events/server/update-lobby-answers.d.ts.map +0 -1
- package/dist/events/server/update-lobby-answers.js +0 -4
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -25
- package/dist/managers/lobby-manager.d.ts +0 -12
- package/dist/managers/lobby-manager.d.ts.map +0 -1
- package/dist/managers/lobby-manager.js +0 -112
- package/dist/types/events.d.ts +0 -16
- package/dist/types/events.d.ts.map +0 -1
- package/dist/types/events.js +0 -1
- package/dist/types/lobby.d.ts +0 -42
- package/dist/types/lobby.d.ts.map +0 -1
- package/dist/types/lobby.js +0 -10
- package/dist/types/question.d.ts +0 -106
- package/dist/types/question.d.ts.map +0 -1
- package/dist/types/question.js +0 -22
- package/dist/types/questions/multiple-choice.d.ts +0 -51
- package/dist/types/questions/multiple-choice.d.ts.map +0 -1
- package/dist/types/questions/multiple-choice.js +0 -21
- package/dist/types/questions/short-answer.d.ts +0 -34
- package/dist/types/questions/short-answer.d.ts.map +0 -1
- package/dist/types/questions/short-answer.js +0 -11
- package/dist/types/questions/true-false.d.ts +0 -36
- package/dist/types/questions/true-false.d.ts.map +0 -1
- package/dist/types/questions/true-false.js +0 -12
- package/dist/types/quizfile.d.ts +0 -79
- package/dist/types/quizfile.d.ts.map +0 -1
- package/dist/types/quizfile.js +0 -23
- package/dist/types/quizstep.d.ts +0 -73
- package/dist/types/quizstep.d.ts.map +0 -1
- package/dist/types/quizstep.js +0 -7
- package/dist/types/quiztheme.d.ts +0 -7
- package/dist/types/quiztheme.d.ts.map +0 -1
- package/dist/types/quiztheme.js +0 -5
- package/dist/types/slide.d.ts +0 -18
- package/dist/types/slide.d.ts.map +0 -1
- package/dist/types/slide.js +0 -9
- package/dist/types/slides/comparison.d.ts +0 -9
- package/dist/types/slides/comparison.d.ts.map +0 -1
- package/dist/types/slides/comparison.js +0 -7
- package/dist/types/slides/titleImageTextSlide.d.ts +0 -9
- package/dist/types/slides/titleImageTextSlide.d.ts.map +0 -1
- package/dist/types/slides/titleImageTextSlide.js +0 -7
- package/dist/types/slides/titleSlide.d.ts +0 -8
- package/dist/types/slides/titleSlide.d.ts.map +0 -1
- package/dist/types/slides/titleSlide.js +0 -6
- package/dist/util/guards.d.ts +0 -18
- package/dist/util/guards.d.ts.map +0 -1
- package/dist/util/guards.js +0 -15
- package/dist/util/names/additives.json +0 -23
- package/dist/util/names/animals.json +0 -23
- package/dist/util/names/colors.json +0 -14
- package/dist/util/names/names.d.ts +0 -4
- package/dist/util/names/names.d.ts.map +0 -1
- package/dist/util/names/names.js +0 -31
- package/dist/util/sanitizer.d.ts +0 -3
- package/dist/util/sanitizer.d.ts.map +0 -1
- package/dist/util/sanitizer.js +0 -18
- package/dist/util/score.d.ts +0 -11
- package/dist/util/score.d.ts.map +0 -1
- package/dist/util/score.js +0 -33
- package/dist/util/validator.d.ts +0 -14
- package/dist/util/validator.d.ts.map +0 -1
- package/dist/util/validator.js +0 -27
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let zod = require("zod");
|
|
25
|
+
zod = __toESM(zod);
|
|
26
|
+
//#region src/events/client/host/kick-player.ts
|
|
27
|
+
const createKickPlayerEvent = (playerId) => ({
|
|
28
|
+
type: "KICK_PLAYER",
|
|
29
|
+
payload: { playerId }
|
|
30
|
+
});
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/events/client/host/next-step.ts
|
|
33
|
+
const createNextStepEvent = () => ({ type: "NEXT_STEP" });
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/events/client/host/start-lobby.ts
|
|
36
|
+
const createStartLobbyEvent = () => ({
|
|
37
|
+
type: "START_LOBBY",
|
|
38
|
+
payload: {}
|
|
39
|
+
});
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/events/client/player/submit-answer.ts
|
|
42
|
+
const createSubmitAnswerEvent = (submission) => ({
|
|
43
|
+
type: "SUBMIT_ANSWER",
|
|
44
|
+
payload: { submission }
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/events/server/lobby-deleted.ts
|
|
48
|
+
const createLobbyDeletedEvent = (reason) => ({
|
|
49
|
+
type: "LOBBY_DELETED",
|
|
50
|
+
payload: { reason }
|
|
51
|
+
});
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/events/server/lobby-joined.ts
|
|
54
|
+
const createLobbyJoinedEvent = (lobby, me, isHost) => ({
|
|
55
|
+
type: "LOBBY_JOINED",
|
|
56
|
+
payload: {
|
|
57
|
+
lobby: {
|
|
58
|
+
code: lobby.code,
|
|
59
|
+
quizInfo: lobby.quizInfo,
|
|
60
|
+
status: lobby.status,
|
|
61
|
+
timeoutStartedAt: lobby.timeoutStartedAt,
|
|
62
|
+
duration: lobby.duration,
|
|
63
|
+
currentStep: lobby.currentStep,
|
|
64
|
+
settings: lobby.settings
|
|
65
|
+
},
|
|
66
|
+
me,
|
|
67
|
+
players: isHost ? lobby.players : void 0,
|
|
68
|
+
currentAnswers: isHost ? lobby.currentAnswers : void 0,
|
|
69
|
+
answers: isHost ? lobby.answers : void 0
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/events/server/lobby-status-update.ts
|
|
74
|
+
const createLobbyStatusUpdateEvent = (payload) => ({
|
|
75
|
+
type: "LOBBY_STATUS_UPDATE",
|
|
76
|
+
payload
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/events/server/player-answer-result.ts
|
|
80
|
+
const createPlayerAnswerResultEvent = (isCorrect, pointsAwarded, score, streak) => ({
|
|
81
|
+
type: "PLAYER_ANSWER_RESULT",
|
|
82
|
+
payload: {
|
|
83
|
+
isCorrect,
|
|
84
|
+
pointsAwarded,
|
|
85
|
+
score,
|
|
86
|
+
streak
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/events/server/player-joined.ts
|
|
91
|
+
const createPlayerJoinedEvent = (player) => ({
|
|
92
|
+
type: "PLAYER_JOINED",
|
|
93
|
+
payload: { player }
|
|
94
|
+
});
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/events/server/player-kicked.ts
|
|
97
|
+
const createPlayerKickedEvent = () => ({
|
|
98
|
+
type: "PLAYER_KICKED",
|
|
99
|
+
payload: {}
|
|
100
|
+
});
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/events/server/player-left.ts
|
|
103
|
+
const createPlayerLeftEvent = (player) => ({
|
|
104
|
+
type: "PLAYER_LEFT",
|
|
105
|
+
payload: { player }
|
|
106
|
+
});
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/events/server/player-update.ts
|
|
109
|
+
const createPlayerUpdateEvent = (player) => ({
|
|
110
|
+
type: "PLAYER_UPDATE",
|
|
111
|
+
payload: { player }
|
|
112
|
+
});
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/events/server/update-lobby-answers.ts
|
|
115
|
+
const createUpdateLobbyAnswersEvent = (count) => ({
|
|
116
|
+
type: "UPDATE_LOBBY_ANSWERS",
|
|
117
|
+
payload: { count }
|
|
118
|
+
});
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/types/lobby.ts
|
|
121
|
+
let LobbyStatus = /* @__PURE__ */ function(LobbyStatus) {
|
|
122
|
+
LobbyStatus["waiting"] = "waiting";
|
|
123
|
+
LobbyStatus["slide"] = "slide";
|
|
124
|
+
LobbyStatus["question"] = "question";
|
|
125
|
+
LobbyStatus["answer"] = "answer";
|
|
126
|
+
LobbyStatus["answers"] = "answers";
|
|
127
|
+
LobbyStatus["score"] = "score";
|
|
128
|
+
LobbyStatus["end"] = "end";
|
|
129
|
+
return LobbyStatus;
|
|
130
|
+
}({});
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/util/guards.ts
|
|
133
|
+
const isQuestion = (step) => {
|
|
134
|
+
return step.type === "question";
|
|
135
|
+
};
|
|
136
|
+
const isSlide = (step) => {
|
|
137
|
+
return step.type === "slide";
|
|
138
|
+
};
|
|
139
|
+
const isMultipleChoice = (data) => {
|
|
140
|
+
return data.type === "multiple-choice";
|
|
141
|
+
};
|
|
142
|
+
const isTrueFalse = (data) => {
|
|
143
|
+
return data.type === "true-false";
|
|
144
|
+
};
|
|
145
|
+
const isShortAnswer = (data) => {
|
|
146
|
+
return data.type === "short-answer";
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/util/score.ts
|
|
150
|
+
const BASE_SCORE = 500;
|
|
151
|
+
const TIME_BONUS_MAX = 500;
|
|
152
|
+
const calculateScore = (player, question, answer, quiz) => {
|
|
153
|
+
if (!answer.isCorrect) return {
|
|
154
|
+
newScore: player.score,
|
|
155
|
+
pointsAwarded: 0
|
|
156
|
+
};
|
|
157
|
+
const pointMultiplier = {
|
|
158
|
+
noPoints: 0,
|
|
159
|
+
normalPoints: 1,
|
|
160
|
+
doublePoints: 2
|
|
161
|
+
}[question.points] ?? 1;
|
|
162
|
+
if (pointMultiplier === 0) return {
|
|
163
|
+
newScore: player.score,
|
|
164
|
+
pointsAwarded: 0
|
|
165
|
+
};
|
|
166
|
+
let timeBonus = 0;
|
|
167
|
+
if (question.timeLimit > 0) {
|
|
168
|
+
const timeLimitMs = question.timeLimit * 1e3;
|
|
169
|
+
timeBonus = TIME_BONUS_MAX * (1 - Math.max(0, Math.min(answer.timeTaken, timeLimitMs)) / timeLimitMs);
|
|
170
|
+
}
|
|
171
|
+
let questionScore = (BASE_SCORE + timeBonus) * pointMultiplier;
|
|
172
|
+
if (player.streak >= 2) {
|
|
173
|
+
const totalQuestions = quiz.steps.filter((s) => s.type === "question").length;
|
|
174
|
+
const dynamicCap = Math.min(1.2 + Math.max(0, totalQuestions - 5) * .02, 1.5);
|
|
175
|
+
const streakMultiplier = Math.min(1 + (player.streak - 1) * .05, dynamicCap);
|
|
176
|
+
questionScore *= streakMultiplier;
|
|
177
|
+
}
|
|
178
|
+
const finalPoints = Math.trunc(questionScore);
|
|
179
|
+
return {
|
|
180
|
+
newScore: player.score + finalPoints,
|
|
181
|
+
pointsAwarded: finalPoints
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/util/validator.ts
|
|
186
|
+
const isCorrect = (question, submission) => {
|
|
187
|
+
if (isMultipleChoice(question) && submission.type === "multiple-choice") {
|
|
188
|
+
if (question.matchAll) {
|
|
189
|
+
const correctIndices = question.choices.map((c, i) => c.correct ? i : -1).filter((i) => i !== -1);
|
|
190
|
+
return submission.choices.length === correctIndices.length && submission.choices.every((index) => correctIndices.includes(index));
|
|
191
|
+
}
|
|
192
|
+
if (submission.choices.length === 0) return false;
|
|
193
|
+
return submission.choices.every((index) => {
|
|
194
|
+
const choice = question.choices[index];
|
|
195
|
+
return choice ? choice.correct : false;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (isTrueFalse(question) && submission.type === "true-false") return question.answer === submission.answer;
|
|
199
|
+
if (isShortAnswer(question) && submission.type === "short-answer") {
|
|
200
|
+
const playerAns = submission.answer.trim().toLowerCase();
|
|
201
|
+
return question.answers.some((ans) => ans.trim().toLowerCase() === playerAns);
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/managers/lobby-manager.ts
|
|
207
|
+
const createLobby = (code, hostId, quiz) => ({
|
|
208
|
+
code,
|
|
209
|
+
host: hostId,
|
|
210
|
+
quiz,
|
|
211
|
+
quizInfo: {
|
|
212
|
+
title: quiz.title,
|
|
213
|
+
stepCount: quiz.steps.length,
|
|
214
|
+
theme: quiz.theme
|
|
215
|
+
},
|
|
216
|
+
players: [],
|
|
217
|
+
status: LobbyStatus.waiting,
|
|
218
|
+
currentStep: 0,
|
|
219
|
+
timeoutStartedAt: null,
|
|
220
|
+
duration: null,
|
|
221
|
+
currentAnswers: [],
|
|
222
|
+
answers: [],
|
|
223
|
+
settings: {
|
|
224
|
+
customNames: true,
|
|
225
|
+
displayOnDevice: true
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const advanceLobby = (lobby) => {
|
|
229
|
+
const isLastStep = lobby.currentStep >= lobby.quiz.steps.length - 1;
|
|
230
|
+
switch (lobby.status) {
|
|
231
|
+
case LobbyStatus.waiting: return prepareStep(lobby, 0);
|
|
232
|
+
case LobbyStatus.slide:
|
|
233
|
+
case LobbyStatus.score: return isLastStep ? {
|
|
234
|
+
...lobby,
|
|
235
|
+
status: LobbyStatus.end
|
|
236
|
+
} : prepareStep(lobby, lobby.currentStep + 1);
|
|
237
|
+
case LobbyStatus.question: {
|
|
238
|
+
const step = lobby.quiz.steps[lobby.currentStep];
|
|
239
|
+
if (step.type !== "question") return lobby;
|
|
240
|
+
return {
|
|
241
|
+
...lobby,
|
|
242
|
+
status: LobbyStatus.answer,
|
|
243
|
+
timeoutStartedAt: Date.now(),
|
|
244
|
+
duration: step.data.timeLimit * 1e3,
|
|
245
|
+
currentAnswers: []
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
case LobbyStatus.answer: return {
|
|
249
|
+
...lobby,
|
|
250
|
+
status: LobbyStatus.answers
|
|
251
|
+
};
|
|
252
|
+
case LobbyStatus.answers: return {
|
|
253
|
+
...lobby,
|
|
254
|
+
status: LobbyStatus.score
|
|
255
|
+
};
|
|
256
|
+
default: return lobby;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const prepareStep = (lobby, index) => {
|
|
260
|
+
const step = lobby.quiz.steps[index];
|
|
261
|
+
if (!step) return lobby;
|
|
262
|
+
if (step.type === "slide") return {
|
|
263
|
+
...lobby,
|
|
264
|
+
status: LobbyStatus.slide,
|
|
265
|
+
currentStep: index
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
...lobby,
|
|
269
|
+
status: LobbyStatus.question,
|
|
270
|
+
currentStep: index,
|
|
271
|
+
timeoutStartedAt: null,
|
|
272
|
+
duration: null,
|
|
273
|
+
currentAnswers: []
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const handleSubmission = (lobby, playerId, submission) => {
|
|
277
|
+
if (lobby.status !== LobbyStatus.answer) return null;
|
|
278
|
+
const step = lobby.quiz.steps[lobby.currentStep];
|
|
279
|
+
if (!isQuestion(step)) return null;
|
|
280
|
+
const player = lobby.players.find((p) => p.id === playerId);
|
|
281
|
+
if (!player) return null;
|
|
282
|
+
if (lobby.currentAnswers.some((a) => a.playerId === playerId)) return null;
|
|
283
|
+
const correct = isCorrect(step.data, submission);
|
|
284
|
+
const answerObj = {
|
|
285
|
+
playerId,
|
|
286
|
+
submission,
|
|
287
|
+
timeTaken: Date.now() - (lobby.timeoutStartedAt ?? 0),
|
|
288
|
+
isCorrect: correct,
|
|
289
|
+
pointsAwarded: 0
|
|
290
|
+
};
|
|
291
|
+
const { newScore, pointsAwarded } = calculateScore(player, step.data, answerObj, lobby.quiz);
|
|
292
|
+
answerObj.pointsAwarded = pointsAwarded;
|
|
293
|
+
return {
|
|
294
|
+
nextLobby: {
|
|
295
|
+
...lobby,
|
|
296
|
+
players: lobby.players.map((p) => p.id === playerId ? {
|
|
297
|
+
...p,
|
|
298
|
+
score: newScore,
|
|
299
|
+
streak: correct ? p.streak + 1 : 0
|
|
300
|
+
} : p),
|
|
301
|
+
currentAnswers: [...lobby.currentAnswers, answerObj]
|
|
302
|
+
},
|
|
303
|
+
result: {
|
|
304
|
+
type: "PLAYER_ANSWER_RESULT",
|
|
305
|
+
payload: {
|
|
306
|
+
isCorrect: correct,
|
|
307
|
+
pointsAwarded,
|
|
308
|
+
score: newScore,
|
|
309
|
+
streak: correct ? player.streak + 1 : 0
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/types/questions/true-false.ts
|
|
316
|
+
const TrueFalseQuestionSchema = BaseQuestionSchema.extend({
|
|
317
|
+
type: zod.default.literal("true-false"),
|
|
318
|
+
answer: zod.default.boolean(),
|
|
319
|
+
labels: zod.default.array(zod.default.string()).min(2).max(2)
|
|
320
|
+
});
|
|
321
|
+
const SafeTrueFalseQuestionSchema = TrueFalseQuestionSchema.omit({ answer: true });
|
|
322
|
+
const TrueFalseQuestionAnswerSchema = zod.default.object({
|
|
323
|
+
type: zod.default.literal("true-false"),
|
|
324
|
+
answer: zod.default.boolean()
|
|
325
|
+
});
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/types/questions/short-answer.ts
|
|
328
|
+
const ShortAnswerQuestionSchema = BaseQuestionSchema.extend({
|
|
329
|
+
type: zod.default.literal("short-answer"),
|
|
330
|
+
answers: zod.default.array(zod.default.string()).min(1)
|
|
331
|
+
});
|
|
332
|
+
const SafeShortAnswerQuestionSchema = ShortAnswerQuestionSchema.omit({ answers: true });
|
|
333
|
+
const ShortAnswerQuestionAnswerSchema = zod.default.object({
|
|
334
|
+
type: zod.default.literal("short-answer"),
|
|
335
|
+
answer: zod.default.string()
|
|
336
|
+
});
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/types/question.ts
|
|
339
|
+
const QuestionSchema = zod.default.discriminatedUnion("type", [
|
|
340
|
+
MultipleChoiceQuestionSchema,
|
|
341
|
+
TrueFalseQuestionSchema,
|
|
342
|
+
ShortAnswerQuestionSchema
|
|
343
|
+
]);
|
|
344
|
+
zod.default.discriminatedUnion("type", [
|
|
345
|
+
SafeMultipleChoiceQuestionSchema,
|
|
346
|
+
SafeTrueFalseQuestionSchema,
|
|
347
|
+
SafeShortAnswerQuestionSchema
|
|
348
|
+
]);
|
|
349
|
+
const QuestionPointsSchema = zod.default.enum([
|
|
350
|
+
"normalPoints",
|
|
351
|
+
"doublePoints",
|
|
352
|
+
"noPoints"
|
|
353
|
+
]);
|
|
354
|
+
const BaseQuestionSchema = zod.default.object({
|
|
355
|
+
question: zod.default.string().min(1),
|
|
356
|
+
imageHash: zod.default.hash("sha256", { error: "Invalid image hash" }).optional(),
|
|
357
|
+
displayTime: zod.default.number().min(1).max(60),
|
|
358
|
+
timeLimit: zod.default.number().min(1).max(180),
|
|
359
|
+
points: QuestionPointsSchema
|
|
360
|
+
});
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/types/questions/multiple-choice.ts
|
|
363
|
+
const ChoiceSchema = zod.default.object({
|
|
364
|
+
text: zod.default.string(),
|
|
365
|
+
correct: zod.default.boolean()
|
|
366
|
+
});
|
|
367
|
+
const SafeChoiceSchema = ChoiceSchema.omit({ correct: true });
|
|
368
|
+
const MultipleChoiceQuestionSchema = BaseQuestionSchema.extend({
|
|
369
|
+
type: zod.default.literal("multiple-choice"),
|
|
370
|
+
choices: zod.default.array(ChoiceSchema).min(2),
|
|
371
|
+
matchAll: zod.default.boolean()
|
|
372
|
+
});
|
|
373
|
+
const SafeMultipleChoiceQuestionSchema = MultipleChoiceQuestionSchema.omit({ choices: true }).extend({ choices: zod.default.array(SafeChoiceSchema).min(2) });
|
|
374
|
+
const MultipleChoiceQuestionAnswerSchema = zod.default.object({
|
|
375
|
+
type: zod.default.literal("multiple-choice"),
|
|
376
|
+
choices: zod.default.array(zod.default.number()).min(1)
|
|
377
|
+
});
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/types/quiztheme.ts
|
|
380
|
+
const QuizThemeSchema = zod.default.object({
|
|
381
|
+
color: zod.default.string().regex(/^#[0-9a-fA-F]{6}$/, { message: "Invalid color format. Must be a 7-character hex code (e.g., #RRGGBB)." }),
|
|
382
|
+
background: zod.default.hash("sha256", { error: "Invalid background hash" }).optional()
|
|
383
|
+
});
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/types/slides/titleSlide.ts
|
|
386
|
+
const TitleSlideLayoutSchema = zod.default.object({
|
|
387
|
+
slideType: zod.default.literal("title"),
|
|
388
|
+
title: zod.default.string(),
|
|
389
|
+
subtitle: zod.default.string().optional()
|
|
390
|
+
});
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/types/slides/titleImageTextSlide.ts
|
|
393
|
+
const TitleImageTextSlideLayoutSchema = zod.default.object({
|
|
394
|
+
slideType: zod.default.literal("titleImageText"),
|
|
395
|
+
title: zod.default.string(),
|
|
396
|
+
imageHash: zod.default.hash("sha256", { error: "Invalid image hash" }).optional(),
|
|
397
|
+
text: zod.default.string()
|
|
398
|
+
});
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/types/slides/comparison.ts
|
|
401
|
+
const ComparisonSlideLayoutSchema = zod.default.object({
|
|
402
|
+
slideType: zod.default.literal("comparison"),
|
|
403
|
+
title: zod.default.string(),
|
|
404
|
+
left: zod.default.string(),
|
|
405
|
+
right: zod.default.string()
|
|
406
|
+
});
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/types/slide.ts
|
|
409
|
+
const SlideSchema = zod.default.discriminatedUnion("slideType", [
|
|
410
|
+
TitleSlideLayoutSchema,
|
|
411
|
+
TitleImageTextSlideLayoutSchema,
|
|
412
|
+
ComparisonSlideLayoutSchema
|
|
413
|
+
]);
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/types/quizstep.ts
|
|
416
|
+
const QuizStepSchema = zod.default.discriminatedUnion("type", [zod.default.object({
|
|
417
|
+
type: zod.default.literal("question"),
|
|
418
|
+
data: QuestionSchema
|
|
419
|
+
}), zod.default.object({
|
|
420
|
+
type: zod.default.literal("slide"),
|
|
421
|
+
data: SlideSchema
|
|
422
|
+
})]);
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/types/quizfile.ts
|
|
425
|
+
const QuizFileSchema = zod.default.object({
|
|
426
|
+
id: zod.default.uuid(),
|
|
427
|
+
version: zod.default.literal(2),
|
|
428
|
+
title: zod.default.string().min(1, "Title must be atleast 1 character long").max(64, "Title can't be longer than 64 characters"),
|
|
429
|
+
description: zod.default.string().max(255, "Description can't be longer than 256 characters").optional(),
|
|
430
|
+
theme: QuizThemeSchema,
|
|
431
|
+
language: zod.default.string().length(2, "Language must be a 2-letter ISO 639-1 code"),
|
|
432
|
+
steps: zod.default.array(QuizStepSchema).min(1, "Quiz must have at least 1 step"),
|
|
433
|
+
images: zod.default.record(zod.default.hash("sha256", { error: "Invalid image hash" }), zod.default.string().refine((val) => {
|
|
434
|
+
return val.startsWith("http") || val.startsWith("data:image/");
|
|
435
|
+
}, "Image must be a valid URL or Base64 data string")),
|
|
436
|
+
updatedAt: zod.default.iso.datetime(),
|
|
437
|
+
createdAt: zod.default.iso.datetime()
|
|
438
|
+
});
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/util/names/animals.json
|
|
441
|
+
var animals_default = [
|
|
442
|
+
"Dog",
|
|
443
|
+
"Cat",
|
|
444
|
+
"Rabbit",
|
|
445
|
+
"Lion",
|
|
446
|
+
"Tiger",
|
|
447
|
+
"Bear",
|
|
448
|
+
"Wolf",
|
|
449
|
+
"Fox",
|
|
450
|
+
"Panda",
|
|
451
|
+
"Elephant",
|
|
452
|
+
"Gorilla",
|
|
453
|
+
"Giraffe",
|
|
454
|
+
"Hippo",
|
|
455
|
+
"Rhino",
|
|
456
|
+
"Zebra",
|
|
457
|
+
"Crocodile",
|
|
458
|
+
"Alligator",
|
|
459
|
+
"Octopus",
|
|
460
|
+
"Butterfly",
|
|
461
|
+
"Bee",
|
|
462
|
+
"Ant"
|
|
463
|
+
];
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/util/names/additives.json
|
|
466
|
+
var additives_default = [
|
|
467
|
+
"Funny",
|
|
468
|
+
"Happy",
|
|
469
|
+
"Sad",
|
|
470
|
+
"Angry",
|
|
471
|
+
"Scary",
|
|
472
|
+
"Serious",
|
|
473
|
+
"Cautious",
|
|
474
|
+
"Joyful",
|
|
475
|
+
"Broken",
|
|
476
|
+
"Smart",
|
|
477
|
+
"Shy",
|
|
478
|
+
"Silly",
|
|
479
|
+
"Lazy",
|
|
480
|
+
"Cheerful",
|
|
481
|
+
"Calm",
|
|
482
|
+
"Sleepy",
|
|
483
|
+
"Tired",
|
|
484
|
+
"Lonely",
|
|
485
|
+
"Crazy",
|
|
486
|
+
"Mad",
|
|
487
|
+
"Gentle"
|
|
488
|
+
];
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/util/names/colors.json
|
|
491
|
+
var colors_default = [
|
|
492
|
+
"Red",
|
|
493
|
+
"Blue",
|
|
494
|
+
"Green",
|
|
495
|
+
"Yellow",
|
|
496
|
+
"Purple",
|
|
497
|
+
"Orange",
|
|
498
|
+
"Pink",
|
|
499
|
+
"Brown",
|
|
500
|
+
"Black",
|
|
501
|
+
"White",
|
|
502
|
+
"Gray",
|
|
503
|
+
"Cyan"
|
|
504
|
+
];
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/util/names/names.ts
|
|
507
|
+
const generateName = () => {
|
|
508
|
+
const animal = animals_default[Math.floor(Math.random() * animals_default.length)];
|
|
509
|
+
const additive = additives_default[Math.floor(Math.random() * additives_default.length)];
|
|
510
|
+
return `${colors_default[Math.floor(Math.random() * colors_default.length)]}${additive}${animal}`;
|
|
511
|
+
};
|
|
512
|
+
const generateUniqueName = (players) => {
|
|
513
|
+
const maxPossible = animals_default.length * additives_default.length * colors_default.length;
|
|
514
|
+
if (players.length >= maxPossible) throw new Error("No unique names available");
|
|
515
|
+
const existingNames = new Set(players.map((p) => p.name));
|
|
516
|
+
for (let i = 0; i < 100; i++) {
|
|
517
|
+
const candidate = generateName();
|
|
518
|
+
if (!existingNames.has(candidate)) return candidate;
|
|
519
|
+
}
|
|
520
|
+
for (const c of colors_default) for (const ad of additives_default) for (const an of animals_default) {
|
|
521
|
+
const candidate = `${c}${ad}${an}`;
|
|
522
|
+
if (!existingNames.has(candidate)) return candidate;
|
|
523
|
+
}
|
|
524
|
+
throw new Error("No unique names available");
|
|
525
|
+
};
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/util/sanitizer.ts
|
|
528
|
+
const sanitizeQuestion = (question) => {
|
|
529
|
+
if (isMultipleChoice(question)) return {
|
|
530
|
+
...question,
|
|
531
|
+
choices: question.choices.map(({ text }) => ({ text }))
|
|
532
|
+
};
|
|
533
|
+
if (isTrueFalse(question)) {
|
|
534
|
+
const { answer, ...sanitized } = question;
|
|
535
|
+
return sanitized;
|
|
536
|
+
}
|
|
537
|
+
if (isShortAnswer(question)) {
|
|
538
|
+
const { answers, ...sanitized } = question;
|
|
539
|
+
return sanitized;
|
|
540
|
+
}
|
|
541
|
+
throw new Error("Invalid question type");
|
|
542
|
+
};
|
|
543
|
+
//#endregion
|
|
544
|
+
exports.ChoiceSchema = ChoiceSchema;
|
|
545
|
+
exports.LobbyStatus = LobbyStatus;
|
|
546
|
+
exports.MultipleChoiceQuestionAnswerSchema = MultipleChoiceQuestionAnswerSchema;
|
|
547
|
+
exports.MultipleChoiceQuestionSchema = MultipleChoiceQuestionSchema;
|
|
548
|
+
exports.QuizFileSchema = QuizFileSchema;
|
|
549
|
+
exports.SafeChoiceSchema = SafeChoiceSchema;
|
|
550
|
+
exports.SafeMultipleChoiceQuestionSchema = SafeMultipleChoiceQuestionSchema;
|
|
551
|
+
exports.SafeShortAnswerQuestionSchema = SafeShortAnswerQuestionSchema;
|
|
552
|
+
exports.SafeTrueFalseQuestionSchema = SafeTrueFalseQuestionSchema;
|
|
553
|
+
exports.ShortAnswerQuestionAnswerSchema = ShortAnswerQuestionAnswerSchema;
|
|
554
|
+
exports.ShortAnswerQuestionSchema = ShortAnswerQuestionSchema;
|
|
555
|
+
exports.TrueFalseQuestionAnswerSchema = TrueFalseQuestionAnswerSchema;
|
|
556
|
+
exports.TrueFalseQuestionSchema = TrueFalseQuestionSchema;
|
|
557
|
+
exports.advanceLobby = advanceLobby;
|
|
558
|
+
exports.calculateScore = calculateScore;
|
|
559
|
+
exports.createKickPlayerEvent = createKickPlayerEvent;
|
|
560
|
+
exports.createLobby = createLobby;
|
|
561
|
+
exports.createLobbyDeletedEvent = createLobbyDeletedEvent;
|
|
562
|
+
exports.createLobbyJoinedEvent = createLobbyJoinedEvent;
|
|
563
|
+
exports.createLobbyStatusUpdateEvent = createLobbyStatusUpdateEvent;
|
|
564
|
+
exports.createNextStepEvent = createNextStepEvent;
|
|
565
|
+
exports.createPlayerAnswerResultEvent = createPlayerAnswerResultEvent;
|
|
566
|
+
exports.createPlayerJoinedEvent = createPlayerJoinedEvent;
|
|
567
|
+
exports.createPlayerKickedEvent = createPlayerKickedEvent;
|
|
568
|
+
exports.createPlayerLeftEvent = createPlayerLeftEvent;
|
|
569
|
+
exports.createPlayerUpdateEvent = createPlayerUpdateEvent;
|
|
570
|
+
exports.createStartLobbyEvent = createStartLobbyEvent;
|
|
571
|
+
exports.createSubmitAnswerEvent = createSubmitAnswerEvent;
|
|
572
|
+
exports.createUpdateLobbyAnswersEvent = createUpdateLobbyAnswersEvent;
|
|
573
|
+
exports.generateName = generateName;
|
|
574
|
+
exports.generateUniqueName = generateUniqueName;
|
|
575
|
+
exports.handleSubmission = handleSubmission;
|
|
576
|
+
exports.isCorrect = isCorrect;
|
|
577
|
+
exports.isMultipleChoice = isMultipleChoice;
|
|
578
|
+
exports.isQuestion = isQuestion;
|
|
579
|
+
exports.isShortAnswer = isShortAnswer;
|
|
580
|
+
exports.isSlide = isSlide;
|
|
581
|
+
exports.isTrueFalse = isTrueFalse;
|
|
582
|
+
exports.sanitizeQuestion = sanitizeQuestion;
|