diff --git a/app/components/admin/QuestList.vue b/app/components/admin/QuestList.vue
index 938f226..1b4df8b 100644
--- a/app/components/admin/QuestList.vue
+++ b/app/components/admin/QuestList.vue
@@ -168,6 +168,13 @@ async function reorderQuest(quest: QuestListItem, direction: 'up' | 'down') {
+
Opt
diff --git a/app/components/admin/QuestModal.vue b/app/components/admin/QuestModal.vue
index 484ef72..01c799f 100644
--- a/app/components/admin/QuestModal.vue
+++ b/app/components/admin/QuestModal.vue
@@ -31,6 +31,7 @@ const defaultForm = () => ({
title_en: '',
description_de: '',
description_en: '',
+ isBingo: false,
question_de: '',
question_en: '',
location: '',
@@ -73,6 +74,7 @@ watch(showModal, async (visible) => {
title_en: quest.title_en,
description_de: quest.description_de,
description_en: quest.description_en,
+ isBingo: quest.isBingo,
question_de: quest.question_de ?? '',
question_en: quest.question_en ?? '',
location: quest.location ?? '',
@@ -295,6 +297,10 @@ async function deleteImage() {
+
import { parseError } from '#imports';
+import { findBingo } from '#shared/utils/bingo';
import TeamAddModal from '~/components/TeamAddModal.vue';
import TeamSettingsModal from '~/components/TeamSettingsModal.vue';
import { useHuntStore } from '~/stores/hunt';
@@ -25,6 +26,21 @@ const { data, pending, refresh, error } = await useFetch(`/api/hunt/${id}`);
huntStore.setMembership(id, data.value?.isMember ?? false);
+const hasBingo = computed(
+ () => data.value?.quests?.some((q) => q.isBingo) ?? false
+);
+
+const hasBingoDone = computed(() => {
+ if (!hasBingo.value || !data.value?.quests) {
+ return 0;
+ }
+ const bingoQuests = data
+ .value!.quests!.filter((q) => q.isBingo)
+ .map((q) => q.answer?.final ?? false);
+
+ return findBingo(bingoQuests);
+});
+
const settings = useSettingsStore();
useHead({
@@ -136,74 +152,136 @@ ${document.location.href}`
>
-
-
- {{ data ? tKey(data, 'name') : name }}
-
-
-
-
-
-
-
- {{
- t('page.home.start', {
- start: d(data.start, fullDate)
- })
- }}
-
-
- {{
- t('page.home.end', {
- start: d(data.end, fullDate)
- })
- }}
-
-
- {{
- t(
- data.ownTeam !== null
- ? 'page.hunt.team_count_joined'
- : 'page.hunt.team_count',
- data._count.teams
- )
- }}
-
-
-
- {{ t('page.hunt.your_team') }}
- {{ data.ownTeam.name }}
-
-
-
{{ t('page.hunt.invite') }}
-
- {{ data.ownTeam.password }}
-
{{ t('page.hunt.team_settings') }}
+
+
+
+ {{ data ? tKey(data, 'name') : name }}
+
+
+
+
+
-
-
-
-
+
+ {{
+ t('page.home.start', {
+ start: d(data.start, fullDate)
+ })
+ }}
+
+
+ {{
+ t('page.home.end', {
+ start: d(data.end, fullDate)
+ })
+ }}
+
+
+ {{
+ t(
+ data.ownTeam !== null
+ ? 'page.hunt.team_count_joined'
+ : 'page.hunt.team_count',
+ data._count.teams
+ )
+ }}
+
+
+
+ {{ t('page.hunt.your_team') }}
+ {{ data.ownTeam.name }}
+
+
+ {{ t('page.hunt.invite') }}
+
+ {{ data.ownTeam.password }}
+ {{ t('page.hunt.team_settings') }}
+
+
+
+
+
+
+ {{ t('page.hunt.bingo_board') }}
+
+
+ {{ tKey(quest, 'title') }}
+
+
+
+ 🎉 {{ hasBingoDone > 1 ? `${hasBingoDone}x ` : '' }}BINGO!
+ 🎉
+
+ {{ tKey(quest, 'title') }}
+
+
+
+
{
title_en: true,
description_de: true,
description_en: true,
+ isBingo: true,
question_de: true,
question_en: true,
location: true,
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts
index f0b14ce..31829f3 100644
--- a/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts
@@ -42,6 +42,7 @@ export default defineEventHandler(async (event) => {
title_en: z.string().min(1).max(256).trim(),
description_de: z.string().max(2000).trim().default(''),
description_en: z.string().max(2000).trim().default(''),
+ isBingo: z.boolean().default(false),
question_de: z.string().max(1000).trim().nullish(),
question_en: z.string().max(1000).trim().nullish(),
location: z.string().max(256).trim().nullish(),
diff --git a/server/api/admin/hunt/[huntId]/quest/index.get.ts b/server/api/admin/hunt/[huntId]/quest/index.get.ts
index d83a555..ab42cf4 100644
--- a/server/api/admin/hunt/[huntId]/quest/index.get.ts
+++ b/server/api/admin/hunt/[huntId]/quest/index.get.ts
@@ -39,6 +39,7 @@ export default defineEventHandler(async (event) => {
id: true,
title_de: true,
title_en: true,
+ isBingo: true,
order: true,
points: true,
optional: true,
diff --git a/server/api/admin/hunt/[huntId]/quest/index.post.ts b/server/api/admin/hunt/[huntId]/quest/index.post.ts
index cb9e587..8d9f4a4 100644
--- a/server/api/admin/hunt/[huntId]/quest/index.post.ts
+++ b/server/api/admin/hunt/[huntId]/quest/index.post.ts
@@ -43,6 +43,7 @@ export default defineEventHandler(async (event) => {
title_en: z.string().min(1).max(256).trim(),
description_de: z.string().max(2000).trim().default(''),
description_en: z.string().max(2000).trim().default(''),
+ isBingo: z.boolean().default(false),
question_de: z.string().max(1000).trim().nullish(),
question_en: z.string().max(1000).trim().nullish(),
location: z.string().max(256).trim().nullish(),
diff --git a/server/api/hunt/[huntId]/index.get.ts b/server/api/hunt/[huntId]/index.get.ts
index 1bea4ee..92a9f1d 100644
--- a/server/api/hunt/[huntId]/index.get.ts
+++ b/server/api/hunt/[huntId]/index.get.ts
@@ -86,6 +86,7 @@ export default defineEventHandler(async (event) => {
title_en: true,
description_de: true,
description_en: true,
+ isBingo: true,
picture: {
select: {
url: true
@@ -197,7 +198,6 @@ export default defineEventHandler(async (event) => {
0
);
- // TODO: filter out questions before start
return {
isMember,
ownTeam,
diff --git a/server/api/quest/[questId]/index.get.ts b/server/api/quest/[questId]/index.get.ts
index a05d7d7..b69d999 100644
--- a/server/api/quest/[questId]/index.get.ts
+++ b/server/api/quest/[questId]/index.get.ts
@@ -22,6 +22,7 @@ export default defineEventHandler(async (event) => {
title_en: true,
description_de: true,
description_en: true,
+ isBingo: true,
picture: {
select: {
url: true
diff --git a/shared/utils/bingo.test.ts b/shared/utils/bingo.test.ts
new file mode 100644
index 0000000..d907924
--- /dev/null
+++ b/shared/utils/bingo.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'bun:test';
+import { bingoDone, findBingo } from './bingo';
+
+describe('Bingo', () => {
+ it('no bingo', () => {
+ expect(bingoDone([])).toBeFalse();
+ expect(bingoDone(new Array(9).fill(false))).toBeFalse();
+
+ expect(findBingo([])).toBe(0);
+ expect(findBingo(new Array(9).fill(false))).toBe(0);
+ });
+
+ it('bingo', () => {
+ expect(
+ bingoDone([true, true, true, false, false, false, false, false, false])
+ ).toBeTrue();
+ expect(
+ bingoDone([true, false, false, true, false, false, true, false, false])
+ ).toBeTrue();
+ expect(
+ bingoDone([true, false, false, false, true, false, false, false, true])
+ ).toBeTrue();
+ expect(
+ bingoDone([true, false, true, true, true, false, false, false, true])
+ ).toBeTrue();
+ expect(bingoDone(new Array(9).fill(true))).toBeTrue();
+
+ expect(
+ findBingo([true, true, true, false, false, false, false, false, false])
+ ).toBe(1);
+ expect(
+ findBingo([true, false, false, true, false, false, true, false, false])
+ ).toBe(1);
+ expect(
+ findBingo([true, false, false, false, true, false, false, false, true])
+ ).toBe(1);
+ expect(
+ findBingo([true, false, true, true, true, false, false, false, true])
+ ).toBe(1);
+ expect(
+ findBingo([true, false, false, true, true, false, true, false, true])
+ ).toBe(2);
+ expect(
+ findBingo([true, false, true, true, true, false, true, false, true])
+ ).toBe(3);
+ expect(findBingo(new Array(9).fill(true))).toBe(8);
+ });
+});
diff --git a/shared/utils/bingo.ts b/shared/utils/bingo.ts
new file mode 100644
index 0000000..efc9d15
--- /dev/null
+++ b/shared/utils/bingo.ts
@@ -0,0 +1,31 @@
+const possibleCombos = [
+ // horizontal
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ // vertical
+ [0, 3, 6],
+ [1, 4, 7],
+ [2, 5, 8],
+ // diagonal
+ [0, 4, 8],
+ [2, 4, 6]
+];
+
+export function bingoDone(fields: boolean[]) {
+ // TODO: Support different sizes
+ if (fields.length < 9) {
+ return false;
+ }
+
+ return possibleCombos.some((combo) => combo.every((ci) => fields[ci]));
+}
+export function findBingo(fields: boolean[]) {
+ // TODO: Support different sizes
+ if (fields.length < 9) {
+ return 0;
+ }
+
+ return possibleCombos.filter((combo) => combo.every((ci) => !!fields[ci]))
+ .length;
+}