sparkletown/sparkle

View on GitHub
scripts/simulation/table.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { strict as assert } from "assert";

import chalk from "chalk";
import faker from "faker";

import { takeSeatAtTable as actualTakeSeat } from "../lib/bot";
import { getVenueTablesInfo } from "../lib/documents";
import { withErrorReporter } from "../lib/log";
import { SimContext, TableInfo } from "../lib/types";
import { generateMultipleTablesFor, pickValueFrom, sleep } from "../lib/utils";

export const DEFAULT_TABLE_CHUNK_SIZE = 10;
export const DEFAULT_TABLE_TICK_MS = 1000;
export const DEFAULT_TABLE_AFFINITY = 0.01;
export const DEFAULT_TABLE_IMPATIENCE = 0.01;

export const DEFAULT_JAZZ_COUNT = 12;
export const DEFAULT_CONV_COUNT = 10;

export const DEFAULT_TABLE_COL = 3;
export const DEFAULT_TABLE_ROW = 2;
export const DEFAULT_TABLE_CAP = DEFAULT_TABLE_ROW * DEFAULT_TABLE_COL;

export const DEFAULT_JAZZ_TABLE_INFOS: TableInfo[] = generateMultipleTablesFor(
  DEFAULT_JAZZ_COUNT,
  { cap: DEFAULT_TABLE_CAP, col: DEFAULT_TABLE_COL, row: DEFAULT_TABLE_ROW }
);
export const DEFAULT_CONV_TABLE_INFOS: TableInfo[] = generateMultipleTablesFor(
  DEFAULT_CONV_COUNT,
  { cap: DEFAULT_TABLE_CAP, col: DEFAULT_TABLE_COL, row: DEFAULT_TABLE_ROW }
);

export const simTable: (options: SimContext) => Promise<void> = async (
  options
) => {
  const { userRefs, usersById, conf, stop, template } = options;

  const impatience = conf.table?.impatience ?? DEFAULT_TABLE_IMPATIENCE;
  const affinity =
    conf.table?.affinity ?? conf.affinity ?? DEFAULT_TABLE_AFFINITY;
  const tick = conf.table?.tick ?? conf.tick ?? DEFAULT_TABLE_TICK_MS;
  const chunkSize =
    conf.table?.chunkSize ?? conf.chunkSize ?? DEFAULT_TABLE_CHUNK_SIZE;

  assert.ok(
    Number.isSafeInteger(chunkSize) && chunkSize > 0,
    chalk`${simTable.name}(): {magenta chunkCount} must be integer {yellow > 0}`
  );
  assert.ok(
    Number.isFinite(tick) && tick >= 10,
    chalk`${simTable.name}(): {magenta tick} must be integer {yellow >= 10}`
  );
  assert.ok(
    0 <= affinity && affinity <= 1,
    chalk`${simTable.name}(): {magenta affinity} must be a number {yellow from 0 to 1}`
  );

  // Manual config should take precedence if the col/row values are provided
  const tableInfos = await getVenueTablesInfo(options);

  const tables: TableInfo[] =
    tableInfos?.length > 0
      ? tableInfos
      : template === "jazzbar"
      ? DEFAULT_JAZZ_TABLE_INFOS
      : DEFAULT_CONV_TABLE_INFOS;

  // keep track of who's already seated
  const seated: Record<string, boolean> = {};

  const takeSeat = withErrorReporter(conf.log, actualTakeSeat);

  // flag that will not let loop going on when user pressed CTRL+C
  let isStopped = false;
  stop.then(() => (isStopped = true));

  const loop = async () => {
    for (let i = 0, j = userRefs.length; !isStopped && i < j; i += chunkSize) {
      await Promise.all(
        userRefs.slice(i, i + chunkSize).map(async (userRef) => {
          const userId = userRef.id;

          // affinity works only for those already seated
          if (seated[userId] && Math.random() >= affinity) {
            return;
          }

          // more impatient users will sit down fast, then affinity to move will kick in
          if (!seated[userId] && Math.random() >= impatience) {
            return;
          }

          const table = pickValueFrom(tables);

          assert.ok(
            table,
            chalk`${simTable.name}(): {magenta table} must be provided to be able to sit`
          );

          const col = faker.datatype.number({ min: 0, max: table.col });
          const row = faker.datatype.number({ min: 0, max: table.row });

          // TODO: add logic checking for already taken seats

          await takeSeat({
            ...options,
            ...table,
            row,
            col,
            userRef,
            user: usersById[userRef.id],
          });
          seated[userId] = true;
        })
      );
      // explicit sleep between the chunks
      !isStopped && (await sleep(tick));
    }
    // implicit sleep between the loops
    !isStopped && setTimeout(loop, tick);
  };

  // start looping the move updates
  return loop();
};