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; +}