Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ dist-ssr

# Editor directories and files
!.vscode/extensions.json
.idea
.claude
.DS_Store
*.suo
*.ntvs*
.env*.local
.idea
.vercel
*.njsproj
*.ntvs*
*.sln
*.suo
*.sw?
.vercel
.env*.local
4 changes: 4 additions & 0 deletions convex/_fixtures/createMockTournamentCompetitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const createMockTournamentCompetitor = (
_id: overrides.id as Id<'tournamentCompetitors'>,
activeRegistrationCount: 0,
availableActions: [],
details: {
alignments: [],
factions: [],
},
});

export const createMockTournamentCompetitors = (
Expand Down
12 changes: 10 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import type * as _model_common__helpers_getStaticEnumConvexValidator from "../_m
import type * as _model_common__helpers_getStorageUrl from "../_model/common/_helpers/getStorageUrl.js";
import type * as _model_common__helpers_intersectArrays from "../_model/common/_helpers/intersectArrays.js";
import type * as _model_common__helpers_notNullOrUndefined from "../_model/common/_helpers/notNullOrUndefined.js";
import type * as _model_common_alignment from "../_model/common/alignment.js";
import type * as _model_common_baseStats from "../_model/common/baseStats.js";
import type * as _model_common_errors from "../_model/common/errors.js";
import type * as _model_common_faction from "../_model/common/faction.js";
import type * as _model_common_gameSystemConfig from "../_model/common/gameSystemConfig.js";
import type * as _model_common_leagueStatus from "../_model/common/leagueStatus.js";
import type * as _model_common_location from "../_model/common/location.js";
Expand Down Expand Up @@ -121,6 +123,7 @@ import type * as _model_photos_queries_getPhoto from "../_model/photos/queries/g
import type * as _model_photos_table from "../_model/photos/table.js";
import type * as _model_tournamentCompetitors__helpers_deepenTournamentCompetitor from "../_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.js";
import type * as _model_tournamentCompetitors__helpers_getAvailableActions from "../_model/tournamentCompetitors/_helpers/getAvailableActions.js";
import type * as _model_tournamentCompetitors__helpers_getDetails from "../_model/tournamentCompetitors/_helpers/getDetails.js";
import type * as _model_tournamentCompetitors__helpers_getDisplayName from "../_model/tournamentCompetitors/_helpers/getDisplayName.js";
import type * as _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName from "../_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName.js";
import type * as _model_tournamentCompetitors_index from "../_model/tournamentCompetitors/index.js";
Expand Down Expand Up @@ -169,7 +172,7 @@ import type * as _model_tournamentRegistrations__helpers_getDeleteSuccessMessage
import type * as _model_tournamentRegistrations_index from "../_model/tournamentRegistrations/index.js";
import type * as _model_tournamentRegistrations_mutations_createTournamentRegistration from "../_model/tournamentRegistrations/mutations/createTournamentRegistration.js";
import type * as _model_tournamentRegistrations_mutations_deleteTournamentRegistration from "../_model/tournamentRegistrations/mutations/deleteTournamentRegistration.js";
import type * as _model_tournamentRegistrations_mutations_toggleActive from "../_model/tournamentRegistrations/mutations/toggleActive.js";
import type * as _model_tournamentRegistrations_mutations_toggleTournamentRegistrationActive from "../_model/tournamentRegistrations/mutations/toggleTournamentRegistrationActive.js";
import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationByTournamentUser from "../_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.js";
import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationsByCompetitor from "../_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.js";
import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationsByTournament from "../_model/tournamentRegistrations/queries/getTournamentRegistrationsByTournament.js";
Expand Down Expand Up @@ -218,6 +221,7 @@ import type * as _model_tournaments_mutations_endTournamentRound from "../_model
import type * as _model_tournaments_mutations_publishTournament from "../_model/tournaments/mutations/publishTournament.js";
import type * as _model_tournaments_mutations_startTournament from "../_model/tournaments/mutations/startTournament.js";
import type * as _model_tournaments_mutations_startTournamentRound from "../_model/tournaments/mutations/startTournamentRound.js";
import type * as _model_tournaments_mutations_toggleTournamentAlignmentsRevealed from "../_model/tournaments/mutations/toggleTournamentAlignmentsRevealed.js";
import type * as _model_tournaments_mutations_updateTournament from "../_model/tournaments/mutations/updateTournament.js";
import type * as _model_tournaments_queries_getTournament from "../_model/tournaments/queries/getTournament.js";
import type * as _model_tournaments_queries_getTournamentByTournamentPairing from "../_model/tournaments/queries/getTournamentByTournamentPairing.js";
Expand Down Expand Up @@ -328,8 +332,10 @@ declare const fullApi: ApiFromModules<{
"_model/common/_helpers/getStorageUrl": typeof _model_common__helpers_getStorageUrl;
"_model/common/_helpers/intersectArrays": typeof _model_common__helpers_intersectArrays;
"_model/common/_helpers/notNullOrUndefined": typeof _model_common__helpers_notNullOrUndefined;
"_model/common/alignment": typeof _model_common_alignment;
"_model/common/baseStats": typeof _model_common_baseStats;
"_model/common/errors": typeof _model_common_errors;
"_model/common/faction": typeof _model_common_faction;
"_model/common/gameSystemConfig": typeof _model_common_gameSystemConfig;
"_model/common/leagueStatus": typeof _model_common_leagueStatus;
"_model/common/location": typeof _model_common_location;
Expand Down Expand Up @@ -422,6 +428,7 @@ declare const fullApi: ApiFromModules<{
"_model/photos/table": typeof _model_photos_table;
"_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor": typeof _model_tournamentCompetitors__helpers_deepenTournamentCompetitor;
"_model/tournamentCompetitors/_helpers/getAvailableActions": typeof _model_tournamentCompetitors__helpers_getAvailableActions;
"_model/tournamentCompetitors/_helpers/getDetails": typeof _model_tournamentCompetitors__helpers_getDetails;
"_model/tournamentCompetitors/_helpers/getDisplayName": typeof _model_tournamentCompetitors__helpers_getDisplayName;
"_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName": typeof _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName;
"_model/tournamentCompetitors/index": typeof _model_tournamentCompetitors_index;
Expand Down Expand Up @@ -470,7 +477,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournamentRegistrations/index": typeof _model_tournamentRegistrations_index;
"_model/tournamentRegistrations/mutations/createTournamentRegistration": typeof _model_tournamentRegistrations_mutations_createTournamentRegistration;
"_model/tournamentRegistrations/mutations/deleteTournamentRegistration": typeof _model_tournamentRegistrations_mutations_deleteTournamentRegistration;
"_model/tournamentRegistrations/mutations/toggleActive": typeof _model_tournamentRegistrations_mutations_toggleActive;
"_model/tournamentRegistrations/mutations/toggleTournamentRegistrationActive": typeof _model_tournamentRegistrations_mutations_toggleTournamentRegistrationActive;
"_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationByTournamentUser;
"_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationsByCompetitor;
"_model/tournamentRegistrations/queries/getTournamentRegistrationsByTournament": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationsByTournament;
Expand Down Expand Up @@ -519,6 +526,7 @@ declare const fullApi: ApiFromModules<{
"_model/tournaments/mutations/publishTournament": typeof _model_tournaments_mutations_publishTournament;
"_model/tournaments/mutations/startTournament": typeof _model_tournaments_mutations_startTournament;
"_model/tournaments/mutations/startTournamentRound": typeof _model_tournaments_mutations_startTournamentRound;
"_model/tournaments/mutations/toggleTournamentAlignmentsRevealed": typeof _model_tournaments_mutations_toggleTournamentAlignmentsRevealed;
"_model/tournaments/mutations/updateTournament": typeof _model_tournaments_mutations_updateTournament;
"_model/tournaments/queries/getTournament": typeof _model_tournaments_queries_getTournament;
"_model/tournaments/queries/getTournamentByTournamentPairing": typeof _model_tournaments_queries_getTournamentByTournamentPairing;
Expand Down
14 changes: 14 additions & 0 deletions convex/_model/common/alignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Alignment as flamesOfWarV4 } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4';
import { Alignment as teamYankeeV2 } from '@ianpaschal/combat-command-game-systems/teamYankeeV2';
import { v } from 'convex/values';

export type Alignment = `${flamesOfWarV4}` | `${teamYankeeV2}`;

const values: Alignment[] = [
...Object.values(flamesOfWarV4),
...Object.values(teamYankeeV2),
];

export const alignment = v.union(
...values.map((item) => v.literal(item)),
);
14 changes: 14 additions & 0 deletions convex/_model/common/faction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Faction as flamesOfWarV4 } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4';
import { Faction as teamYankeeV2 } from '@ianpaschal/combat-command-game-systems/teamYankeeV2';
import { v } from 'convex/values';

export type Faction = `${flamesOfWarV4}` | `${teamYankeeV2}`;

const values: Faction[] = [
...Object.values(flamesOfWarV4),
...Object.values(teamYankeeV2),
];
Comment on lines +7 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Same duplicate values concern as alignment.ts.

Apply the same deduplication to prevent potential issues with overlapping faction values between game systems.

♻️ Proposed fix
-const values: Faction[] = [
-  ...Object.values(flamesOfWarV4),
-  ...Object.values(teamYankeeV2),
-];
+const values: Faction[] = [
+  ...new Set([
+    ...Object.values(flamesOfWarV4),
+    ...Object.values(teamYankeeV2),
+  ]),
+];
🤖 Prompt for AI Agents
In `@convex/_model/common/faction.ts` around lines 7 - 10, The values array
defined in faction.ts currently concatenates ...Object.values(flamesOfWarV4) and
...Object.values(teamYankeeV2) which can produce duplicate Faction entries;
update the construction of values to deduplicate entries (e.g., by using a Set
or Map keyed on a unique Faction identifier such as id or name) so that the
final values array contains only unique factions. Locate the values declaration
and replace the simple spread concat with a deduplication step that preserves
one canonical Faction per unique id/name.


export const faction = v.union(
...values.map((item) => v.literal(item)),
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QueryCtx } from '../../../_generated/server';
import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistrations';
import { getTournamentResultsByCompetitor } from '../../tournamentResults';
import { getAvailableActions } from './getAvailableActions';
import { getDetails } from './getDetails';
import { getDisplayName } from './getDisplayName';

/* eslint-disable @typescript-eslint/explicit-function-return-type */
Expand Down Expand Up @@ -33,13 +34,15 @@ export const deepenTournamentCompetitor = async (
});
const availableActions = await getAvailableActions(ctx, doc);
const displayName = await getDisplayName(ctx, doc);
const details = getDetails(registrations);
return {
...doc,
...results,
activeRegistrationCount: registrations.filter((r) => r.active).length,
availableActions,
displayName,
registrations,
details,
};
};

