Skip to content

Commit f47e699

Browse files
committed
Add global recipe assignment scoring
1 parent 7126160 commit f47e699

6 files changed

Lines changed: 531 additions & 2 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { Activity, AssignmentCode, Person } from '@wca/helpers';
3+
import type { Constraint } from 'wca-group-generators';
4+
import {
5+
buildActivity,
6+
buildPerson,
7+
buildWcif,
8+
} from '../../store/reducers/_tests_/helpers';
9+
import { optimizeAssignmentsGlobally } from './globalScoring';
10+
11+
const assignmentCode: AssignmentCode = 'competitor';
12+
13+
const assignment = (activityId: number) => ({
14+
activityId,
15+
assignmentCode,
16+
stationNumber: null,
17+
});
18+
19+
const activity = (id: number, groupNumber: number): Activity =>
20+
buildActivity({
21+
id,
22+
name: `Group ${groupNumber}`,
23+
activityCode: `333-r1-g${groupNumber}`,
24+
});
25+
26+
const person = (registrantId: number, name: string, activityId?: number): Person =>
27+
buildPerson({
28+
registrantId,
29+
name,
30+
assignments: activityId ? [assignment(activityId)] : [],
31+
});
32+
33+
const preferredActivity: Constraint = {
34+
name: 'Preferred Activity',
35+
score: ({ person, activity }) => {
36+
if (person.name.includes('One')) {
37+
return activity.id === 101 ? 1 : 0;
38+
}
39+
40+
if (person.name.includes('Two')) {
41+
return activity.id === 102 ? 1 : 0;
42+
}
43+
44+
return 0;
45+
},
46+
};
47+
48+
const onePersonPerActivity: Constraint = {
49+
name: 'One Person Per Activity',
50+
score: ({ wcif, activity, assignmentCode }) => {
51+
const peopleInActivity = wcif.persons.filter((candidate) =>
52+
candidate.assignments?.some(
53+
(assignment) =>
54+
assignment.assignmentCode === assignmentCode && assignment.activityId === activity.id
55+
)
56+
);
57+
58+
return peopleInActivity.length > 0 ? null : 0;
59+
},
60+
};
61+
62+
describe('global recipe scoring', () => {
63+
it('can repair an invalid full assignment score', () => {
64+
const activities = [activity(101, 1), activity(102, 2)];
65+
const beforePeople = [person(1, 'Person One'), person(2, 'Person Two')];
66+
const generatedPeople = [person(1, 'Person One', 101), person(2, 'Person Two', 101)];
67+
68+
const optimizedWcif = optimizeAssignmentsGlobally({
69+
beforeWcif: buildWcif(activities, beforePeople),
70+
wcif: buildWcif(activities, generatedPeople),
71+
cluster: beforePeople,
72+
activities,
73+
assignmentCode,
74+
constraints: [{ constraint: onePersonPerActivity, weight: 1 }],
75+
});
76+
77+
const assignedActivityIds = optimizedWcif.persons.flatMap(
78+
(candidate) => candidate.assignments?.map((personAssignment) => personAssignment.activityId) ?? []
79+
);
80+
81+
expect(new Set(assignedActivityIds)).toEqual(new Set([101, 102]));
82+
});
83+
84+
it('improves the full assignment score with swaps when single moves are illegal', () => {
85+
const activities = [activity(101, 1), activity(102, 2)];
86+
const beforePeople = [person(1, 'Person One'), person(2, 'Person Two')];
87+
const generatedPeople = [person(1, 'Person One', 102), person(2, 'Person Two', 101)];
88+
89+
const optimizedWcif = optimizeAssignmentsGlobally({
90+
beforeWcif: buildWcif(activities, beforePeople),
91+
wcif: buildWcif(activities, generatedPeople),
92+
cluster: beforePeople,
93+
activities,
94+
assignmentCode,
95+
constraints: [
96+
{ constraint: onePersonPerActivity, weight: 1 },
97+
{ constraint: preferredActivity, weight: 1 },
98+
],
99+
});
100+
101+
expect(
102+
optimizedWcif.persons.find((candidate) => candidate.registrantId === 1)?.assignments
103+
).toContainEqual(assignment(101));
104+
expect(
105+
optimizedWcif.persons.find((candidate) => candidate.registrantId === 2)?.assignments
106+
).toContainEqual(assignment(102));
107+
});
108+
109+
it('does not move assignments that existed before the current step', () => {
110+
const activities = [activity(101, 1), activity(102, 2)];
111+
const fixedPerson = person(1, 'Person One', 102);
112+
const movablePerson = person(2, 'Person Two');
113+
114+
const optimizedWcif = optimizeAssignmentsGlobally({
115+
beforeWcif: buildWcif(activities, [fixedPerson, movablePerson]),
116+
wcif: buildWcif(activities, [fixedPerson, person(2, 'Person Two', 101)]),
117+
cluster: [fixedPerson, movablePerson],
118+
activities,
119+
assignmentCode,
120+
constraints: [{ constraint: preferredActivity, weight: 1 }],
121+
});
122+
123+
expect(
124+
optimizedWcif.persons.find((candidate) => candidate.registrantId === 1)?.assignments
125+
).toContainEqual(assignment(102));
126+
});
127+
});

0 commit comments

Comments
 (0)