picker/js/modes.js
2026-10-09 13:45:54 +02:00

111 lines
3.6 KiB
JavaScript

// 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 };
});
}