111 lines
3.6 KiB
JavaScript
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 };
|
|
});
|
|
}
|