import { RoundEvent } from '../entities/channel-turn-state.entity.js'; export type ShuffleResult = { paused: true } | { paused: false; newOrder: string[] }; function shuffleInPlace(arr: T[]): T[] { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } // End-of-round shuffle. // - tail = the *last contiguous run* of /no-reply in the round's turn events // (anchor = first of that run; kept in event order) // - head = every current member not in tail, shuffled randomly // - constraint: head[0] !== D, where D = the last member who delivered a // normal message in the round // - if the constraint is unsatisfiable (head empty, or head === [D]) the // rotation pauses instead of shuffling (per spec B.3) export function computeShuffle(roundEvents: RoundEvent[], currentMembers: string[]): ShuffleResult { const memberSet = new Set(currentMembers); // trailing contiguous /no-reply run, in event order, limited to current members const tail: string[] = []; for (let i = roundEvents.length - 1; i >= 0; i--) { if (roundEvents[i].a !== 'noreply') break; if (memberSet.has(roundEvents[i].u)) tail.unshift(roundEvents[i].u); } const tailSet = new Set(tail); // D = last normal speaker in the round (the one right before the trailing run) let d: string | null = null; for (let i = roundEvents.length - 1; i >= 0; i--) { if (roundEvents[i].a === 'normal' && memberSet.has(roundEvents[i].u)) { d = roundEvents[i].u; break; } } const head = currentMembers.filter((u) => !tailSet.has(u)); if (head.length === 0) return { paused: true }; if (head.length === 1 && head[0] === d) return { paused: true }; shuffleInPlace(head); if (head[0] === d) { // length >= 2 here (the [d] singleton case returned paused above) [head[0], head[1]] = [head[1], head[0]]; } return { paused: false, newOrder: [...head, ...tail] }; }