DEV Community

Joe Lin for BeGoodTool.com

Posted on

How to Generate Bingo Cards Without Accidentally Reusing the Same Words

Generating a bingo card looks like filling a grid, but the details show up as soon as users request different sizes, custom words, free centers, and multiple cards. I built the generator around a normalized list, a Fisher–Yates shuffle, and a separate play-state model so printable cards and clickable games do not fight each other.

Validate the content before building cells

Custom text is turned into a trimmed list:

const wordList = computed(() =>
  wordText.value
    .split(/\r?\n/)
    .map((word) => word.trim())
    .filter((word) => word.length > 0)
);

const neededWords = computed(() => {
  const totalCells = boardSize.value * boardSize.value;
  return freeCenter.value ? totalCells - 1 : totalCells;
});
Enter fullscreen mode Exit fullscreen mode

The count changes when the free-center option changes. generate refuses to create a word card if there are fewer entries than neededWords. That is better than silently placing blank cells and discovering the problem only after downloading a card.

Shuffle copies, not the source array

The source uses a small Fisher–Yates implementation:

function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}
Enter fullscreen mode Exit fullscreen mode

The slice is important. A generator should not reorder the user’s original word list just because it is preparing one card. Fisher–Yates gives every remaining position a random candidate under Math.random; it is not cryptographic randomness, but it is appropriate for a casual game.

Number cards use a column pool:

const numbersPerColumn = size === 5 ? 15 : size;
for (let c = 0; c < size; c++) {
  const start = c * numbersPerColumn + 1;
  const pool = [];
  for (let n = start; n < start + numbersPerColumn; n++) {
    pool.push(n);
  }
  cols.push(shuffle(pool).slice(0, size));
}
Enter fullscreen mode Exit fullscreen mode

The classic five-column card has fifteen numbers per column. Larger boards use a pool equal to the board size. That is a product rule, not a universal bingo standard, so it belongs in the implementation rather than being implied by a generic “random grid” helper.

A free cell is data with behavior

While creating cells, the center is marked and labeled:

const isFree =
  freeCenter.value && row === center && col === center;
cells.push({
  label: isFree ? t("bingoCardGenerator.freeText")
                : String(cols[col][row]),
  marked: isFree,
  free: isFree,
});
Enter fullscreen mode Exit fullscreen mode

Later, toggleMark refuses to toggle free cells and records every other click in history. That history powers undo without trying to infer intent from the current grid.

Winning lines are generated from the board size

The win checker creates horizontal rows, vertical columns, and two diagonals, then tests every cell in a line. connectedLineCount counts completed lines and isCardWon compares it to the configured target. This supports a five-line game without hard-coding a 5×5 scan, and the maximum target is size * 2 + 2.

The limitation is that “random” does not mean unique. Multiple generated cards can share words or number patterns, especially with small custom lists. Word cards require enough distinct input entries, but the tool does not promise globally unique cards across a batch. Very large boards also create a lot of cells and a lot of work for browser-side PNG/ZIP downloads.

Accessibility and unusual board sizes are implementation details

The generator creates column labels with spreadsheet-style letters:

function createColumnLabels(size) {
  if (size === 5) return ["B", "I", "N", "G", "O"];
  return Array.from({ length: size }, (_, index) => {
    let label = "";
    let value = index;
    do {
      label = String.fromCharCode(65 + (value % 26)) + label;
      value = Math.floor(value / 26) - 1;
    } while (value >= 0);
    return label;
  });
}
Enter fullscreen mode Exit fullscreen mode

The special five-column labels preserve the familiar B-I-N-G-O convention. Larger boards use A, B, …, Z, then AA and beyond, so an accessible cell label can still identify a position even when the visual card has no standard letter heading.

The source also computes contrast text color from a six-digit hex color. That is a helpful default for custom themes, but it is not a complete accessibility audit: color contrast for every text/background combination, print output, and user-selected word length still needs testing. A free center is represented as a marked cell from the beginning, which means the play-mode win logic naturally includes it without a special case in every line scan.

The download path is another boundary. Browser canvas and ZIP libraries can handle ordinary batches, but memory use grows with card count and board size. A phone generating many 31×31 image cards may hit a practical limit long before the JavaScript algorithm is mathematically wrong.

The separate mode, cards, and playMode refs are useful here. Generating a new batch can reset the active card and leave the printable data intact; entering play mode does not need to regenerate anything. That separation also makes it possible to download a generated card without marking cells, which is the expected behavior for a printable worksheet.

There is a similar separation between a card’s cells and its history. The cell array describes the current board, while history describes the sequence of manual marks. That means undo can remove the most recent action without trying to reconstruct the whole board from a random seed. It also means a free cell can remain marked when the rest of the card is reset, because resetCard restores each cell to its original free status.

For me, that is the main design lesson: generation and play are related features, but they are not the same state transition. Keeping them separate makes the printable workflow predictable and leaves room for future game rules without changing the code that creates a valid card.

I turned this generator into a small free tool: Bingo Card Generator.

Top comments (0)