129

Connect "Fore!": Code Golf in Javascript

 6 years ago
source link: https://www.promptworks.com/blog/connect-fore-code-golf-in-javascript
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Connect "Fore!": Code Golf in Javascript

Test and expand your Javascript knowledge with this fun challenge.

September 25, 2017

by Peter Caisse

Picture of a person playing golf

Let’s implement the game Connect Four in as few characters as possible to improve our knowledge of the language and have some fun along the way.

Code golf involves writing a program in the fewest number of characters possible. This presents both a fun challenge and a unique way to learn about the dark corners of the language. We’ll think about code from a different perspective – number of key strokes instead of performance, reliability, or maintainability. We’ll also test the limits of our knowledge since we need to draw from everything the language has to offer to make the size of the source as small as we can!

Code golf traces its origins1 back to 1999 as a pastime of Perl hackers. These days, there are code golf tournaments and languages created specifically for golfing, though one can golf in any language. Javascript is an interesting choice for code golfing as it is both ubiquitous and a deeply flawed language whose standard library is lacking as compared to many modern languages. It combines great flexibility with serious limitations that require a certain amount of creativity to work around. This is especially true when limiting oneself to ECMAScript 5 as we will be doing in this post!

The Challenge

Our goal is to implement the game Connect Four, a connection game similar to tic-tac-toe, so that the source code is as short as possible. Normal gameplay is as follows: two players take turns dropping yellow (Player 1) and red (Player 2) discs into a 6 x 7 grid until one of them wins by connecting 4 like-colored discs in a row (across, down, or diagonally).

For our version, there are some extra requirements:

  • Clicking on a cell in the game board will cause the disc to be "dropped" into that column
  • Attempting to place a disc in a column that is full is ignored
  • Upon winning, the string 'WINNER!' must be logged to the console and no further moves can be made
  • No errors are thrown through normal gameplay
  • Conforms to ECMAScript 5 spec
  • No external libraries

Disallowing the use of external libraries is important since otherwise you could just import an implementation of the game, which defeats the whole purpose of the exercise. No external libraries also forces us to gain a better understanding of the core language so we can take full advantage of what we have to work with.

We are given some scaffolding up front: an HTML page with a 6 x 7 <table> to serve as our game board and some minimal CSS to style it.

You can play a version of the game below:

Basic Implementation

Here is a "normal" implementation of the game2 to serve as a starting point, written without any particular concern for the size of the source file:

var COLS = 7
var ROWS = 6
var MATCHES_TO_WIN = 4

var playerTurn = 0
var grid = [[], [], [], [], [], []]
var gameOver = false

function updateMatches(value, matches) {
  return value === playerTurn ? (matches || 0) + 1 : 0
}

var table = document.querySelector('table')
table.addEventListener('click', function(e) {
  var colIndex = e.target.cellIndex
  if (gameOver || typeof colIndex === "undefined") {
    return
  }
  // Drop into column
  var i, rowIndex
  for (i = ROWS - 1; i >= 0; i--) {
    if (typeof grid[i][colIndex] === "undefined") {
      rowIndex = i
      break
    }
  }
  if (typeof rowIndex === "undefined") {
    return
  }
  // Keep track of latest move
  grid[rowIndex][colIndex] = playerTurn
  // Count matches
  var matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2
  for (i = 0; i < COLS; i++) {
    // Horizontal matches
    matchesHoriz = updateMatches(grid[rowIndex][i], matchesHoriz)
    if (i < ROWS) {
      // Vertical matches
      matchesVert = updateMatches(grid[i][colIndex], matchesVert)
      // Diagonal matches
      matchesDiagonal1 = updateMatches(
        grid[i][colIndex + rowIndex - i], matchesDiagonal1)
      matchesDiagonal2 = updateMatches(
        grid[i][colIndex - rowIndex + i], matchesDiagonal2)
    }
    // Check win
    if (matchesHoriz >= MATCHES_TO_WIN ||
        matchesVert >= MATCHES_TO_WIN ||
        matchesDiagonal1 >= MATCHES_TO_WIN ||
        matchesDiagonal2 >= MATCHES_TO_WIN) {
      gameOver = true
    }
  }
  if (gameOver) {
    console.log('WINNER!')
  }
  // Update board
  table.rows[rowIndex].cells[colIndex].style.backgroundColor =
    playerTurn ? "red" : "yellow"
  // Switch turns
  playerTurn = playerTurn ? 0 : 1
})

