1
\$\begingroup\$

New to coding/Javascript. Writing a basic Tic Tac Toe game.

I'm writing the game to be dynamic, so it won't always be a three-by-three grid, but rather any size grid larger than 1. I have a completely functioning game and win checker function, but the win checker is very, very wet. I've tried several way to dry it up, but can't really find a good pattern that doesn't cause bugs or issues.

The winChecker function below receives a 2D array that could resemble something like (for a 3x3 grid):

const squares = [
  [null, null, null],
  [null, null, null],
  [null, null, null]
];

The winChecker function looks like:

const winChecker = squares => {
  for (let column = 0; column < squares.length; column++) {
    const horizontalCase = squares[column][0];
    const verticalCase = squares[0][column];
    const negativeCase = squares[0][0];
    const positiveCase = squares[0][squares.length - 1];
    if (horizontalCase !== null) {
      for (let row = 0; row < squares.length; row++) {
        if (horizontalCase !== squares[column][row]) {
          break;
        } else {
          if (row === squares.length - 1) {
            return true;
          }
        }
      }
    }
    if (verticalCase !== null) {
      for (let row = 0; row < squares.length; row++) {
        if (verticalCase !== squares[row][column]) {
          break;
        } else {
          if (row === squares.length - 1) {
            return true;
          }
        }
      }
    }
    if (negativeCase !== null) {
      for (let row = 0; row < squares.length; row++) {
        if (negativeCase !== squares[row][row]) {
          break;
        } else {
          if (row === squares.length - 1) {
            return true;
          }
        }
      }
    }
    if (positiveCase !== null) {
      for (let row = 0; row < squares.length; row++) {
        if (positiveCase !== squares[row][squares.length - 1 - row]) {
          break;
        } else {
          if (row === squares.length - 1) {
            return true;
          }
        }
      }
    }
  }
  return false;

The function above actually works, it's just real wet. Any help in drying it up would be lovely.

\$\endgroup\$
  • \$\begingroup\$ Welcome to CodeReview@SE. Site standard is to title questions for what the code is to accomplish, not your issues with it. Repeating the language tag in the title is frowned upon. \$\endgroup\$ – greybeard Mar 21 at 4:39
1
\$\begingroup\$

I think the nicest thing to do is to split your task into three bits, essentially you want to see if a row, column or diagonal has all the entries the same.

A row, column or diagonal can all be thought of just as a (1D) array.

In fact, these things are all the 'lines' (pictorially) that you can find in a 2D array (which span the whole array).

Now given a 2D array A, there's several ways to define a line, for instance the line which corresponds to the second row we can say is the pairs (i, j) such that i === 1 (and j can be anything). Similarly the left diagonal is the pairs (i, j) such that i === j.

Ok so I want to think of a line as a function, for instance I will think of the second row as the function

(i, j) => i === 1

OK, now given a line (which we represent as a function), I want to extract the elements of A which lie on that line, so I should traverse A, and for each element (i, j) I check if that element is in my line (i.e. if my function returns true for it) and if so include it in my result.

How to do this? Well I /want/ to be able to loop through a 2D array gracefully, sadly we can't do that in JS, so I'll just look at each row in turn, determine which elements of that row are in my line and keep them, something like.

function getSectionOf2DArray(arr, lineFunc) {
    return arr.map((row, i) => row.filter((_, j) => lineFunc(i, j)));
}

So now I have a 2D array still, the element at position i of the array is an array consisting of the things in row i which are in my line. If I flatten it I'll have the array of things in my line. I'm not sure if you're using node or are in the browser, so I'll write a flatten to be safe.

function getSectionOf2DArray(arr, sectionFunc) {
    return Array.prototype.concat.apply(
        [],
        arr.map((row, i) => row.filter((_, j) => sectionFunc(i, j)))
    );
}

Nearly there now, let me define the lines of a 2D array.

First the rows, well given a 2D array A, these are just the things (i, j) => i === k for k up to the length of the array.

const rowLines = array =>
    Array(array.length)
        .fill(0)
        .map((_, k) => (i, j) => i === k);

Now the columns, similar story just with a slight reversal, now it's (i, j) => j === k

const colLines = array =>
    Array(array.length)
        .fill(0)
        .map((_, k) => (i, j) => j === k);

Finally the diagonals, the left diagonal (i.e. starting in the top left) is (i, j) => i === j, while the right diagonal is (i, j) => i + j === A.length - 1

const leftDiag = array => (i, j) => i === j;
const rightDiag = array => (i, j) => i + j === array.length - 1;

(I have made leftDiag continue to be a function defined in terms of array just for consistency, it will make our life nicer later).

Great, so now if we do

const ourLines = array =>
    rowLines(array)
        .concat(colLines(array))
        .concat(leftDiag(array))
        .concat(rightDiag(array));

Then ourLines is a function which takes an array and returns an array of lines (aka functions).

Now we just need to define a function which says whether an 'evaluated' (as in I have the actual elements rather than the function) line corresponds to a win, this is easy, it's just if all the elements are the same and not equal to undefined. If all the elements are the same then when we turn the array into a set it will have just one element

const win = evalLine => new Set(evalLine).size === 1 && evalLine [0] !== undefined;

OK, now we should just go through our lines, get the actual array of elements in A they correspond to, and check if any is a win.

const winResults = array =>
    ourLines(array).map(line => win(getSectionOf2DArray(array, line)));

const isThereAWin = array => winResults(array).includes(true);

In summary,

const winChecker = squares => {
    const getSectionOf2DArray = (arr, sectionFunc) =>
        Array.prototype.concat.apply(
            [],
            arr.map((row, i) => row.filter((_, j) => sectionFunc(i, j)))
        );

    const rowLines = array => Array(array.length).fill(0).map((_, k) => (i, j) => i === k);

    const colLines = array => Array(array.length).fill(0).map((_, k) => (i, j) => j === k);

    const leftDiag = array => (i, j) => i === j;
    const rightDiag = array => (i, j) => i + j === array.length - 1;

    const ourLines = array =>
        rowLines(array)
           .concat(colLines(array))
           .concat(leftDiag(array))
           .concat(rightDiag(array));

    const win = evalLine => new Set(evalLine).size === 1 && evalLine[0] !== undefined;

    const winResults = array => ourLines(array).map(line => win(getSectionOf2DArray(array, line)));

    const isThereAWin = array => winResults(array).includes(true);

    return isThereAWin(squares);
};

I hope you agree this pattern is nice. There are ways to write more concise things for your specific use case but this is simple and extendable.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.