@@ -92,6 +92,249 @@ function createMockRepos() {
9292// ---------------------------------------------------------------------------
9393
9494describe ( 'MultiplayerQueueService — property-based' , ( ) => {
95+ // ── computeCompatibilityScore ──────────────────────────────────────
96+ describe ( 'computeCompatibilityScore' , ( ) => {
97+ function score (
98+ service : MultiplayerQueueService ,
99+ a : Queue ,
100+ b : Queue ,
101+ ) : number {
102+ return ( service as any ) . computeCompatibilityScore ( a , b ) ;
103+ }
104+
105+ it ( 'is symmetric for neutral players (no preferences)' , async ( ) => {
106+ const { mocks, module } = await buildModule ( ) ;
107+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
108+
109+ await fc . assert (
110+ fc . property ( queuePlayerArb , queuePlayerArb , ( p1 , p2 ) => {
111+ const a : Queue = {
112+ ...p1 , id : 'p1' , userId : 'u1' ,
113+ preferences : { ...p1 . preferences , avoidOpponents : undefined , preferredOpponents : undefined } ,
114+ } ;
115+ const b : Queue = {
116+ ...p2 , id : 'p2' , userId : 'u2' ,
117+ preferences : { ...p2 . preferences , avoidOpponents : undefined , preferredOpponents : undefined } ,
118+ } ;
119+ const sAB = score ( service , a , b ) ;
120+ const sBA = score ( service , b , a ) ;
121+ expect ( sAB ) . toBe ( sBA ) ;
122+ expect ( sAB ) . toBe ( 10 ) ;
123+ } ) ,
124+ ) ;
125+
126+ module . close ( ) ;
127+ } ) ;
128+
129+ it ( 'returns -1 when either player avoids the other' , async ( ) => {
130+ const { mocks, module } = await buildModule ( ) ;
131+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
132+
133+ await fc . assert (
134+ fc . property ( queuePlayerArb , queuePlayerArb , ( p1 , p2 ) => {
135+ const a : Queue = {
136+ ...p1 , id : 'p1' , userId : 'u1' ,
137+ preferences : { ...p1 . preferences , avoidOpponents : [ 'u2' ] } ,
138+ } ;
139+ const b : Queue = {
140+ ...p2 , id : 'p2' , userId : 'u2' ,
141+ preferences : { ...p2 . preferences , avoidOpponents : undefined } ,
142+ } ;
143+ expect ( score ( service , a , b ) ) . toBe ( - 1 ) ;
144+ } ) ,
145+ ) ;
146+
147+ module . close ( ) ;
148+ } ) ;
149+
150+ it ( 'returns 100 for mutual preferredOpponents' , async ( ) => {
151+ const { mocks, module } = await buildModule ( ) ;
152+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
153+
154+ await fc . assert (
155+ fc . property ( queuePlayerArb , queuePlayerArb , ( p1 , p2 ) => {
156+ const a : Queue = {
157+ ...p1 , id : 'p1' , userId : 'u1' ,
158+ preferences : { ...p1 . preferences , preferredOpponents : [ 'u2' ] , avoidOpponents : undefined } ,
159+ } ;
160+ const b : Queue = {
161+ ...p2 , id : 'p2' , userId : 'u2' ,
162+ preferences : { ...p2 . preferences , preferredOpponents : [ 'u1' ] , avoidOpponents : undefined } ,
163+ } ;
164+ expect ( score ( service , a , b ) ) . toBe ( 100 ) ;
165+ } ) ,
166+ ) ;
167+
168+ module . close ( ) ;
169+ } ) ;
170+
171+ it ( 'scores avoidOpponents over preferredOpponents' , async ( ) => {
172+ const { mocks, module } = await buildModule ( ) ;
173+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
174+
175+ await fc . assert (
176+ fc . property ( queuePlayerArb , queuePlayerArb , ( p1 , p2 ) => {
177+ const a : Queue = {
178+ ...p1 , id : 'p1' , userId : 'u1' ,
179+ preferences : {
180+ ...p1 . preferences ,
181+ preferredOpponents : [ 'u2' ] ,
182+ avoidOpponents : [ 'u2' ] ,
183+ } ,
184+ } ;
185+ const b : Queue = {
186+ ...p2 , id : 'p2' , userId : 'u2' ,
187+ preferences : { ...p2 . preferences , avoidOpponents : undefined } ,
188+ } ;
189+ // avoidOpponents takes priority over preferredOpponents
190+ expect ( score ( service , a , b ) ) . toBe ( - 1 ) ;
191+ } ) ,
192+ ) ;
193+
194+ module . close ( ) ;
195+ } ) ;
196+ } ) ;
197+
198+ // ── pairPlayersInGroup ──────────────────────────────────────────────
199+ describe ( 'pairPlayersInGroup' , ( ) => {
200+ function pairPlayers (
201+ service : MultiplayerQueueService ,
202+ group : Queue [ ] ,
203+ ) : [ Queue , Queue ] [ ] {
204+ return ( service as any ) . pairPlayersInGroup ( group ) ;
205+ }
206+
207+ it ( 'every player in a pair is compatible (no avoidOpponents violated)' , async ( ) => {
208+ const { mocks, module } = await buildModule ( ) ;
209+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
210+
211+ // Batch of 6 players with random avoidOpponents
212+ const playerBatch6Arb = fc . array ( queuePlayerArb , { minLength : 4 , maxLength : 8 } ) ;
213+
214+ await fc . assert (
215+ fc . property ( playerBatch6Arb , ( players ) => {
216+ // Assign unique IDs to avoid self-avoid issues
217+ const indexed = players . map ( ( p , i ) => ( {
218+ ...p ,
219+ id : `p${ i } ` ,
220+ userId : `u${ i } ` ,
221+ preferences : {
222+ ...p . preferences ,
223+ // Ensure avoidOpponents only reference valid userIds that exist
224+ avoidOpponents : p . preferences ?. avoidOpponents ?. filter (
225+ ( uid ) => uid !== `u${ i } ` , // can't avoid yourself
226+ ) ,
227+ } ,
228+ } ) ) ;
229+
230+ // Map avoidOpponents to actual indices
231+ for ( const p of indexed ) {
232+ if ( p . preferences ?. avoidOpponents ) {
233+ p . preferences . avoidOpponents = p . preferences . avoidOpponents . filter (
234+ ( uid ) => indexed . some ( ( op ) => op . userId === uid ) ,
235+ ) ;
236+ }
237+ }
238+
239+ const pairs = pairPlayers ( service , indexed ) ;
240+
241+ // Verify no pair violates avoidOpponents
242+ for ( const [ a , b ] of pairs ) {
243+ const aAvoidsB = a . preferences ?. avoidOpponents ?. includes ( b . userId ) ?? false ;
244+ const bAvoidsA = b . preferences ?. avoidOpponents ?. includes ( a . userId ) ?? false ;
245+ expect ( aAvoidsB || bAvoidsA ) . toBe ( false ) ;
246+ }
247+ } ) ,
248+ ) ;
249+
250+ module . close ( ) ;
251+ } ) ;
252+
253+ it ( 'never matches a player twice' , async ( ) => {
254+ const { mocks, module } = await buildModule ( ) ;
255+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
256+
257+ const playerBatchArb = fc . array ( queuePlayerArb , { minLength : 2 , maxLength : 20 } ) ;
258+
259+ await fc . assert (
260+ fc . property ( playerBatchArb , ( players ) => {
261+ const indexed = players . map ( ( p , i ) => ( {
262+ ...p ,
263+ id : `p${ i } ` ,
264+ userId : `u${ i } ` ,
265+ } ) ) ;
266+
267+ const pairs = pairPlayers ( service , indexed ) ;
268+ const matchedIds = new Set < string > ( ) ;
269+
270+ for ( const [ a , b ] of pairs ) {
271+ expect ( matchedIds . has ( a . userId ) ) . toBe ( false ) ;
272+ expect ( matchedIds . has ( b . userId ) ) . toBe ( false ) ;
273+ matchedIds . add ( a . userId ) ;
274+ matchedIds . add ( b . userId ) ;
275+ }
276+ } ) ,
277+ ) ;
278+
279+ module . close ( ) ;
280+ } ) ;
281+
282+ it ( 'leftover count is at most 1 (odd group leaves 1 unmatched)' , async ( ) => {
283+ const { mocks, module } = await buildModule ( ) ;
284+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
285+
286+ await fc . assert (
287+ fc . property (
288+ fc . array ( queuePlayerArb , { minLength : 2 , maxLength : 20 } ) ,
289+ ( players ) => {
290+ const indexed = players . map ( ( p , i ) => ( {
291+ ...p ,
292+ id : `p${ i } ` ,
293+ userId : `u${ i } ` ,
294+ preferences : {
295+ ...p . preferences ,
296+ avoidOpponents : undefined , // no avoid conflicts
297+ } ,
298+ } ) ) ;
299+
300+ const pairs = pairPlayers ( service , indexed ) ;
301+ const matchedCount = pairs . length * 2 ;
302+ const leftoverCount = indexed . length - matchedCount ;
303+
304+ expect ( leftoverCount ) . toBeLessThanOrEqual ( 1 ) ;
305+ } ,
306+ ) ,
307+ ) ;
308+
309+ module . close ( ) ;
310+ } ) ;
311+
312+ it ( 'result is deterministic' , async ( ) => {
313+ const { mocks, module } = await buildModule ( ) ;
314+ const service = module . get < MultiplayerQueueService > ( MultiplayerQueueService ) ;
315+
316+ await fc . assert (
317+ fc . property (
318+ fc . array ( queuePlayerArb , { minLength : 2 , maxLength : 12 } ) ,
319+ ( players ) => {
320+ const indexed = players . map ( ( p , i ) => ( {
321+ ...p ,
322+ id : `p${ i } ` ,
323+ userId : `u${ i } ` ,
324+ } ) ) ;
325+
326+ const result1 = pairPlayers ( service , indexed ) ;
327+ const result2 = pairPlayers ( service , indexed ) ;
328+
329+ expect ( result1 ) . toEqual ( result2 ) ;
330+ } ,
331+ ) ,
332+ ) ;
333+
334+ module . close ( ) ;
335+ } ) ;
336+ } ) ;
337+
95338 // ── groupPlayersForMatching ─────────────────────────────────────────
96339 describe ( 'groupPlayersForMatching' , ( ) => {
97340 /** Access the private method. */
0 commit comments