| |
| |
| |
| |
| |
| |
|
|
|
|
|
|
|
|
| interface ShuffleDeck {
|
| order: readonly string[];
|
| index: number;
|
| idsKey: string;
|
| }
|
|
|
|
|
|
|
| const decks = new Map<string, ShuffleDeck>();
|
| const mutexes = new Map<string, Promise<void>>();
|
|
|
|
|
|
|
| |
| |
| |
|
|
| export function fisherYatesShuffle<T>(arr: readonly T[]): T[] {
|
| const result = [...arr];
|
| for (let i = result.length - 1; i > 0; i--) {
|
| const j = Math.floor(Math.random() * (i + 1));
|
| const tmp = result[i];
|
| result[i] = result[j];
|
| result[j] = tmp;
|
| }
|
| return result;
|
| }
|
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function getNextFromDeck(
|
| namespace: string,
|
| itemIds: readonly string[]
|
| ): Promise<string> {
|
| if (itemIds.length === 0) return "";
|
| if (itemIds.length === 1) return itemIds[0];
|
|
|
|
|
| const currentMutex = mutexes.get(namespace) ?? Promise.resolve();
|
| let resolveMutex: (() => void) | undefined;
|
| mutexes.set(
|
| namespace,
|
| new Promise<void>((resolve) => {
|
| resolveMutex = resolve;
|
| })
|
| );
|
|
|
| try {
|
| await currentMutex;
|
|
|
| const idsKey = [...itemIds].sort().join(",");
|
| const existing = decks.get(namespace);
|
|
|
| // If deck exists, same item set, and not exhausted β advance
|
| if (existing && existing.idsKey === idsKey && existing.index < existing.order.length) {
|
| const id = existing.order[existing.index];
|
| decks.set(namespace, { ...existing, index: existing.index + 1 });
|
| return id;
|
| }
|
|
|
|
|
| const lastUsedId =
|
| existing && existing.idsKey === idsKey && existing.order.length > 0
|
| ? existing.order[existing.order.length - 1]
|
| : undefined;
|
|
|
| const newOrder = fisherYatesShuffle(itemIds);
|
|
|
| if (lastUsedId !== undefined && newOrder[0] === lastUsedId && newOrder.length > 1) {
|
| const swapIdx = 1 + Math.floor(Math.random() * (newOrder.length - 1));
|
| const tmp = newOrder[0];
|
| newOrder[0] = newOrder[swapIdx];
|
| newOrder[swapIdx] = tmp;
|
| }
|
|
|
| decks.set(namespace, { order: newOrder, index: 1, idsKey });
|
| return newOrder[0];
|
| } finally {
|
| resolveMutex?.();
|
| }
|
| }
|
|
|
|
|
|
|
| |
| |
| |
|
|
| export function getNextFromDeckSync(namespace: string, itemIds: readonly string[]): string {
|
| if (itemIds.length === 0) return "";
|
| if (itemIds.length === 1) return itemIds[0];
|
|
|
| const idsKey = [...itemIds].sort().join(",");
|
| const existing = decks.get(namespace);
|
|
|
| if (existing && existing.idsKey === idsKey && existing.index < existing.order.length) {
|
| const id = existing.order[existing.index];
|
| decks.set(namespace, { ...existing, index: existing.index + 1 });
|
| return id;
|
| }
|
|
|
| const lastUsedId =
|
| existing && existing.idsKey === idsKey && existing.order.length > 0
|
| ? existing.order[existing.order.length - 1]
|
| : undefined;
|
|
|
| const newOrder = fisherYatesShuffle(itemIds);
|
|
|
| if (lastUsedId !== undefined && newOrder[0] === lastUsedId && newOrder.length > 1) {
|
| const swapIdx = 1 + Math.floor(Math.random() * (newOrder.length - 1));
|
| const tmp = newOrder[0];
|
| newOrder[0] = newOrder[swapIdx];
|
| newOrder[swapIdx] = tmp;
|
| }
|
|
|
| decks.set(namespace, { order: newOrder, index: 1, idsKey });
|
| return newOrder[0];
|
| }
|
|
|
|
|
|
|
|
|
| export function _resetAllDecks(): void {
|
| decks.clear();
|
| mutexes.clear();
|
| }
|
|
|