After declaring and assigning the constants, we have variables to handle our game state. playerTurn keeps track of whose turn it is (0 for Player 1; 1 for Player 2). grid is an array of six arrays (one for each row) and represents the game board. gameOver, as you might imagine, is a boolean to show whether or not the game has ended.

Within the click event handler attached to <table>, we use the Event object to determine what cell was clicked and thus what column the disc should be "dropped" into. As long as a table cell was clicked3Event.target will be a HTMLTableCellElement which has a cellIndex property, the zero-based column index for that row, which is then used to find the last free cell in that column.

Once we know the column (colIndex) and row (rowIndex) where the disc should be placed, we update the game board and count matches to check for a connection of 4 or more.

The matching algorithm hinges on the fact that for any given move we don't need to check for a match for every cell in the grid; we only need to check if the most recent move resulted in a win since once there is a winner the game is over. There are four ways in which one can connect four: across (matchesHoriz), down (matchesVert), diagonally top-right to bottom-left (matchesDiagonal1), and diagonally top-left to bottom-right (matchesDiagonal2). These counts of the number of discs in a row in each direction are tallied and checked for a win, in which case we set gameOver and log to the console.

Finally, we update the table to show the last move using the HTMLTableElement API and switch turns in preparation for the next move.

That's it! The code is relatively easy to understand and the matching algorithm is straightforward. Variables have meaningful names, functions are used where appropriate, and there are no magic numbers. All in all, this looks like sane, old-school, cross-browser Javascript that gets the job done. Let's see how compact we can make the code!

Code Golf

As we look at code golfing techniques and examples, we will try to make changes that are effectively equivalent; the code might not work the same way or have the same performance characteristics, but in terms of it meeting the requirements it is the same. We will forgo showing the minified version of the code -- minification being the more mechanical process of removing unnecessary whitespace, comments, shortening variable names, etc. -- until the end to keep the code readable and understandable.

Before making any changes, our basic implementation clocks in at:

Character Count: 1468

Avoid Constants

Let's start off with some low-hanging fruit. While they are a boon for maintainability, constants are a waste in code golf. Let's replace COLS, ROWS, and MATCHES_TO_WIN with literals everywhere they're used.

