// Outcome planning. Every outcome is decided here, up front, with // crypto randomness; the suspense schedules only *present* it. import { randomInt, shuffle } from './random.js'; export const MODES = ['one', 'pickN', 'teams', 'order', 'elimination']; export const TEAM_NAMES = ['A', 'B', 'C', 'D']; /** * @param {string} mode * @param {string[]} ids participating finger ids * @param {{pickN:number, teams:number}} settings */ export function planRound(mode, ids, settings) { const n = ids.length; switch (mode) { case 'pickN': { const k = Math.max(1, Math.min(settings.pickN | 0, n - 1)); return { mode, winners: shuffle(ids).slice(0, k) }; } case 'teams': { const teamCount = Math.max(2, Math.min(settings.teams | 0, n)); const teams = new Map(); shuffle(ids).forEach((id, i) => teams.set(id, i % teamCount)); return { mode, teams, teamCount }; } case 'order': return { mode, order: shuffle(ids) }; case 'elimination': { // Knock-out order; the last entry is the survivor. const eliminationOrder = shuffle(ids); return { mode, eliminationOrder }; } case 'one': default: return { mode: 'one', winners: [ids[randomInt(n)]] }; } } /** Step intervals that start fast and slow down like a roulette wheel. */ function rouletteTimes(duration, reduced) { let interval = reduced ? 170 : 55; const growth = reduced ? 1.1 : 1.15; const intervals = []; let sum = 0; while (sum < duration) { intervals.push(interval); sum += interval; interval *= growth; } const scale = duration / sum; const times = []; let t = 0; for (const iv of intervals) { times.push(t); t += iv * scale; } return times; } /** * Highlight schedule landing on `target` (array of ids). * Single target: the highlight walks clockwise around the group, like a * roulette ball. Several targets: random subsets flash, ending on the target. * @param {{id:string,x:number,y:number}[]} rings * @returns {{t:number, ids:string[]}[]} */ export function makeHighlightSchedule(rings, target, duration, reduced = false) { const times = rouletteTimes(duration, reduced); const steps = times.length; const ids = rings.map((r) => r.id); if (target.length === 1) { const cx = rings.reduce((s, r) => s + r.x, 0) / rings.length; const cy = rings.reduce((s, r) => s + r.y, 0) / rings.length; const ordered = rings .slice() .sort((a, b) => Math.atan2(a.y - cy, a.x - cx) - Math.atan2(b.y - cy, b.x - cx)) .map((r) => r.id); const m = ordered.length; const end = ordered.indexOf(target[0]); const start = (((end - (steps - 1)) % m) + m) % m; return times.map((t, i) => ({ t, ids: [ordered[(start + i) % m]] })); } // Build backwards from the target so every step differs from the next one. const key = (a) => a.slice().sort().join('|'); const out = new Array(steps); out[steps - 1] = target.slice(); for (let i = steps - 2; i >= 0; i--) { const next = key(out[i + 1]); let set; for (let tries = 0; tries < 20; tries++) { set = shuffle(ids).slice(0, target.length); if (key(set) !== next) break; } out[i] = set; } return times.map((t, i) => ({ t, ids: out[i] })); } /** Team schedule: rings flicker through random balanced splits, ending on the real one. */ export function makeTeamSchedule(ids, teams, teamCount, duration, reduced = false) { const times = rouletteTimes(duration, reduced); return times.map((t, i) => { if (i === times.length - 1) return { t, teams }; const m = new Map(); shuffle(ids).forEach((id, j) => m.set(id, j % teamCount)); return { t, teams: m }; }); }