Expand Down
28 changes: 28 additions & 0 deletions convex/_model/tournamentCompetitors/_helpers/getDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Alignment } from '../../common/alignment';
import { Faction } from '../../common/faction';
import { DeepTournamentRegistration } from '../../tournamentRegistrations/_helpers/deepenTournamentRegistration';

export const getDetails = (
registrations: DeepTournamentRegistration[],
): {
alignments: Alignment[];
factions: Faction[];
} => {

const alignments = new Set<Alignment>();
const factions = new Set<Faction>();

for (const reg of registrations) {
for (const a of reg.alignments) {
alignments.add(a);
}
for (const f of reg.factions) {
factions.add(f);
}
}

return {
alignments: Array.from(alignments),
factions: Array.from(factions),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const getDisplayName = async (
}

// If competitor has only 1 player, just use the player's name:
if (tournament?.competitorSize === 1 && activeRegistrations[0].user) {
if (tournament.competitorSize === 1 && activeRegistrations[0]?.user) {
return activeRegistrations[0].user.displayName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const toggleTournamentCompetitorActiveArgs = v.object({
export const toggleTournamentCompetitorActive = async (
ctx: MutationCtx,
args: Infer<typeof toggleTournamentCompetitorActiveArgs>,
): Promise<void> => {
): Promise<boolean> => {
// --- CHECK AUTH ----
const userId = await checkAuth(ctx);

Expand Down Expand Up @@ -48,7 +48,7 @@ export const toggleTournamentCompetitorActive = async (
}

// ---- PRIMARY ACTIONS ----
await ctx.db.patch(args.id, {
active: !tournamentCompetitor.active,
});
const active = !tournamentCompetitor.active;
await ctx.db.patch(args.id, { active });
return active;
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('generateDraftPairings', () => {

it('Does allow repeat pairings when explicitly enabled.', () => {
// ---- Act ----
const pairings = generateDraftPairings(competitors, true);
const pairings = generateDraftPairings(competitors, { allowRepeats: true });

// ---- Assert ----
expect(pairings.length).toBe(2);
Expand Down
67 changes: 55 additions & 12 deletions convex/_model/tournamentPairings/_helpers/generateDraftPairings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { assignBye } from './assignBye';
*/
export type CompetitorPair = [DeepTournamentCompetitor, DeepTournamentCompetitor | null];

export type PairingOptions = Partial<{
allowRepeats: boolean;
allowSameAlignment: boolean;
}>;

/**
* Generates draft pairings for an array of ranked TournamentCompetitors.
*
Expand All @@ -22,7 +27,10 @@ export type CompetitorPair = [DeepTournamentCompetitor, DeepTournamentCompetitor
*/
export const generateDraftPairings = (
orderedCompetitors: DeepTournamentCompetitor[],
allowRepeats: boolean = false,
options: PairingOptions = {
allowRepeats: false,
allowSameAlignment: true,
},
): CompetitorPair[] => {
const pairings: CompetitorPair[] = [];

Expand All @@ -33,14 +41,9 @@ export const generateDraftPairings = (
}

// Resolve pairings by input order:
const resolvedPairings = recursivePair(restCompetitors, allowRepeats);
const resolvedPairings = recursivePair(restCompetitors, options);
if (resolvedPairings === null) {
if (allowRepeats) {
// TODO: Figure out if this is needed... it should be impossible!
// ...but good to know if we ever see it, that it is, indeed, possible...
throw new ConvexError(getErrorMessage('NO_VALID_PAIRINGS_POSSIBLE'));
}
throw new ConvexError(getErrorMessage('NO_VALID_PAIRINGS_POSSIBLE_WITHOUT_REPEAT'));
throw new ConvexError(getErrorMessage('NO_VALID_PAIRINGS_POSSIBLE'));
}
pairings.push(...resolvedPairings);
return pairings;
Expand All @@ -51,23 +54,63 @@ export const generateDraftPairings = (
*/
export const recursivePair = (
pool: DeepTournamentCompetitor[],
allowRepeats: boolean,
options: PairingOptions,
): CompetitorPair[] | null => {
if (pool.length === 0) {
return []; // everyone paired
}
const [ anchor, ...rest ] = pool; // best remaining
for (let i = 0; i < rest.length; ++i) {
const opponent = rest[i];
const havePlayed = anchor.opponentIds.includes(opponent._id);
if (havePlayed && !allowRepeats) {
if (checkIfRepeat(anchor, opponent) && !options.allowRepeats) {
continue; // hard‑constraint
}
if (checkIfSameAlignment(anchor, opponent) && !options.allowSameAlignment) {
continue; // hard‑constraint
}
const nextPool = rest.slice(0, i).concat(rest.slice(i + 1));
const sub = recursivePair(nextPool, allowRepeats);
const sub = recursivePair(nextPool, options);
if (sub) {
return [ [ anchor, opponent ], ...sub ];
} // success – unwind
}
return null; // dead end – back‑track
};

const checkIfRepeat = (
a: DeepTournamentCompetitor,
b: DeepTournamentCompetitor,
): boolean => {
if (a.opponentIds.includes(b._id)) {
return true;
}
return false;
};

const checkIfSameAlignment = (
a: DeepTournamentCompetitor,
b: DeepTournamentCompetitor,
): boolean => {
const aAlignments = a.details.alignments;
const bAlignments = b.details.alignments;

// A and B must either:

// have at least 1 with alignment 'flexible'
if (aAlignments.includes('flexible') || bAlignments.includes('flexible')) {
return false;
}

// have at least 1 with multiple alignments
if (aAlignments.length > 1 || bAlignments.length > 1) {
return false;
}

// have 1 each, but be different
if (aAlignments.length === 1 && bAlignments.length === 1 && aAlignments[0] !== bAlignments[0]) {
return false;
}

// Otherwise, they have the same single alignment
return true;
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError } from 'convex/values';

import { Doc } from '../../../_generated/dataModel';
import { QueryCtx } from '../../../_generated/server';
import { getErrorMessage } from '../../common/errors';
import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers';
import { getUser } from '../../users';
import { getAvailableActions } from './getAvailableActions';

Expand All @@ -11,17 +13,35 @@ export const deepenTournamentRegistration = async (
ctx: QueryCtx,
doc: Doc<'tournamentRegistrations'>,
) => {
const userId = await getAuthUserId(ctx);
const { details, ...restDoc } = doc;

const user = await getUser(ctx, { id: doc.userId });
if (!user) {
throw new ConvexError(getErrorMessage('USER_NOT_FOUND'));
}
const tournament = await ctx.db.get(doc.tournamentId);
if (!tournament) {
throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND'));
}

const availableActions = await getAvailableActions(ctx, doc);

const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId);
const alignmentsVisible = isOrganizer || tournament.alignmentsRevealed;
const factionsVisible = isOrganizer || tournament.factionsRevealed;

// TODO: Use lists if they are present. getDetails()
const alignments = Array.from(new Set(alignmentsVisible && details?.alignment ? [details.alignment] : []));
const factions = Array.from(new Set(factionsVisible && details?.faction ? [details.faction] : []));

return {
...doc,
...restDoc,
availableActions,
user,
displayName: user.displayName,
alignments,
factions,
};
};

Expand Down
2 changes: 1 addition & 1 deletion convex/_model/tournamentRegistrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export {
export {
toggleTournamentRegistrationActive,
toggleTournamentRegistrationActiveArgs,
} from './mutations/toggleActive';
} from './mutations/toggleTournamentRegistrationActive';

// Queries
export {
Expand Down
Loading