-var COLS = 7
-var ROWS = 6
-var MATCHES_TO_WIN = 4
...
-  for (i = ROWS - 1; i >= 0; i--) {
+  for (i = 5; i >= 0; i--) {
...
-  for (i = 0; i < COLS; i++) {
+  for (i = 0; i < 7; i++) {
...
-    if (i < ROWS) {
+    if (i < 6) {
...
-    if (matchesHoriz >= MATCHES_TO_WIN ||
-        matchesVert >= MATCHES_TO_WIN ||
-        matchesDiagonal1 >= MATCHES_TO_WIN ||
-        matchesDiagonal2 >= MATCHES_TO_WIN) {
+    if (matchesHoriz >= 4 ||
+        matchesVert >= 4 ||
+        matchesDiagonal1 >= 4 ||
+        matchesDiagonal2 >= 4) {
...

Character Count: 1598

Loose Equality Comparison

Our basic implementation uses strict equality comparison (===) which is generally a good practice as Javascript will gladly coerce the values for comparison when using the loose equality (==) operator, making the result of the comparisons hard to predict as shown in the following examples:

> 0 == false
true
> undefined == false
false
> undefined == null
true
> 1 == true
true
> "hi" == true
false
> "1" == true
true

However, in our case we are only comparing primitives to each other and in such a way that coercion won't change the result of the comparisons. We can safely go ahead and replace all occurrences of === with ==.

...
-  return value === playerTurn ? (matches || 0) + 1 : 0
+  return value == playerTurn ? (matches || 0) + 1 : 0
...
-  if (gameOver || typeof colIndex === "undefined") {
+  if (gameOver || typeof colIndex == "undefined") {
...
-    if (typeof grid[i][colIndex] === "undefined") {
+    if (typeof grid[i][colIndex] == "undefined") {
...
-  if (typeof rowIndex === "undefined") {
+  if (typeof rowIndex == "undefined") {
...

Character Count: 1594

Checking for undefined

You'll notice that in several places in the code we check if a value is undefined thusly:

typeof myVar === "undefined"

Why is this? Well, undefined is a Javascript primitive and a property of the global object, i.e. a variable in global scope, and as such is globally available. Despite this, undefined is not a reserved word in any scope besides global scope4:

> (function() { var undefined = 'hi'; console.log(undefined) })()
hi

The use of typeof is a safe way to check if a value is undefined since, as we saw in the previous example, undefined cannot be trusted. It's also a bit more fool-proof in that checking typeof myVar === "undefined" will never error as opposed to doing an equality comparison on a non-existent variable:

> typeof nonExistentVar === "undefined"
true
> nonExistentVar === undefined
ReferenceError: nonExistentVar is not defined

For code golfing purposes, there is an alternative to undefined which is shorter and will always return undefined: the void operator. void evaluates the expression given to it and always returns undefined as a way to get around undefined's weirdness. It also saves us 36 characters!

...
-  if (gameOver || typeof colIndex == "undefined") {
+  if (gameOver || colIndex == void 0) {
...
-    if (typeof grid[i][colIndex] == "undefined") {
+    if (grid[i][colIndex] == void 0) {
...
-  if (typeof rowIndex == "undefined") {
+  if (rowIndex == void 0) {
...

There is yet another trick we can use to return undefined:

> x = 1
1
> x._
undefined

Wait, what? Numbers are primitives! How can we try to access its property? The answer is that while numbers are indeed primitives, Javascript will readily coerce primitives into their wrapper object counterparts. It's the same reason we can call methods on primitives:

> x = 1
1
> x.toString()
'1'

We can exploit this coercion by accessing a non-existent property on an object which will return undefined. Once our code is minified and our variables are all one character, we can get undefined in only 3 characters!

Character Count: 1558

DOM Hacks

Now let's move on to a bit of DOM hacking. When we update the game board with the latest move, we do it like so:

  table.rows[rowIndex].cells[colIndex].style.backgroundColor =
    playerTurn ? "red" : "yellow"

We can get away with doing this in less characters by just setting the background property (the browser is smart enough to figure out we're setting it to a color, as opposed to say, an image, when computing the element's style):

- table.rows[rowIndex].cells[colIndex].style.backgroundColor =
+ table.rows[rowIndex].cells[colIndex].style.background =
    playerTurn ? "red" : "yellow"

We can make our code shorter still by using the hexadecimal string representation of the color yellow instead of its name:

  table.rows[rowIndex].cells[colIndex].style.background =
-   playerTurn ? "red" : "yellow"
+   playerTurn ? "red" : "#ff0"

What about the table itself? When we attach the event listener, we query the DOM for the first <table>, save that to a variable table, and then attach the event listener. We do this so we can later update table to reflect the latest move. However, there is a way we can do this in fewer characters:

-var table = document.querySelector('table')
-table.addEventListener('click', function(e) {
+document.querySelector('table').addEventListener('click', function(e) {
    ...
- table.rows[rowIndex].cells[colIndex].style.background =
+ this.rows[rowIndex].cells[colIndex].style.background =
    playerTurn ? "red" : "#ff0"
    ...
})

This way we can do away with the table variable entirely and reference this within the function. this is a bit tricky in Javascript since the value of this is determined by how a function is called5. That said, in our case this will be the element that the listener was added to.

There also happens to be a shorter alternative to using document.querySelector due to the fact that <table> is the first child of <body>:

-document.querySelector('table').addEventListener('click', function(e) {
+document.body.children[0].addEventListener('click', function(e) {
  ...
})

Finally, instead of using addEventListener, we can use a terser alternative in onclick:

-document.body.children[0].addEventListener('click', function(e) {
+document.body.children[0].onclick = function(e) {
  ...
-})
+}

Setting the onclick property allows us to attach an event listener in much the same way, except we are fundamentally limited in only being able to attach one of them. That lack of flexibility doesn't matter in our case. All of this adds up to a savings of a cool 49 characters.

Character Count: 1509

Toggle Binary Number

Let's take a look at how we are switching turns. We are storing playerTurn as either 0 or 1, and we can exploit the logical operator ! and the unary plus + to substitute for our ternary expression.

- playerTurn = playerTurn ? 0 : 1
+ playerTurn = +!playerTurn

The ! is a logical NOT operator and will return false if the operand can be converted to true, i.e. is truthy. The + will then convert its operand to a number:

> +!0
1
> +!1
0

Character Count: 1503

Math.max()

If we continue working our way up, there are more improvements to be had in checking for a win. Since we really only care if a connection of at least 4 has been made in any direction, we can use Math.max():

    // Check win
-   if (matchesHoriz >= 4 ||
-       matchesVert >= 4 ||
-       matchesDiagonal1 >= 4 ||
-       matchesDiagonal2 >= 4) {
-     gameOver = true
-   }
+   if (Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) >= 4) {
+     gameOver = true
+   }

Character Count: 1468

Comparison Operators

We can save one extra character if we change the comparison slightly to use > instead of >=:

-   if (Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) >= 4) {
+   if (Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) > 3) {

Character Count: 1467

Short-circuiting via &&

While we're looking at checking for a win, there are some other things we can do to make this shorter. Since we can check for truthiness in Javascript and many values are truthy, gameOver doesn't need to be a boolean and can in fact just be a number with a value of 0 or 1. Making gameOver a number will also allow us to exploit another trick to shorten many of our if statements which always require 4 characters at a minimum, even if the if has no block and no whitespace (if()). We can use the logical AND operator && to short-circuit if the expression is not truthy in conjunction with the increment operator ++, allowing us to write the code that sets gameOver to a truthy value like so:

-   if (Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) > 3) {
-     gameOver = true
-   }
+   Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) > 3 && gameOver++

If one of our match variables is greater than 3, gameOver will be incremented to 1 which passes our check for truthiness and results in 'WINNER!' being logged to the console as is required.

We can use this same technique for the console logging:

-   if (gameOver) {
-     console.log('WINNER!')
-   }
+   gameOver && console.log('WINNER!')

Character Count: 1430

Inline Functions

Next let's take a look at our updateMatches function. In code golf, particularly in ECMAScript 5, functions are expensive. A function declaration is at least 12 characters (function(){}) before you've even made it do anything, and the return statement adds another 6 for a total of 18.

Functions are great for code structure and reuse, but they're just too many characters to abide for our needs. Here is a new version of the matching algorithm with the updateMatches function inlined:

-function updateMatches(value, matches) {
- return value === playerTurn ? (matches || 0) + 1 : 0
-}
...
var matchesHoriz = matchesVert = matchesDiagonal1 = matchesDiagonal2 = 0
for (i = 0; i < 7; i++) {
  // Horizontal matches
- matchesHoriz = updateMatches(grid[rowIndex][i], matchesHoriz)
+ matchesHoriz = playerTurn == grid[rowIndex][i] && matchesHoriz + 1 || 0
  if (i < 6) {
    // Vertical matches
-   matchesVert = updateMatches(grid[i][colIndex], matchesVert)
+   matchesVert = playerTurn == grid[i][colIndex] && matchesVert + 1 || 0
    // Diagonal matches
-   matchesDiagonal1 = updateMatches(
-     grid[i][colIndex + rowIndex - i], matchesDiagonal1)
+   matchesDiagonal1 = playerTurn == grid[i][colIndex + rowIndex - i] && matchesDiagonal1 + 1 || 0
-   matchesDiagonal2 = updateMatches(
-     grid[i][colIndex - rowIndex + i], matchesDiagonal2)
+   matchesDiagonal2 = playerTurn == grid[i][colIndex - rowIndex + i] && matchesDiagonal2 + 1 || 0
  }
  // Check win
  Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) > 3 && gameOver++
}

Note that the multiple variable assignment to set all the matching variables to zero may not behave the way you assume; only the first variable is scoped to the current function whereas the next three are declared in global scope6. For example:

> (function() { var x = y = z = 0 })()
undefined
> x
ReferenceError: x is not defined
> y
0
> z
0

This doesn't change anything for our program and is a very compact way to assign multiple variables, but don't try this in your production code!

Character Count: 1361

Loops & Truthiness

Let's revisit how we're dropping the discs to the last free cell in a column:

  // Drop into column
  var i, rowIndex
  for (i = 5; i >= 0; i--) {
    if (typeof grid[i][colIndex] == void 0) {
      rowIndex = i
      break
    }
  }
  if (typeof rowIndex == void 0) {
    return
  }

Again, this is a reasonable use of a for loop where we start at the bottom row and decrement the row index until we get to a free cell. If we don't find one even after checking every cell in the column, we bail.

But we can do better:

  // Drop into column
- var i, rowIndex
- for (i = 5; i >= 0; i--) {
-   if (typeof grid[i][colIndex] == void 0) {
-     rowIndex = i
-     break
-   }
- }
+ rowIndex = 6
+ while (grid[--rowIndex] && grid[rowIndex][colIndex] + 1);

Exploiting the fact that we can both mutate a variable and check the loop condition in one shot and that the body of a while loop is in fact optional, the loop condition will both decrement rowIndex and check the value of the cell for truthiness to determine whether or not to continue looping. Crucially, we put the decrement operator before the variable (--rowIndex) which will decrement and return the decremented value, as opposed to putting the decrement operator after the variable which will decrement but return the value of rowIndex before decrementing. For example:

> x = 5
5
> --x
4
> --x
3
> x--
3
> x
2

Since we are checking for truthiness, we need to include the + 1 so that we can differentiate between a free cell which will be undefined and a cell that has a zero in it (1 is of course truthy, but both 0 and undefined are falsey). By adding one to the value of the cell, we'll be covered in all cases:

> 1 + 1  // truthy
2
> 0 + 1  // truthy
1
> undefined + 1  // falsey
NaN

Character Count: 1309

Single exit point

Instead of bailing out early if the column is full with return, we can save characters by having a single exit point for our function and wrapping the rest in an if statement. While we're at it, we can do the same for our original return-early if statement:

- // Drop into column
- if (gameOver || colIndex == void 0) {
-   return
- }
- var i, rowIndex
- for (i = 5; i >= 0; i--) {
-   if (typeof grid[i][colIndex] == void 0) {
-     rowIndex = i
-     break
-   }
- }
- if (typeof rowIndex === "undefined") {
-   return
- }
+ if (!gameOver && colIndex + 1) {
+   // Drop into column
+   rowIndex = 6
+   while (grid[--rowIndex] && grid[rowIndex][colIndex] + 1);
+   if (rowIndex + 1) {
      // ... check matches ...
+   }
+ }

In the first conditional we'll notice a similar technique in adding the +1 to avoid the conditional failing on zero while still rejecting colIndex if it is undefined.

Assuming we decline to indent properly for the two new if blocks to avoid adding lots of whitespace and throwing off our count, this represents a net savings of 33 characters.

Character Count: 1276

Refactoring

At this point, to continue driving our character count down, we're going to have to think outside the box.

Representing our game board state as an array of arrays seems like a reasonable way to go about things since the board is in fact a grid. It makes it easy to iterate and check specific indices to look for matches. Moreover, we already have the column and row indices since the event target will helpfully tell us these things. We also need to know the column and row indices to update the table in a compact way, so holding onto those values makes sense. A grid seems like a good choice.

But what if we could represent the grid differently for the purposes of checking for a win, the logic for which represents the bulk of our code? What if there was a powerful way to check for matches in few characters? If you thought strings and regular expressions, you thought right!

...
-var grid = [[], [], [], [], [], []]
+var gridString = Array(43).join()
...
- while (grid[--rowIndex] && grid[rowIndex][colIndex] + 1);
+ for (var index = 35 + colIndex; index >= 0 && gridString[index] != ","; index -= 7);
...
- if (rowIndex + 1) {
+ if (index >= 0) {
    // Keep track of latest move
-   grid[rowIndex][colIndex] = playerTurn
+   gridString = gridString.slice(0, index) + playerTurn + gridString.slice(index + 1);
    // Count matches
-   var matchesHoriz = matchesVert = matchesDiagonal1 = matchesDiagonal2 = 0
-   for (i = 0; i < 7; i++) {
-     // Horizontal matches
-     matchesHoriz = playerTurn == grid[rowIndex][i] && matchesHoriz + 1 || 0
-     if (i < 6) {
-       // Vertical matches
-       matchesVert = playerTurn == grid[i][colIndex] && matchesVert + 1 || 0
-       // Diagonal matches
-       matchesDiagonal1 = playerTurn == grid[i][colIndex + rowIndex - i] && matchesDiagonal1 + 1 || 0
-       matchesDiagonal2 = playerTurn == grid[i][colIndex - rowIndex + i] && matchesDiagonal2 + 1 || 0
-     }
-     // Check win
-     Math.max(matchesHoriz, matchesVert, matchesDiagonal1, matchesDiagonal2) > 3 && gameOver++
-   }
+   /(\d)(\1{3}|(.{6}\1){3}|(.{7}\1){3}|(.{5}\1){3})/.exec(gridString) && ++gameOver && console.log('WINNER!')
  }
...
  // Update board
- this.rows[rowIndex].cells[colIndex].style.background =
+ this.rows[~~(index / 7)].cells[index % 7].style.background =
    playerTurn ? "red" : "#ff0"
...

Instead of working with an array of arrays, we work with a flat string representation of the grid (gridString). Since ES5 doesn't have a way to easily initialize a string with values7, we use a bit of a hack to initialize the string with all commas by using the Array constructor to create an array of a certain size and then call Array.prototype.join (commas are the default separator). The reason why we give it a length of 43 instead of 42 is because Array.prototype.join() puts the string between the elements in the array:

> Array(43).join()
',,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,'
> Array(43).join().length
42

Dropping the disc is conceptually similar to how it was before except that when we go up a "row" we now have to jump 7 cells at a time since our string is flat.

Setting the value of the latest turn has become slightly more complex as we must now cut the string apart using String.prototype.slice() around the index and then put the two pieces back together again around playerTurn to form our new string.

Of course, checking for a win is where this approach really shines as it all comes down to just matching on a single regex.

Let's examine the regex itself separately. Our compact regex is equivalent to this verbose version (note that this is not syntactically valid in Javascript and is purely for illustrative purposes):

/
  (\d)            // match a single digit (0 or 1 for a player turn)
                  // this is the first capture group and can be referenced
                  // using \1 in the rest of the regex
  (
    \1{3}         // match across -- that same digit (\1) repeated 3 more times
    |             // OR
    (.{6}\1){3}   // match down -- skip 6 cells to get to the next row and match
                  // that same digit (\1) again 3 more times
    |             // OR
    (.{7}\1){3}   // match across top-left to bottom-right -- skip 7 cells to
                  // offset each successive row by one more cell,
                  // matching that same digit (\1) again 3 more times
    |             // OR
    (.{5}\1){3}   // match across top-right to bottom-left -- skip 5 cells to
                  // offset each successive row by one less cell,
                  // matching that same digit (\1) again 3 more times
  )
/

The fact that the call to RegExp.prototype.exec() is an expression allows us to write a one-liner to check for a match and, if found, increment gameOver and log 'WINNER!' to the console.

Finally, to update the game board, we make use of a few more operators. The bitwise NOT operator ~ inverts the bits of its operand and when used twice is equivalent to Math.floor for positive numbers -- it rounds down to the nearest integer. The modulo operator % will return the remainder after dividing the second operand by the first. These are used to get the row and column indices from the current index.

Character Count: 779

Global Namespace

As a final "optimization", we can remove all variable declarations (all occurrences of var). When assigning a value to an undeclared variable (e.g. x = 1), the variable will be thrown into global scope (when not in strict mode). This is terrible practice since it makes your variable globally available and clobbers that value if it already exists, but it saves us another 20 characters!

Character Count: 757

Wrapping Up

Here is the code (with proper indentation added back in for readability):

playerTurn = gameOver = 0
gridString = Array(43).join()

document.body.children[0].onclick = function(e) {
  colIndex = e.target.cellIndex
  if (!gameOver && colIndex + 1) {
    // Drop into column
    for (index = 35 + colIndex; index >= 0 && gridString[index] != ","; index -= 7);
    if (index >= 0) {
      // Keep track of latest move
      gridString = gridString.slice(0, index) + playerTurn + gridString.slice(index + 1);
      // Count matches
      /(\d)(\1{3}|(.{6}\1){3}|(.{7}\1){3}|(.{5}\1){3})/.exec(gridString) && ++gameOver && console.log('WINNER!')
      // Update board
      this.rows[~~(index / 7)].cells[index % 7].style.background =
        playerTurn ? "red" : "#ff0"
      // Switch turns
      playerTurn = +!playerTurn
    }
  }
}

Final Version

Up until this point, we have preserved whitespace, comments, and descriptive variable names, all of which make the code much easier to understand and maintain but take up precious characters. If we minify our code (remove all comments, whitespace, and rename all variables to be only one letter, among other compression tricks), we get this:

t=o=0,g=Array(43).join(),document.body.children[0].onclick=function(e){if(c=e.target.cellIndex,!o&&c+1){for(i=35+c;i>=0&&","!=g[i];i-=7);i>=0&&(g=g.slice(0,i)+t+g.slice(i+1),/(\d)(\1{3}|(.{6}\1){3}|(.{7}\1){3}|(.{5}\1){3})/.exec(g)&&++o&&console.log("WINNER!"),this.rows[~~(i/7)].cells[i%7].style.background=t?"red":"#ff0",t=+!t)}}

Character Count: 332

Recap

Understanding Javascript

Here are some lessons about Javascript that we have touched on:

Value of Code Golf

Code golfing is valuable in that you learn to approach coding from a totally different perspective. While it is clearly not desirable to write code that is inefficient or whose intent or purpose is impenetrable to others, writing code in a compact manner does have its advantages. All things being equal, less code is less cognitive overhead, less room for bugs, and less to read and maintain!

While the specific coding style and techniques used for code golfing should not be emulated in production code (please no!), the terseness it exhibits is a desirable quality and the learning involved in pushing your implementation to be as concise as possible can make you a better programmer.

PRs Welcome

I hope you've enjoyed this adventure in code golf in Javascript and have learned a thing or two about the language. If you would like to try your hand at creating an even shorter version of the code, please check out the connect-fore repo:

https://github.com/pcaisse/connect-fore

The project has automated tests, continuous integration, and helpful commands to ease development. Please take a swing at beating the current low score!

Happy golfing! ⛳️ 🏌️


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK